dbt-plan

Exit codes and migration

This contract starts with 0.16.0. It is a breaking CI-interface change from 0.15.x. Earlier package versions and Action tags retain their behavior until upgraded.

Default code Meaning Is there a completed check report?
0 No blocking findings under the configured policy Yes
1 Destructive findings Yes
2 Review required Yes
3 Execution could not complete No reliable completed verdict

This table describes check and run. Setup commands and --help/--version exit 0 on success without producing an analysis report. Invalid CLI arguments also exit 3, including errors normally reported by argparse as 2. Unsupported, empty, or unknown --select terms now exit 3 without a completed report, even when combined with valid terms. Earlier versions could warn and return 0. A known but unchanged model remains a valid empty selection; see selection.

A review finding includes unparseable or non-UTF-8 model SQL, unknown rules, incomplete baselines, stale inputs, and potentially broken builds/tests that can be represented in a report. A missing required directory, unreadable current manifest/config, failed compile or recovery, or unexpected exception is an execution failure. Fix the error on stderr and rerun. Do not accept stdout as a completed report when the process failed, even if it wrote partial output first.

Warning policy stays separate

The default remains warning_exit_code: 2. File/environment configuration and warning codes keep their existing behavior. warning_exit_code: 0 allows review findings without hiding them from the report; execution failures still exit 3. Other custom warning codes remain supported, except 3, which is now reserved. The resolved configuration is checked after environment overrides. An unreadable configuration fails instead of silently applying defaults.

For check and run, choose --fail-on destructive|warning|never to set an explicit exit policy. Resolution is CLI flag > DBT_PLAN_FAIL_ON > fail_on in .dbt-plan.yml > legacy behavior. With none supplied, warning_exit_code keeps its existing behavior, including its default of 2 and opt-out value 0.

Resolved fail_on Destructive finding Review finding Execution error
Absent 1 warning_exit_code (default 2) 3
warning 1 Configured nonzero warning_exit_code, or 2 when configured as 0 3
destructive 1 0 3
never 0 0 3

These policies apply to completed findings after the existing resource-specific acknowledgements. They do not expand acknowledgement scope or change raw safety, uncertainty, report contents, or summary counts. In particular, a parse failure still requires review even when the exit policy permits it. run validates the policy before invoking subprocesses or changing the worktree and passes it to check. snapshot has no --fail-on option.

Only the exact lowercase values above are valid. The resolved value is validated after all overrides: a valid higher layer may replace an invalid lower value, but an invalid winning value is a configuration error (exit 3), before analysis or side effects. An explicitly empty environment variable overrides the file and is invalid unless a valid CLI flag overrides it. Empty file values and CLI arguments are also invalid. Unreadable configuration and reserved warning_exit_code: 3 remain errors even with --fail-on never.

dbt-plan check --fail-on warning
dbt-plan run --fail-on destructive
DBT_PLAN_FAIL_ON=never dbt-plan check --format json

The Action’s fail-on: destructive|warning|never and generated workflow’s FAIL_ON own the CI gate. Their analysis and rendering calls override the new file/environment fail_on setting while retaining legacy warning_exit_code and resource acknowledgements. In particular, warning_exit_code: 0 still opts out of warnings even when the Action input is warning. This differs deliberately from the explicit CLI --fail-on warning, which forces warnings to fail. The wrappers set the override before reading configuration; an invalid lower fail_on cannot prevent that override. Other configuration errors still fail. Only an environment variable is used, so pinned older CLIs need no new option and keep their existing behavior.

The Action’s verdict output now describes raw JSON findings independently of its exit-code output. Acknowledged destructive findings can therefore produce verdict=destructive with exit-code=0; warning opt-out produces verdict=warning with exit-code=0. The gate uses the policy code, not the raw verdict. A zero code does not establish safety. The generated Check step exposes the same separate outputs.

Policy applies only after a completed check; code 3 fails the Action Check step before the Gate step, even for never. The wrappers require consistent JSON summary counts and model safety values, and validate uncertainty fields, so legacy package errors that exit 1 or 2 without a report still fail the step. The Action continues to recognize standard verdict codes 0/1/2; custom warning codes outside those values fail its Check step. Custom policies can change the exit status, so inspect the report’s findings rather than treating code 0 as proof of safety.

