> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sqlbuild.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Change-aware builds

> Opt into skipping unchanged dbt models by fingerprinting them in the warehouse, with cascade propagation and source freshness.

By default, `sqb dbt build` and `sqb dbt run` rebuild everything in the selection, exactly like dbt. With `--changes-only`, SQLBuild instead plans which dbt models actually need to run: models whose SQL has not changed and whose inputs are current are pruned from the dbt command, so a second build skips them entirely.

This works on a plain dbt project, with no SQLBuild models and no changes to your dbt files. SQLBuild reads the dbt manifest, compares it against state it stores in your warehouse, and decides per model whether dbt needs to run it. The fingerprints below are recorded on every build, so change-aware pruning is ready the moment you pass `--changes-only`.

## How dbt models are tracked

SQLBuild stores a fingerprint for every dbt model it builds, in a `_sqlbuild_fingerprints` table in your target schema. Each fingerprint is keyed with `node_type: "dbt"` and the model's dbt `unique_id`, and records the model's version identity.

The version identity comes from dbt's own `checksum` for the model node in the manifest. On each plan, SQLBuild compares the checksum in the current manifest against the fingerprint it last stored:

| Condition                                         | Action | Reason           |
| ------------------------------------------------- | ------ | ---------------- |
| No stored fingerprint                             | run    | first run        |
| Target relation missing                           | run    | relation missing |
| Manifest checksum differs from stored fingerprint | run    | checksum changed |
| Full refresh requested                            | run    | full refresh     |
| Everything matches and relation exists            | skip   | no change        |

After dbt runs, SQLBuild writes an updated fingerprint for each dbt model that executed, so the next plan sees them as current.

### Enabling change-aware pruning with `--changes-only`

The table above only takes effect under `--changes-only`. Without it, every selected dbt model is planned and run regardless of whether its fingerprint matches; with it, current models are pruned from the dbt command.

```bash theme={null}
sqb dbt build --select path:models/marts --changes-only
```

Use the default full build when you want to rebuild despite a clean plan, for example after an out-of-band change to the warehouse that SQLBuild's fingerprints cannot see. Fingerprints are recorded on every build regardless of the flag, so switching between full and change-aware runs never loses state. It is distinct from `--full-refresh`: `--changes-only` decides *whether* a model runs, while `--full-refresh` decides *how* (a full rebuild rather than an incremental run). This mirrors native `sqb build --changes-only`. See [Flags](/concepts/dbt-compatibility/overview#flags) for how flags route across dbt and SQLBuild.

### What SQLBuild creates in your warehouse

All state is append-only and lives in your target schema, in the same warehouse dbt already uses. There is no external state store, no manifest comparison server, and no requirement to log in anywhere. SQLBuild creates two small tables:

| Table                        | Contents                                                                                             |
| ---------------------------- | ---------------------------------------------------------------------------------------------------- |
| `_sqlbuild_fingerprints`     | One row per build per model, recording its version identity. Drives change detection.                |
| `_sqlbuild_source_freshness` | One row per build per source, recording the last observed freshness. Drives source-change detection. |

These are the only objects SQLBuild adds. Your dbt models, schemas, and tables are otherwise untouched. The [janitor](/cli/janitor) can prune old rows, keeping only the latest per identity.

## Cascade propagation

Change detection runs over the combined dbt and SQLBuild graph, so a change in one dbt model correctly forces the work that depends on it, in both directions:

* **Upstream changed.** A dbt model that is otherwise current is rerun when any of its upstream dbt models is running. The plan reason is `upstream_changed`.
* **Downstream into SQLBuild.** When a dbt model runs, SQLBuild marks the SQLBuild models that read from it (through `__dbt_ref`) as stale, so they rebuild against the new data.

This means a single changed staging model propagates a rebuild signal through the rest of the dbt DAG and across the boundary into your SQLBuild models, without you having to select them by hand.

## Source freshness

SQLBuild translates the sources declared in the dbt manifest into its own source freshness model and observes them as part of planning. Observations are stored in the `_sqlbuild_source_freshness` table in your target schema.

* **Source changed.** A current dbt model whose upstream source has new data is promoted to run, with reason `source_freshness_changed`.
* **Source stale past its age policy.** If an upstream source is older than its configured age tolerance, the dbt models downstream of it are blocked rather than built on stale inputs, with reason `source_freshness_error`. SQLBuild models downstream of a blocked dbt model are blocked too.

Source freshness records are persisted after a successful build, so the next plan can detect new arrivals. See [Sources](/concepts/sources) for freshness strategies and age policies.

## Pruning and steady state

Under `--changes-only`, only the dbt models classified as `run` are passed to the underlying `dbt build`/`dbt run` command. Current models are removed from the dbt selection, dbt tests and seeds for pruned models are dropped from the command, and dbt never sees the models it does not need to rebuild.

When every dbt model in the selection is current, SQLBuild does not invoke dbt at all. The dbt section of the plan reports the skip and lists the current models:

```
dbt (3 selected resources)
  selected by dbt selector: 3 from dbt selector
  planned models: 0 run, 3 current, 0 blocked
  planned non-model dbt work: 0
  skipped: all planned dbt models are current

  Model plan
    Current (3)
      model.analytics.dim_customers   no change
      model.analytics.fct_orders      no change
      model.analytics.dim_products    no change
```

With `--changes-only`, a first build runs the changed models; a second build with no changes skips the entire dbt run and only re-evaluates SQLBuild models, which are themselves current. This is the same opt-in skip-unchanged behavior SQLBuild applies to its own models, extended over your existing dbt project.

## Replay on change

When a changed incremental dbt model is rerun, `replay_on_change` controls whether it runs incrementally or rebuilds in full. It is a single project-wide policy in the `[dbt]` config block, applied to every changed model in the run (not per model):

```toml theme={null}
[dbt]
project_dir = "../dbt_project"
profiles_dir = "../profiles"
target_path = "../dbt_project/target"
replay_on_change = "forward_only"
```

| Value                    | Behavior                                                                                                          |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `forward_only` (default) | Changed models run incrementally, picking up new data from where they left off.                                   |
| `full`                   | Changed models are rebuilt in full. SQLBuild adds `--full-refresh` to the dbt run when there is model work to do. |

Unlike SQLBuild's own per-model [`replay_on_change`](/concepts/planning/cascade-propagation#replay-on-change), the dbt setting is all-or-nothing across the run and does not support bounded windows. A `bounded-<duration>` value is rejected for dbt.

## What is never touched

* dbt source files are never modified. SQLBuild reads the manifest and drives the `dbt` CLI; it does not patch, rewrite, or revert your dbt project.
* dbt internals are never monkey-patched. Selection and execution go through the documented `dbt` CLI surface (`dbt ls`, `dbt compile`, `dbt build`/`dbt run`).
* State lives in your warehouse, not in a separate database or service.
