CertSafari
    DBT Architect· Lessons

    Domain 4 · Lesson 16/39

    Set up a dbt CI job with deferral

    Set up a CI job with deferral

    13 min read
    2.56% of exam
    5 sources
    Published 12 Oct 2026
    Docs as of 12 Oct 2026

    What you will be able to do

    • Prepare the environments a CI job needs before its first run
    • Create a continuous integration job and choose its trigger, command and deferral settings
    • Explain how deferral lets state:modified+ build only changed models while reading unchanged parents from production
    • Diagnose temporary-schema and comparison-manifest problems in a running CI setup

    Key concept

    Deferral in a CI job — A CI job compares the pull request's code with the manifest from a chosen deferred environment, usually Production. It builds only what changed and points every unbuilt parent at that environment's existing tables.

    1.What a CI job does and what it needs first

    A CI job checks a pull request before it reaches production. dbt runs it when someone opens a pull request, then builds and tests only the models that changed and the models downstream of them. Those changes are checked against the rest of the project without rebuilding everything. Teams care about this because a model broken in a PR then fails in a check, not in a report someone opens the next morning. When the run finishes, dbt posts the status back to the pull request in GitHub, GitLab or Azure DevOps.

    The trigger comes from your Git provider. GitHub, GitLab and Azure DevOps have native dbt integrations, which let dbt create CI jobs that fire automatically. Other providers, such as Bitbucket or AWS CodeCommit, import the repository with Git clone and have to start CI runs through the Administrative API. An API-triggered run must include a pull request ID field (for example github_pull_request_id or non_native_pull_request_id) and either git_sha or git_branch. That route has a cost: dbt does not delete the PR's temporary schema for you, because automatic cleanup depends on incoming webhooks that only the native integrations send.

    Three things must already exist before CI has anything to defer to:

    - A dedicated CI environment. dbt Labs recommends a separate deployment environment, usually named CI and connected to a staging database, so temporary PR schemas stay apart from production builds. You can reuse the Production schema name in its deployment credentials, because dbt generates a separate schema name for each PR. Setting up the environment itself is covered in [creating a deployment environment](lesson:3.5). - A marked Production environment. The Set as Production environment checkbox must be selected. Only one deployment environment per project can carry it. - A successful production run. Deferral needs artifacts, and those come only from a run that has already succeeded. CI runs don't count: deferral uses the most recent successful production job, excluding CI jobs.

    CI also works for pull requests that target a branch other than main. For a long-lived staging branch, you link a CI environment to that branch through [custom branch settings](lesson:3.7). Its job then runs only for PRs opened against that branch.

    Checkpoint 1 of 7· Check yourself

    A team has created a CI environment and selected the Set as Production environment checkbox on Production. Production was created yesterday and no job has run in it yet. What still stops CI deferral from working?

    Checkpoint 2 of 7· Exam question

    A dbt Cloud architect creates a new CI job with the run step `dbt build --select state:modified+`. Pull requests against this job build every model in the project instead of only the modified ones. What is the most likely cause, and how should the architect fix it?

    Sources12

    2.Creating the CI job and its trigger

    You create a CI job from the environment it will run in. On the deployment environment page, choose Create job > Continuous integration job, or use the Continuous Integration Job template and give the job a name such as CI Check. The Environment field defaults to the environment you started from. The rest of the page comes pre-filled with values dbt Labs recommends, and most teams leave them alone.

    The Git trigger decides which pull request events start a run. Triggered by pull requests is on by default, so the job runs whenever a developer opens a pull request or pushes a commit to an existing one. Draft pull requests are excluded by default. Run on draft pull request is a separate opt-in for teams that want checks before a PR is marked ready. If developers report that their drafts never get a check, this setting is the reason.

    The CI job settings that shape a run, with their defaults
    SettingDefaultWhat it controls
    Triggered by pull requestsEnabledRuns the job each time a PR is opened or receives a new commit
    Run on draft pull requestOffAlso runs the job for draft PRs and commits pushed to them
    Commandsdbt build --select state:modified+Builds only new or changed models and their downstream dependents
    Compare changes against an environment (Deferral)Production environment, if one existsWhich environment's state the PR is compared with and deferred to
    Run timeoutSet per job; 3600 when dbt compare is onCancels the run if it exceeds the timeout, protecting warehouse resources

    The CI page also has an optional Advanced settings section with environment variables, target name, dbt version and threads. A common use is setting an environment variable so a CI run processes only a subset of data. Those overrides are covered in [job-level optional settings](lesson:4.6). Enterprise accounts can additionally switch on record-level diffing, which belongs to [Advanced CI and compare changes](lesson:4.9).

    Checkpoint 3 of 7· Match them up

    Match each CI job setting to its effect

    Tap a term, then the definition that fits it.

    Sources1

    3.How deferral makes state:modified+ work

    The default command, dbt build --select state:modified+, is what people call "Slim CI". dbt build runs seeds, models, snapshots and tests in DAG order, and skips the children of anything that fails. The state:modified+ selector limits the run to modified nodes and their children. You save the time of rebuilding unchanged nodes and, more importantly, the compute cost.

    Checkpoint 4 of 7· Fill the gap

    Which selector completes the preset CI command so that only modified nodes and their children are built?

    dbt build --select  ? 

    A selector called "modified" only works if dbt has something to compare against, and the deferral setting supplies it. State comparison works only when a deferred environment is selected. By default that is Production, and dbt uses the last successful run of any job in that environment as its comparison state. If Production is marked correctly, you don't have to change anything. If it isn't, pick the right environment from the dropdown. Without a deferred environment, the preset command has nothing to compare with. The environment-side setup is covered in [configuring deferral to other environments](lesson:3.8).

    Deferral is the second half of the mechanism. Comparison decides what to build, and deferral decides where the unbuilt parents come from. Normally dbt resolves each ref inside the run's own target schema. With deferral on, a ref resolves to the deferred environment instead, but only when the referenced node is not selected and does not exist in the target. That second condition is skipped if --favor-state is passed. Ephemeral models are never deferred, because they only pass through to other refs. The same rules apply to tests, so a relationships test on a changed model may join the PR's table to a production parent. That is useful, but it does mean the test spans two environments.

    The command-line form of the same behaviour: build one model and resolve its unbuilt parents from a production manifestbash
    dbt run --select "model_b" --defer --state prod-run-artifacts

    In a dbt CI job you never type those flags. The job's deferral setting supplies the state and the defer behaviour. A job can also defer to its own last successful run, which is covered in [self-deferral](lesson:4.10). Which deferral type fits which job is covered in [choosing a deferral type](lesson:4.11).

    Checkpoint 5 of 7· Exam question

    A CI job has Compare changes against an environment set to 'This job (self)'. After several pull requests, developers start seeing schema-related run failures that reference tables from an unrelated, previously closed pull request. What change resolves this?

    Sources134

    4.Temporary schemas, concurrency and comparison drift

    Every CI run writes to a temporary schema owned by its pull request, named dbt_cloud_pr_<job_id>_<pr_id> (for example dbt_cloud_pr_1862_1704). dbt drops the schema when the PR is merged or closed. Two common setups break that cleanup. One is a custom generate_schema_name macro that drops the dbt_cloud_pr_ prefix. The other is a macro that creates a schema no dbt model writes to, because dbt doesn't drop temporary schemas that weren't written to by a model run.

    Because each PR has its own schema, the scheduler handles CI runs differently from deploy jobs:

    - Runs for different PR numbers execute concurrently, so ten developers each get their own run. - Runs for the same PR with different commit SHAs execute serially. A new commit cancels the stale in-flight run and replaces it with the fresh code. - CI runs don't consume a run slot and never block a production run.

    Comparison drift is the failure teams hit once CI is busy. The comparison manifest is whatever the latest successful run in the deferred environment produced, and *any* job that updates artifacts there can produce it. If several jobs with different settings run in that environment, the comparison state changes in ways that are hard to predict. Busy staging, QA or UAT environments make this worse, which is why Production is the safer default. Even Production moves: if another PR merges and a deploy refreshes the manifest while your PR is still open, your run picks up unrelated state:modified nodes. You also cannot pin a single job to defer to, because built-in deferral targets an environment.

    There are two fixes. The first is to merge or rebase the latest base branch into the open pull request, so its code matches the new comparison state. The second is to refresh the manifest quickly after each merge: add a merge job in the deferred environment that runs dbt parse --no-partial-parse (or dbt compile), instead of waiting for a long deploy to finish.

    Checkpoint 6 of 7· Check yourself

    After a busy afternoon of merges, open PRs start building models their authors never touched. CI defers to Production. Which change addresses the root cause without waiting for the nightly deploy?

    Checkpoint 7 of 7· Exam question

    An architect reviews a custom macro that overrides dbt Cloud's default PR schema naming. dbt Cloud is failing to automatically drop temporary CI schemas after pull requests close. Which line in the macro causes this failure? ``` {% macro generate_schema_name(custom_schema_name, node) -%} {%- set default_schema = target.schema -%} {%- if target.name == 'ci' -%} {{ 'pr_' ~ var('pr_id') ~ '_' ~ default_schema }} {%- else -%} {{ default_schema }} {%- endif -%} {%- endmacro %} ```

    Sources531

    Exam traps

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

    1. 1.The preset state:modified+ command works on its own, even with no deferred environment selected.Why is that wrong?

      dbt can only tell what is modified by comparing against a deferred environment's state. Production is the default, and without a deferred environment there is nothing to compare against.

      Covered in How deferral makes state:modified+ work

    2. 2.A CI job can be set to defer to one specific deploy job, so other jobs in that environment don't affect it.Why is that wrong?

      Deferral picks an environment. Any job that updates artifacts there can become the comparison state.

      Covered in Temporary schemas, concurrency and comparison drift

    3. 3.Only the main production deploy job sets the comparison state CI uses.Why is that wrong?

      The latest successful run of any artifact-producing job in the deferred environment becomes the comparison manifest.

      Covered in Temporary schemas, concurrency and comparison drift

    4. 4.A CI job triggered through the Administrative API cleans up its temporary PR schema just like a natively triggered one.Why is that wrong?

      Automatic schema deletion depends on webhooks from native Git integrations, so API-triggered CI leaves the schema in place.

      Covered in What a CI job does and what it needs first

    Sources

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

    1. 1.
      “You can set up continuous integration (CI) jobs to run when someone opens a new pull request (PR) in your Git repository.”
      ↩︎ What a CI job does and what it needs first
      “dbt Labs recommends that you create your CI job in a dedicated dbt deployment environment that's connected to a staging database.”
      ↩︎ What a CI job does and what it needs first
      “Provide the git_sha or git_branch to target the correct commit or branch to run the job against.”
      ↩︎ What a CI job does and what it needs first
      “On your deployment environment page, click Create job > Continuous integration job to create a new CI job.”
      ↩︎ Creating the CI job and its trigger
      “opens up a pull request or pushes a commit to an existing pull request”
      ↩︎ Creating the CI job and its trigger
      “Cancel the CI job if the run time exceeds the timeout value.”
      ↩︎ Creating the CI job and its trigger
      “This informs dbt to build only new or changed models and their downstream dependents.”
      ↩︎ How deferral makes state:modified+ work
      “merge or rebase the latest base branch into your pull request so your branch includes recent merges.”
      ↩︎ Temporary schemas, concurrency and comparison drift
      “dbt doesn't drop temporary schemas that weren't written to as a result of running a dbt model.”
      ↩︎ Temporary schemas, concurrency and comparison drift
      “This option allows dbt to check the state of the code in the PR against the code running in the deferred environment”
      ↩︎ Key concept
      “Importantly, state comparison can only happen when there is a deferred environment selected to compare state to.”
      ↩︎ Exam trap 1
      “Built-in deferral targets an environment, not a hand-picked job ID.”
      ↩︎ Exam trap 2
      “The latest successful run in the environment can come from any job that updates artifacts there.”
      ↩︎ Exam trap 3
      “However, dbt will not automatically delete the temporary schema for you.”
      ↩︎ Exam trap 4
      “Enable this option if you want to also trigger the job to run every time a developer opens up a draft pull request”
      ↩︎ Checkpoint
      “use a merge job in the same environment your CI job defers to.”
      ↩︎ Checkpoint
    2. 2.
      “You must select the Production environment checkbox in the Environment Settings page.”
      ↩︎ What a CI job does and what it needs first
      “When using defer, it compares artifacts from the most recent successful production job, excluding CI jobs.”
      ↩︎ What a CI job does and what it needs first
      “You must have a successful job run first.”
      ↩︎ Checkpoint
    3. 3.
      “dbt uses the last successful run of any job in your Production environment as its comparison state.”
      ↩︎ How deferral makes state:modified+ work
      “CI jobs will never block a production run.”
      ↩︎ Temporary schemas, concurrency and comparison drift
      “Busy staging, QA, or UAT environments add another failure mode as many jobs with different settings overwrite manifest.json.”
      ↩︎ Temporary schemas, concurrency and comparison drift
    4. 4.
      “Ephemeral models are never deferred”
      ↩︎ How deferral makes state:modified+ work
      “Because model_a is unselected, dbt will check to see if dev_alice.model_a exists.”
      ↩︎ How deferral makes state:modified+ work
      “If it doesn't exist, dbt will resolve all instances of {{ ref('model_a') }} to prod.model_a instead.”
      ↩︎ Prediction
    5. 5.
      “dbt deletes the temporary schema from your data warehouse when you close or merge the pull request.”
      ↩︎ Temporary schemas, concurrency and comparison drift
      “CI runs with the same PR number and different commit SHAs execute serially”
      ↩︎ Temporary schemas, concurrency and comparison drift
      “CI runs don't consume a run slot.”
      ↩︎ Temporary schemas, concurrency and comparison drift

    Ready to test yourself?

    Practise the 9 questions on this subdomain.

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