New to Kestra?
Use blueprints to kickstart your first workflows.
Catch dbt models that got slower without failing. Kestra stores run_results timings in DuckDB and alerts Slack when a model exceeds its median.
A dbt model that gets slower does not fail. It still builds, its tests still pass, and the warehouse bill and the morning dashboards are the first to notice. Models slow down for ordinary reasons: a join that starts fanning out, an incremental model whose filter stops pruning and rebuilds the whole table, a source that grows ten times in a quarter. On a warehouse billed by compute time, a model that went from 40 seconds to 6 minutes costs nine times as much every run, and it can delay every model downstream of it.
dbt writes the execution time of every model to run_results.json, but it only keeps the latest run, so there is nothing to compare against. This blueprint keeps that history. After each dbt build it stores every model's execution time in a DuckDB table, compares the run with the median of each model's previous runs, and posts the models that got slower to Slack.
clone_repository checks out the dbt project with io.kestra.plugin.git.Clone, inside a WorkingDirectory so the next task sees the files.dbt_build runs dbt build with io.kestra.plugin.dbt.cli.DbtCLI in the ghcr.io/kestra-io/dbt-duckdb image, on the jaffle_shop project from kestra-io/dbt-demo with an in-memory DuckDB profile. The task uploads target/run_results.json and exposes it as outputs.dbt_build.outputFiles['run_results.json'].record_run (io.kestra.plugin.jdbc.duckdb.Queries) reads that file with read_json_auto and appends one row per successful model to dbt_model_runs: execution ID, dbt invocation ID, timestamp, model unique ID and execution time.find_regressions (io.kestra.plugin.jdbc.duckdb.Query) takes, for each model, the median of its previous baseline_runs runs and returns the models from this run that are slower than that median by more than threshold_pct.alert_on_regressions (io.kestra.plugin.core.flow.If) posts the list to Slack when there is at least one regression, and logs a line otherwise.The median is used rather than the mean so that one unusually slow run does not raise the baseline and hide the next regression. A model needs at least three previous runs before it is checked, and models faster than min_duration_seconds in the current run are skipped, because a 0.2s model can easily show +200% from noise alone.
nightly (Schedule, 0 3 * * *, disabled by default): run after the nightly load.on_merge (Webhook): call /api/v1/main/executions/webhook/company.team/dbt-model-runtime-regression-alert/<key> from CI after a merge, so the regression is reported on the change that caused it./app/storage/dbt_runtime_history.duckdb, is the storage volume in the standard Kestra Docker setup. Set history_db to md:<database> to keep the history in MotherDuck instead (requires a motherduck_token).SLACK_WEBHOOK_URL: Slack incoming webhook URL the alert is posted to.| Input | Type | Default | Purpose |
|---|---|---|---|
threshold_pct |
INT | 50 |
How much slower than the baseline median a model must run to alert. |
baseline_runs |
INT | 7 |
Number of previous runs per model that make up the baseline. |
min_duration_seconds |
FLOAT | 5.0 |
Models faster than this in the current run are ignored. |
history_db |
STRING | /app/storage/dbt_runtime_history.duckdb |
DuckDB database that stores the history. |
The jaffle_shop models run in well under a second, so the default min_duration_seconds skips all of them. To try the alert on the demo, run the flow at least four times with min_duration_seconds set to 0, and raise threshold_pct to 200: below a second, run-to-run jitter alone can exceed 50%.
{{ outputs.dbt_build.outputFiles['run_results.json'] }}: the raw dbt run results of this execution.{{ outputs.find_regressions.rows }}: one row per regressed model with model, baseline_s, latest_s and pct_slower.dbt_model_runs table in the history database, which you can also query for runtime trends.ghcr.io/kestra-io/dbt-snowflake, and add a profiles block or your secrets.record_run, find_regressions and alert_on_regressions after your own DbtCLI task and change the inputFiles reference.adapter_response field of run_results.json, where your adapter reports bytes or credits.