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?
Deferral compares against artifacts from a successful production run, and Production has none yet. CI runs never supply those artifacts, and reusing the Production schema name is explicitly safe.
“You must have a successful job run first.”Source: docs.getdbt.com
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?
Correct answer: A — No deferral environment is selected for the CI job, so `state:modified+` has no manifest to diff against; setting Compare changes against an environment to the production job restores incremental selection.
- A. This is correct: `state:modified+` needs a deferred manifest to diff against, and without a deferral environment selected the CI job has nothing to compare state to, so it silently builds every model. Pointing Compare changes against an environment at the production job restores the artifact dbt needs for incremental selection.
- B. Source freshness blocks control the `dbt source freshness` check, not the artifact comparison that `state:modified+` relies on. Adding freshness config to sources has no effect on whether the selector can resolve a modified set.
- C. Thread count controls parallel execution of models during a run, not whether a comparison manifest exists for state selection. Raising it would only change run concurrency, not fix the missing deferral configuration.
- D. The trigger type determines when a job fires, not whether it has a manifest to diff against; `github_webhook` alone still requires a deferral environment for `state:modified+` to work. Switching triggers does not restore incremental selection on its own.
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.
| Setting | Default | What it controls |
|---|---|---|
| Triggered by pull requests | Enabled | Runs the job each time a PR is opened or receives a new commit |
| Run on draft pull request | Off | Also runs the job for draft PRs and commits pushed to them |
| Commands | dbt build --select state:modified+ | Builds only new or changed models and their downstream dependents |
| Compare changes against an environment (Deferral) | Production environment, if one exists | Which environment's state the PR is compared with and deferred to |
| Run timeout | Set per job; 3600 when dbt compare is on | Cancels 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.
Pull request triggering is on by default and draft PRs need the separate opt-in. The deferral setting picks the comparison environment, and the timeout limits how much warehouse time a run can use.
“Enable this option if you want to also trigger the job to run every time a developer opens up a draft pull request”Source: docs.getdbt.com
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 ? state:modified+ selects modified nodes plus everything downstream of them. Putting the + before the selector would select upstream parents, which deferral exists to avoid rebuilding.
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.
dbt run --select "model_b" --defer --state prod-run-artifactsIn 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?
Correct answer: A — Change the deferral setting from self-deferral to a specific production job or environment, so builds always diff against the latest production manifest instead of a prior PR's temporary schema.
- A. Correct: pointing deferral at production or another job's environment gives every PR a stable, current manifest to compare against, instead of the temporary schema of whichever PR last ran under self-deferral. This is exactly the fix dbt recommends when schema errors reference an unrelated closed pull request.
- B. Disabling the required schema prefix would break dbt Cloud's ability to detect and clean up any PR schema at all, making stale-schema references worse rather than better, since every PR would then write into the same fixed schema.
- C. Run timeout only limits how long a job step is allowed to execute; it does not change what environment or run a job defers to, so a longer timeout has no effect on self-deferral pointing at the wrong schema.
- D. Changing the trigger to `on_merge` alone would stop the job running on pull requests at all rather than fixing the deferral target, and it does not address the self-deferral setting that is the actual root cause.
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?
Refreshing the manifest in the deferred environment, together with rebasing the open branches, brings the comparison state back in line. Deferral cannot target a job ID, and a busy staging environment adds drift instead of removing it.
“use a merge job in the same environment your CI job defers to.”Source: docs.getdbt.com
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 %} ```
Correct answer: C — {{ 'pr_' ~ var('pr_id') ~ '_' ~ default_schema }}
- A. This line only defines the macro signature and does not construct the schema name, so it cannot be the cause of the missing prefix.
- B. This line just reads the connection's default target schema into a variable; it does not decide the final PR schema name, so it is not responsible for the failure.
- C. This line builds the PR schema name with a `pr_` prefix instead of the required `dbt_cloud_pr_` prefix, so dbt Cloud cannot recognize these schemas as PR schemas and never drops them automatically.
- D. This line only closes the macro block and has no effect on how the schema name string is constructed.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
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.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.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.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.https://docs.getdbt.com/docs/deploy/ci-jobsOfficial docs
“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.https://docs.getdbt.com/docs/platform/about-deferOfficial docs
“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.https://docs.getdbt.com/guides/set-up-ciOfficial docs
“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.
“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.
“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