Update consumers before upgrading

In 0.15.x, failures commonly exit 2 and uncaught Python exceptions exit 1. Scripts that permit code 2 as a warning cannot reliably detect those failures. Pin the existing package until ready, then upgrade package and Action references together. For a shell consumer using standard warning codes, capture the command status explicitly so set -e does not bypass policy handling:

code=0
dbt-plan check --format json > dbt-plan-report.json || code=$?
case "$code" in
  0) ;;                                      # passed the configured policy
  1) echo "Destructive findings" >&2; exit 1 ;;
  2) echo "Review required; inspect the report" >&2 ;;  # explicit allow-warning policy
  3) echo "Check failed; fix stderr errors and rerun" >&2; exit 3 ;;
  *) echo "Unexpected status: $code" >&2; exit "$code" ;;
esac

A bare CLI command still fails on any nonzero status. Newly generated ci-setup workflows use an explicit FAIL_ON: destructive gate: they allow completed warnings by default, and never allow execution errors. Report rendering cannot bypass that gate. Existing workflow files are not rewritten by a package upgrade; regenerate and review your project-specific settings to adopt this policy. See the CI guide for dependency layouts and policy settings. Update the dbt-plan section in consumer AGENTS.md after upgrading, or remove that section and rerun dbt-plan agent-setup to regenerate it.

We chose a process code instead of only a JSON error envelope because shell consumers need the distinction too. The JSON report schema is unchanged; errors remain diagnostics on stderr. The 0.16.0 minor boundary follows the project’s pre-1.0 status (Semantic Versioning), while this guide makes the compatibility change explicit.

0.17.0: acknowledge each affected resource

In 0.16.0, acknowledging an upstream model also waived the cascade findings printed under that model. Starting with 0.17.0, an acknowledgement covers only the named resource’s known findings. Review existing acknowledge_models lists when upgrading; an upstream-only acknowledgement can now exit 1 or 2.

For example, stg_orders is a view that removes customer_id, and unchanged fct_orders selects * with incremental sync_all_columns:

CLI option Exit Explanation
--acknowledge stg_orders 1 The inherited drop on fct_orders remains active.
--acknowledge fct_orders 0 The drop is accepted; the view’s own replacement is safe.
--acknowledge stg_orders,fct_orders 0 Both resources’ known findings are accepted.

If stg_orders itself is incremental with sync_all_columns, downstream-only acknowledgement still exits 1 for its own drop. Unrelated destructive findings also still exit 1. Active warning impacts use warning_exit_code (default 2, including the opt-out value 0). Action fail-on handling is unchanged; execution errors still exit 3, even with all resource names acknowledged.

Use exact report names, including version suffixes or defined_in file names; there are no globs or blanket acknowledgements. Repeating a name has no extra effect, and one explicitly named target may be shared by several upstream findings. Tests require their own exact resource name, never their parent model’s name. Names shared by distinct manifest resources, including tests or exposures, cannot waive findings; the report explains the ambiguity. Exposure owner lines are informational and are not waived by a model acknowledgement.

Unknown risk kinds, unknown operations, unreadable test fixtures/SQL, unresolved inherited schemas or contracts, parse failures, incomplete baselines and other analysis refusals cannot be acknowledged away. They retain review-level uncertainty under the configured warning policy.

When acknowledgements are supplied, text and GitHub labels say ACKNOWLEDGED: own findings only; each cascade line shows whether its affected resource is waived or active. JSON retains every existing field, raw safety, summary count and cascade detail. acknowledged and the corresponding summary count still indicate requested model names, not the absence of active descendants. Additive fields make policy explicit: models[].own_safety, models[].own_waived, and models[].downstream_impacts[].waived / waiver_reason (the latter two appear when acknowledgements are supplied). A false own_waived means the resource was not acknowledged or its findings/identity could not be waived. MCP verdicts continue to reflect raw severity even when CLI policy exits 0.