CertSafari
    DBT Analytics Engineering· Lessons

    Domain 7 · Lesson 30/31

    dbt state and state selection: state:, result: and source_status:

    Understanding state and state selection

    18 min read
    3.23% of exam
    7 sources
    Published 12 Oct 2026
    Docs as of 12 Oct 2026

    What you will be able to do

    • Explain how a stateless, idempotent tool uses artifacts from a previous run, and which features need the --state flag
    • Choose between state:new, state:modified, state:old, state:unmodified and the state:modified subselectors
    • Predict which changes do and do not mark a node as modified, including tags, meta, macros, seeds and vars
    • Rerun work with result: selectors and source_status:fresher+, and avoid the result:fail+ trap
    • Configure --state, --defer and --defer-state, and keep CI from comparing against a manifest it just overwrote

    Key concept

    State (the --state manifest) — State is a saved set of artifacts, mainly manifest.json, from an earlier dbt invocation. When you pass its path to --state, dbt compares your current project with that snapshot to decide what to select, and the same snapshot tells it where unbuilt parents live.

    1.Why a stateless tool keeps state

    dbt is designed so that a run does not depend on earlier runs. If you run a model once or a thousand times on the same raw data, you get the same result. A run only needs the project code and the database objects that exist right now. This property is called idempotence, and every state feature depends on it: if a model returns different output on a rerun, it is not safe to skip, compare or defer.

    dbt still keeps a record of what happened, in the form of artifacts. The most important one is manifest.json, which describes every resource in the project, including models, tests, macros, sources and exposures, along with their configs and properties. Any command that parses the project writes it, which is every command except deps, clean, debug and init. Even a run that selects only a few models still lists every enabled resource in the manifest. By default the manifest is written to target/.

    Passing the path to an old manifest with --state lets dbt use that snapshot. Three features depend on it:

    - the state: selector, which finds new or modified resources by comparing the current code with the saved manifest - deferral, which points references to unbuilt, unselected parents at the environment recorded in the manifest - dbt clone, which clones nodes based on where the manifest says they live (see [dbt clone](lesson:4.2))

    Using the state selector and deferral together is called "slim CI": a pull request builds only what changed and reads everything else from production.

    Checkpoint 1 of 9· Check yourself

    A colleague says that once a CI job reads a production manifest through --state, its results are no longer reproducible. What is correct?

    Sources12

    2.Selecting new and modified nodes with state:

    The state: method compares nodes by unique_id. A node is new if the comparison manifest has no node with the same unique_id. A node is modified if it is new or if anything about an existing node changed. For each of these there is an inverse: state:old and state:unmodified, which are useful for excluding nodes that have not changed. Because a single modified check can catch more than you want, dbt also offers subselectors that each check one kind of change.

    state: selectors and what each one matches
    SelectorMatches
    state:newNo node with the same unique_id exists in the comparison manifest
    state:oldA node with the same unique_id exists in the comparison manifest
    state:modifiedAll new nodes, plus any change to existing nodes
    state:unmodifiedExisting nodes with no changes
    state:modified.bodyChanges to the node body, such as model SQL or seed values
    state:modified.configsConfig changes, excluding database/schema/alias/tags/meta
    state:modified.relationChanges to database/schema/alias
    state:modified.persisted_descriptionsDescription changes, only where persist_docs is enabled
    state:modified.macrosChanges to upstream macros, called directly or indirectly
    state:modified.contractChanges to a model's contract (column name and data_type)

    The rule behind modification is simple: a change counts if it could affect how the resource is materialized. That is why tags and meta are ignored. A description counts only when persist_docs writes it to the database. Every other config change counts. Plain state:modified also checks a few things specific to certain resource types, such as a source's freshness or quoting rules, an exposure's maturity, and changes to access, deprecation_date or latest_version, because each of these can change how references resolve. A changed contract is a special case: removing a column or changing its type is a breaking change and raises an error.

    Some changes are detected through dependencies, and some are not detected at all. A model that calls a changed macro is marked modified, and so is a model whose macro calls a changed macro. Seeds are compared in two ways. For a seed under 1 MiB, dbt stores a hash of the file, so editing one row is enough to mark it modified. For a seed of 1 MiB or more, dbt only checks whether the file path changed, and it warns that it cannot compare the contents. Changes that arrive through var or env_var are not detected as lineage. The model is caught only if the new value produces a different configuration.

    Simple state selection: run and test whatever changedbash
    dbt run -s "state:modified"
    dbt test -s "state:modified"

    Checkpoint 2 of 9· Check yourself

    An analyst corrects one row in a 3 MiB country_codes.csv seed and opens a pull request. The CI job runs dbt build --select state:modified+. What happens to the seed?

    Checkpoint 3 of 9· Exam question

    Your CI pipeline runs the following commands in order: `dbt build --select state:modified+ --state ./target --target-path ./target`, where `./target` holds both the previous deploy's manifest and the current run's compiled artifacts. On the next CI run, `state:modified+` silently returns no results even though model changes were merged in between. What is the most likely cause, and what should you change?

    Sources34

    3.Selecting by earlier results: result: and source_status:

    Run results are also part of state. dbt run, dbt test, dbt build and dbt seed each write run_results.json, which records each node's execution time and status. With result:<status> and --state, a later command can select nodes by how they finished last time. Not every status applies to every resource type: tests are the only nodes that fail, warn or pass, and tests are never reported as success or skipped.

    Which result statuses apply to which resource types
    Selectormodelseedsnapshottest
    result:erroryesyesyesyes
    result:successyesyesyes—
    result:skippedyesyesyes—
    result:fail———yes
    result:warn———yes
    result:pass———yes
    Rerunning errors, failed tests, and the models behind failed testsbash
    # run all models that generated errors on the prior invocation of dbt run
    dbt run --select "result:error" --state path/to/artifacts
    
    # run all tests that failed on the prior invocation of dbt test
    dbt test --select "result:fail" --state path/to/artifacts
    
    # run all the models associated with failed tests from the prior invocation of dbt build
    dbt build --select "1+result:fail" --state path/to/artifacts

    Where you put the graph operator matters. A test has no downstream nodes, so result:fail+ selects only the failed test. It does not select the model that produced the bad data. To rebuild that model, put the operator in front, as in 1+result:fail, which adds the test's direct parents. 1+result:fail+ also adds everything downstream of those parents. By contrast, result:error matches any resource type that errored. You can also combine state and result selectors in one command, so a single CI step can retry last run's errors and build new work at the same time. For rerunning a failed job from the point of failure, there is a dedicated command, [dbt retry](lesson:7.2).

    Checkpoint 4 of 9· Check yourself

    After a nightly dbt build, the unique test on fct_orders fails. You fix the model SQL and want to rebuild fct_orders and re-run the test. Which selector does that?

    Checkpoint 5 of 9· Fill the gap

    Complete this command so it captures last run's errors OR any new or modified models, plus their children.

    dbt run --select "result:<status>+"  ?  --defer --state ./<dbt-artifact-path>

    Source freshness is also stored as state. dbt source freshness writes sources.json, which records max_loaded_at for each source. The selector source_status:fresher+ compares two of these files and builds only the sources that received new data, plus everything downstream of them. The comparison needs a sources.json on both sides, so freshness has to run in the job that produced the state and again in the current job. Setting up freshness checks is covered in [source freshness](lesson:6.2).

    Building only the children of sources that got fresherbash
    # You can also set the DBT_ENGINE_STATE environment variable instead of the --state flag.
    dbt source freshness # must be run again to compare current to previous state
    dbt build --select "source_status:fresher+" --state path/to/prod/artifacts

    Checkpoint 6 of 9· Exam question

    Review this CI script: ```bash dbt deps dbt build --select state:modified+ --defer --state ./state cp target/manifest.json ./state/manifest.json ``` Each run mostly works, but regressions from two runs ago keep resurfacing as "modified" even when nothing has changed since the last successful run. Which line is misconfigured, and what should happen instead?

    Sources35

    4.Deferral and separate state paths

    State selection alone leaves a gap. If CI builds only a modified model_b, its ref('model_a') points to the CI schema, where model_a was never built, and the query fails. --defer closes that gap. dbt points a ref at the state manifest's environment only when two conditions hold: the referenced node is not selected, and it does not exist in the current target (or --favor-state is passed). Ephemeral models are never deferred, because they are inlined into the queries that reference them. Deferral needs both --defer and --state.

    Deferred run and test against saved production artifactsbash
    dbt run --select [...] --defer --state path/to/artifacts
    dbt test --select [...] --defer --state path/to/artifacts

    Comparison and deferral can use different manifests. --state sets the manifest you compare against to find changes, and --defer-state sets the manifest that unbuilt references resolve to. If --defer-state is not set, deferral uses --state. Usually that is what you want: compare against production and fall back to production. Use a separate path when you want to compare against one point in time but defer to an environment that was built at another.

    State and defer settings and their environment variables (dbt v1.11 and later)
    CLI flagEnvironment variableValue
    --stateDBT_ENGINE_STATEfile path
    --deferDBT_ENGINE_DEFERboolean
    --defer-stateDBT_ENGINE_DEFER_STATEfile path, deferral only (optional)

    If you set both a flag and its environment variable, the flag wins. The old variable names DBT_ARTIFACT_STATE_PATH and DBT_DEFER_TO_STATE were deprecated in dbt v1.5 and still work for now. The state artifacts must use a schema version that the running dbt version can read. dbt v1.8 through v2.0 all write manifest v12.

    Deferral can make tests behave unexpectedly. dbt test -s state:modified selects tests on new or modified resources and also tests that are themselves new or modified. A new relationships test between one modified model and one unmodified model will compare the CI copy of one with the production copy of the other. If CI uses limited or sampled data, that test is likely to fail for reasons unrelated to your change. Excluding those tests from the CI test step is one fix.

    Keeping cross-environment relationships tests out of CIbash
    dbt run -s "state:modified"
    dbt test -s "state:modified" --exclude "test_name:relationships"

    Checkpoint 7 of 9· Check yourself

    A CI job runs dbt run --select int_orders --defer --state prod-artifacts. None of int_orders' parents is selected or built in the CI schema. Which parent reference still resolves to the CI schema rather than to production?

    Sources65

    5.State pitfalls in CI and scheduled jobs

    Pointing --state at target/ breaks change detection. dbt overwrites target/manifest.json while it parses. It writes the new manifest first and reads it back afterwards, so it compares the project with itself, finds no changes, and may warn that the saved manifest was not found. Do not set --state and --target-path to the same location. There are three ways to fix the workflow:

    - move the generated manifest.json into its own folder, such as state/, before the next invocation - write the build stage's output to a different --target-path - pass --no-write-json when listing modified nodes, as in dbt ls --no-write-json --select state:modified --state target

    state:modified only notices changes to code and configuration. New rows in a source table are not a modification. A scheduled job that selects state:modified+ will succeed without building anything on nights when no code changed. That is expected behavior, and it is a sign the selector is in the wrong job. Use it in CI and merge jobs, where the goal is to build what a change touched. Production jobs that must build every time should not use it.

    Checkpoint 8 of 9· Check yourself

    A nightly production job runs dbt build --select state:modified+ --state prod-artifacts. New orders land every night, but the marts stopped updating last week. What is the fix?

    Configs that change by environment used to cause false positives. For example, a materialization set through env_var renders differently in dev and prod, so it could look modified when nothing changed. The behavior flag state_modified_compare_more_unrendered_values fixes this by comparing the unrendered Jinja text instead of the rendered values. It defaults to true from dbt v1.12 and the 2026.09 v1 Latest track, and in v2 the flag is removed and the behavior is always on (see [behavior flags](lesson:3.5)). Expect one side effect. The first CI run after the switch compares a baseline that stored rendered values with a new parse that stores Jinja text, so every node with Jinja in its config looks modified. That costs one extra rebuild. When production writes a new baseline manifest, the extra matches disappear.

    Checkpoint 9 of 9· Exam question

    A nightly `dbt build` runs 20 models. 12 complete successfully, 1 fails on a SQL compilation error, and 7 downstream models are skipped as a result. After fixing the SQL error in that one model, what is the most efficient way to complete the run without re-executing the 12 models that already succeeded?

    Sources47

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 1.Editing a model's tags or meta in YAML makes it state:modified, so CI will rebuild it.Why is that wrong?

      tags and meta are metadata-only, at both the resource and the column level. They never trigger state:modified. A description change counts only when persist_docs is enabled.

      Covered in Selecting new and modified nodes with state:

    2. 2.result:fail+ reruns the failed test along with the model it tests and that model's children.Why is that wrong?

      Tests have no downstream nodes, so result:fail+ returns only the test. Use 1+result:fail to include the tested model, or 1+result:fail+ to add its descendants too.

      Covered in Selecting by earlier results: result: and source_status:

    3. 3.state:modified+ will pick up models whose source tables received new rows.Why is that wrong?

      state:modified only detects changes to code, configuration and manifest metadata. When none of those changed, the job succeeds and builds nothing.

      Covered in State pitfalls in CI and scheduled jobs

    4. 4.Pointing --state at target/ in the same job is fine, because the previous manifest is already there.Why is that wrong?

      dbt overwrites target/manifest.json during parsing, before it reads it for comparison. Keep the saved manifest in a separate folder or use a different --target-path.

      Covered in State pitfalls in CI and scheduled jobs

    Practise it for real

    Save a baseline manifest and use state:modified to list exactly what an edit changed

    1. 1.Run any parsing command, such as dbt ls, then copy target/manifest.json into a new folder called state/.

      Why: The baseline has to live outside target/, because dbt overwrites target/manifest.json every time it parses.

      You should see: state/manifest.json exists and will not change when you run dbt again.

    2. 2.Run dbt ls --select "state:modified" --state state/ without editing anything.

      Why: This confirms that the comparison is against the saved snapshot and not against a manifest that was just rewritten.

      You should see: No nodes are listed.

    3. 3.Add a tag to one model's YAML, then run the same dbt ls command again.

      Why: tags are metadata, not modifications.

      You should see: No nodes are listed, even though the YAML changed.

    4. 4.Edit that model's SQL and run dbt ls --select "state:modified+" --state state/.

      Why: A body change counts as a modification, and + adds everything downstream.

      You should see: The edited model and its children are listed.

    Stuck? Get a nudge

    If step 2 lists nodes you did not touch, check whether your config uses Jinja such as env_var and which dbt version wrote the baseline.

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 1.
      “dbt can leverage artifacts from a prior invocation as long as their file path is passed to the --state flag.”
      ↩︎ Why a stateless tool keeps state
      “dbt can leverage artifacts from a prior invocation as long as their file path is passed to the --state flag.”
      ↩︎ Key concept
      “given the same manifest and the same raw data, dbt will produce the same transformed result.”
      ↩︎ Checkpoint
    2. 2.
      “This file contains a full representation of your dbt project's resources”
      ↩︎ Why a stateless tool keeps state
    3. 3.
      “state:new: There is no node with the same unique_id in the comparison manifest”
      ↩︎ Selecting new and modified nodes with state:
      “can be used to select resources based on their result status from a prior run”
      ↩︎ Selecting by earlier results: result: and source_status:
      “using the result:fail+ selector will only return the failed test itself and not the model or anything built on top of it”
      ↩︎ Exam trap 2
      “Changing a tags or meta value (including on individual columns in a YAML file) doesn't trigger state:modified”
      ↩︎ Prediction
      “Seed files 1 MiB or larger — Included in the state:modified selector only when the seed file path changes.”
      ↩︎ Checkpoint
      “Tests don't have downstream nodes in the DAG, so using the result:fail+ selector will only return the failed test itself”
      ↩︎ Checkpoint
    4. 4.
      “dbt will mark modified any resource that depends on a changed macro, or on a macro that depends on a changed macro.”
      ↩︎ Selecting new and modified nodes with state:
      “First, it overwrites target/manifest.json before it can be used for change detection.”
      ↩︎ State pitfalls in CI and scheduled jobs
      “Changes to tags and meta, whether set at the resource level or on individual columns in a YAML file, don't count as modifications”
      ↩︎ Exam trap 1
      “When dbt detects no code or configuration changes since the deferred manifest was last produced, the job succeeds without building any models.”
      ↩︎ Exam trap 3
      “Avoid setting --state and --target-path to the same path with state-dependent features like --defer and state:modified”
      ↩︎ Exam trap 4
      “If your job needs to build models on every scheduled run regardless of code changes, remove the state:modified selector from that job.”
      ↩︎ Checkpoint
    5. 5.
      “This requires both the previous and current states to have the sources.json artifact available.”
      ↩︎ Selecting by earlier results: result: and source_status:
      “If both the flag and env var are provided, the flag takes precedence.”
      ↩︎ Deferral and separate state paths
    6. 6.
      “If --defer-state is not specified, deferral will use the manifest supplied to --state.”
      ↩︎ Deferral and separate state paths
      “Ephemeral models are never deferred”
      ↩︎ Checkpoint
    7. 7.
      “In dbt v1.12, state_modified_compare_more_unrendered_values defaults to true, reducing false positives during state:modified checks”
      ↩︎ State pitfalls in CI and scheduled jobs

    Ready to test yourself?

    Practise the 11 questions on this subdomain.

    Spotted a mistake, or was something unclear? Tell us.