skip to content

In dbt, why does dbt build skip downstream models on a test failure when dbt run then dbt test does not?

level: seniorimportance: should knowfreq 52%

answer

  1. one command interleaves, two commands phase
  2. the DAG decides what runs when
  3. descendants are skipped, not failed
  4. dbt build versus dbt run then dbt test

basics

~20 s

dbt build walks the DAG node by node, running each model and then its own tests before moving on, so a failed test marks that node an error and its descendants are skipped. dbt run then dbt test builds everything first and only tests afterwards.

solid answer

~40 s

`dbt run` followed by `dbt test` is two sequential phases: every model is built, *then* every test runs. If `stg_orders` has duplicated keys, the whole mart is already built on top of them by the time the test reports — the tests tell you what you published. `dbt build` interleaves instead: it executes seeds, snapshots, models and tests in a single dependency graph, so a model's tests run immediately after that model and before anything that depends on it. A test failure marks the node as an error, and dbt skips its descendants, which stops bad data propagating into the mart. That is why `dbt build` is the recommended production command. Note the ordering guarantee still comes from the DAG: tests must `ref()` their models to be positioned correctly.

code

bash · 9 lines
bash
# production: interleaved, stops bad data propagating
dbt build --target prod

# two phases: every model built before any test runs
dbt run --target prod
dbt test --target prod

# CI on a pull request: only what changed, plus descendants
dbt build --select state:modified+ --defer --state ./prod-artifacts --fail-fast

go deeper

for a junior

Know that dbt build is the command that runs models and their tests together, and that a test failure there stops the models that depend on it from being built.

for a middle

Explain the mechanism: one combined DAG across seeds, snapshots, models and tests, versus two separate phases where every model is built before any test runs.

for a senior

Argue the operational consequence — stale but correct beats fresh but wrong — and name the failure mode where a test without ref() gives no ordering guarantee at all.

for a principal

Own what the production invocation looks like: build with which selection, fail-fast in CI only, retry semantics, and how the team decides when a skipped mart is escalated versus tolerated.

## Two execution shapes `dbt run` and `dbt test` are *phase-ordered*. `dbt run` selects the model nodes and executes them in dependency order until every model is built. `dbt test` then selects the test nodes and executes those. Nothing about the first phase consults the second. If a staging model produced duplicate primary keys tonight, the fact table, the aggregate and the mart every dashboard reads were all built on those duplicates before the first test ran. `dbt build` is *graph-ordered* across node types. It builds one combined DAG containing seeds, snapshots, models and tests, then executes it in dependency order. Because a generic test declared on `stg_orders` has `stg_orders` as its parent, and `fct_orders` also has `stg_orders` as a parent, the scheduler runs the model, then its tests, then — only if those passed — the downstream model. ```text seed raw_countries | stg_orders -> [unique_stg_orders_order_id, not_null_stg_orders_order_id] | | +------------------------+---> fct_orders -> [tests on fct_orders] | mart_revenue ``` When `unique_stg_orders_order_id` errors, `fct_orders` and `mart_revenue` are reported as **skipped**, not failed. The distinction matters in the summary: skipped means dbt chose not to attempt it because an ancestor was unhealthy. ## What that buys you The practical value is blast radius. With `run` then `test`, the bad data is in production and your options are to roll back or to publish a caveat. With `build`, the last known-good version of `fct_orders` is still what consumers see, because tonight's version was never written. A stale mart is almost always better than a wrong one, and "stale but correct" is exactly the trade-off `dbt build` encodes. ## The catch: tests must be positioned in the graph The guarantee comes entirely from dependencies. A generic test declared in YAML always has the right parent. A singular test that hardcodes `analytics.stg_orders` instead of `{{ ref('stg_orders') }}` has *no* parents, so dbt is free to run it at any point — quite possibly before the model it checks is rebuilt. This is the most common reason a team believes `dbt build` protected them when it did not. ## Indirect selection When you run `dbt build --select fct_orders`, which tests come along? dbt's default `--indirect-selection` mode is `eager`: it includes any test that touches the selected node, even if the test also references models outside the selection. `cautious` includes only tests whose every parent is in the selection — safer when you are building a subset and do not want tests failing because of an unselected, unbuilt neighbour. `buildable` sits in between. Knowing this flag exists is a genuine seniority signal, because it explains the confusing case of "my subset build failed on a test about a model I did not select". ## Other flags that shape a failing run - `--fail-fast` (`-x`) aborts the whole invocation on the first failure rather than continuing with unrelated branches. Good for CI, bad for a nightly run where you want the independent branches to finish. - `--warn-error` promotes warnings to errors, which combined with `build` means a warn-severity test now also skips downstream models. - `--exclude` lets you drop a known-failing test to unblock a single incident run. - `dbt retry` re-runs the previous invocation from the point of failure using `run_results.json`, so you do not rebuild the hours of work that already succeeded. ## What build covers that run does not `dbt build` is not only "run plus test". It also executes **seeds** (as `dbt seed` would) and **snapshots** (as `dbt snapshot` would), in graph order. That matters because a snapshot must capture source state *before* the models that read it are rebuilt, and a seed feeding a lookup must load first. Teams that scripted `dbt seed && dbt snapshot && dbt run && dbt test` in a shell were approximating build with a fixed phase order; `build` derives the order from the actual graph. ## When run and test separately is still right Development. Iterating on one model, you often want `dbt run --select my_model` repeatedly and to test only when you are ready. Some teams also keep them separate when they deliberately want the full build to complete so they can inspect everything, accepting bad data in a dev schema. In production, though, `dbt build` is the default answer and saying otherwise needs a reason.

  • What node types does dbt build execute besides models and tests?
    Seeds and snapshots as well, all in one dependency graph. That is why it replaces a scripted `dbt seed && dbt snapshot && dbt run && dbt test` chain: instead of a fixed phase order, the actual DAG decides that a snapshot capturing source state runs before the models reading it, and a lookup seed loads before its consumers.
  • Why might a singular test not protect downstream models even under dbt build?
    Because ordering comes from dependencies. If the test file hardcodes a schema-qualified table name instead of using `ref()`, the test node has no parents and dbt may schedule it anywhere — including before the model is rebuilt. Using `ref()` is what places it between the model and its descendants.
  • What does --fail-fast change during a dbt build?
    It aborts the entire invocation as soon as any node fails, rather than continuing to execute independent branches of the graph. It shortens the CI feedback loop, but in a nightly production run it means unrelated marts that would have built fine are left stale, so most teams enable it in CI only.
  • What is indirect selection and when does it bite?
    When you select a subset, dbt also pulls in tests touching those nodes. The default eager mode includes tests that reference unselected models too, so a partial build can fail on a test about a neighbour you never built. Switching to `--indirect-selection cautious` restricts it to tests whose parents are all selected.

Phase-ordered is inspecting the whole car after assembly; graph-ordered is checking each part as it goes on and stopping the line when one fails.

saying these in an interview costs you the question

  • Says dbt build is just an alias for run plus test
  • Thinks downstream models are marked failed rather than skipped
  • Believes tests run before their model in dbt run
  • Assumes a hardcoded table name in a test still orders correctly
  • Claims dbt rolls back a model when its test fails

context