A CI job runs every time someone opens a pull request or pushes to one. It builds only the models the pull request changed, plus everything downstream, in a temporary schema, and reports the result as a check on the pull request. A change that breaks a model or a test is caught before it reaches production.
Copy and paste
Before you start:
- the project is connected to GitHub, GitLab or Azure DevOps through dbt’s native integration;
- a production environment exists, marked as Production environment, with at least one successful run;
- ideally, a separate deployment environment for CI that points to a staging database.
Create the job. On the CI environment’s page, click Create job > Continuous integration job, and keep the defaults:
- Triggered by pull requests: on
- Commands:
dbt build --select state:modified+ - Compare changes against: the production environment
Optional settings:
- Run on draft pull request, to test drafts as well;
- SQL linting, to lint the changed SQL files first;
- dbt compare (Enterprise plans with Advanced CI), to post a summary of the data differences.
What happens on each pull request
- dbt compares the pull request’s code with the manifest of the last successful production run.
- It builds the new and changed models and everything after them, with their tests.
- Unchanged parents are read from production, so nothing else is rebuilt.
- The results go to a temporary schema whose name starts with
dbt_cloud_pr_, such asdbt_cloud_pr_123_456_marketing. - The pull request shows a check: green when the build and the tests pass. The link opens the run in dbt.
When the pull request is merged or closed, dbt drops the temporary schemas it wrote to. That relies on the native Git integration; with other providers, you have to drop them yourself.
Check generate_schema_name first
A custom generate_schema_name that always returns the custom schema as it is, such as finance, makes
the CI job write into the production schemas. Keep the PR prefix outside production. dbt sets
DBT_CLOUD_INVOCATION_CONTEXT to ci in CI runs, which you can test in the macro:
-- macros/generate_schema_name.sql
{% macro generate_schema_name(custom_schema_name, node) -%}
{%- if env_var('DBT_CLOUD_INVOCATION_CONTEXT', '') == 'prod' and custom_schema_name is not none -%}
{{ custom_schema_name | trim }}
{%- elif custom_schema_name is not none -%}
{{ target.schema }}_{{ custom_schema_name | trim }}
{%- else -%}
{{ target.schema }}
{%- endif -%}
{%- endmacro %}
In production this writes to finance; in CI, to dbt_cloud_pr_123_456_finance.
Things to know
- No production run, no comparison. Deferral needs a successful run in the production environment. Before the first one, the CI job has nothing to compare with.
- One run per commit. dbt does not start a new run if the latest commit already triggered one for that job. On Starter and Enterprise plans, stale runs are cancelled when new commits arrive.
- Incremental models. In the temporary schema an incremental model starts empty, so CI runs its
full query. Limit it with a filter on
target.name; see how to use if/else in a dbt model. - The same selector locally.
dbt build -s state:modified+ --deferdoes the same thing on your machine; see how to run only modified models.
Based on the dbt platform documentation (October 2026); not run against a live dbt account.