Benefits of Slim CI for dbt Projects
Your pull request edits one staging model, and CI rebuilds the whole project anyway. Eventually it goes red on a model you never touched, and the reviewer merges on faith.
dbt Slim CI fixes that. It compares your branch to the production manifest.json (the file dbt writes to describe your parsed project) and builds only what the change affects. Below are five benefits, the config to try it, and when a full build is still right.
One correction first: Slim CI doesn't build "only the changed models." state:modified+ selects the changed nodes and everything downstream of them, which is where much of the value comes from.
How dbt Slim CI works with state:modified and defer
Two flags do the work. state:modified selects new nodes plus existing ones that differ from a comparison manifest, and the + adds their descendants. --defer resolves a ref() to a production relation when that upstream model isn't selected and doesn't exist in your CI schema, as described in the dbt defer docs.
Save the manifest at the end of every production run:
aws s3 cp target/manifest.json s3://<your-bucket>/dbt/prod/manifest.json
Then fetch it in CI and build the pull request's slice of the DAG:
# .github/workflows/dbt-ci.yml
name: dbt-ci
on:
pull_request:
jobs:
slim-ci:
runs-on: ubuntu-latest
env:
DBT_CI_SCHEMA: dbt_ci_pr_${{ github.event.pull_request.number }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install dbt-<your-adapter>
- run: dbt deps
# Assumes AWS credentials are already configured for the job
- name: Fetch the production manifest
run: |
mkdir -p state
aws s3 cp s3://<your-bucket>/dbt/prod/manifest.json state/manifest.json
- name: Build modified models and everything downstream
run: dbt build --target ci --select "state:modified+" --defer --state state
The ci target gives each pull request its own schema:
# profiles.yml
my_project:
outputs:
ci:
type: <your-adapter>
schema: "{{ env_var('DBT_CI_SCHEMA') }}"
# host, credentials and the rest, read from environment variables
Keep state/ separate from target/, because dbt overwrites target/manifest.json while parsing. If you use the dbt platform, its CI jobs default to dbt build --select state:modified+ with deferral to your production environment.
Benefits of dbt Slim CI for your pull requests
1. Feedback scales with the change, not the project. Picture a 600-model project where a PR edits one staging model with 14 downstream models. Slim CI builds and tests those 15, about 2.5% of the project, instead of all 600. The project can grow without every PR getting slower.
2. A smaller warehouse bill. Unselected models aren't rebuilt, and deferred refs read existing production tables instead of copies in the CI schema. The CI role needs read access to production, but write access only to its own schema.
3. Red builds that mean something. Full-project CI often fails on a flaky neighbor, so people learn to retry and merge. With Slim CI, every selected node is changed or downstream of a change, so a failure points at your PR.
4. Downstream breakage gets caught. Rename a column in a staging model and the model itself builds fine, while the mart that still selects the old name fails. Plain state:modified would miss that, and the + catches it. Macros work too: the state:modified.macros check flags changes to upstream macros, called directly or indirectly, so editing a shared macro builds every model that uses it.
5. A small, disposable footprint. Only the selected models are created in the per-PR schema, because everything else resolves to production. That leaves few objects to drop when the PR closes, though if you run CI yourself you still need a cleanup job.
When Slim CI is not worth it
The state comparison caveats list where it falls short:
- Small projects. If a full build takes a couple of minutes, you're adding a manifest to store, fetch and keep fresh for very little gain.
- Wide macro changes. Edit a macro half the project uses and half the project is modified. That is effectively a full build, which is correct.
- Data changes.
state:modifiedsees code and config changes, not new rows in a source. Scheduled jobs that must build every run shouldn't use it. - Vars and env vars. dbt can't trace a
varorenv_varto the models that use it, so changing one may not flag them. Run a full build for those PRs. - Large seeds. Seeds of 1 MiB or more are flagged only when the file path changes, not when the contents do.
- Stale manifests. An out-of-date production manifest makes CI select models that aren't in your PR. Upload it after every production run.
- Cross-environment tests. Deferral can make a
relationshipstest compare data across two environments. The docs suggest--exclude "test_name:relationships"in CI.
The best CI is the one people trust enough to wait for, and a check scoped to the real blast radius of a change is the one they will.