> ## 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.

# Planning and Change Detection

> How SQLBuild decides what to build: fingerprints, change reasons, and warehouse-native state.

When you run `sqb plan` or `sqb build`, SQLBuild compiles your project, compares it against the current warehouse state, and produces a plan. By default, SQLBuild runs your full selection - the same predictable behavior as a plain build, with nothing to configure.

Change-aware pruning is opt-in. Pass `--changes-only` (or set `changes_only = true` in config) to narrow the run to only stale work - unchanged models, seeds, audits, and Python nodes are then skipped automatically. The fingerprints and change reasons below are recorded on every successful build regardless, so change detection is ready the moment you enable pruning.

## What is tracked

Every node in the graph has a versioned identity stored in `_sqlbuild_fingerprints` in the target schema. The planner reads these on every run and compares them against the compiled project.

### Models and functions

Each model and function has a **fingerprint** derived from:

* **Query hash** - the normalized SQL after macro expansion and reference resolution.
* **Config hash** - version-identity config values (materialization settings, contracts, hooks, custom config/placeholders).
* **Function hashes** - for models that depend on user-defined functions, the function's own fingerprint is included. A function change cascades to all dependent models.

### Seeds

Seeds are fingerprinted by content hash and load-affecting config. Unchanged seeds are not reloaded.

### Python nodes

Loaders, tasks, assets, checks, and hooks are fingerprinted by source-code hash, transitive project-dependency hashes (scoped to the git root, so third-party package changes don't count), and decorator config.

Python identity tracking is primarily a **visual indicator** in the plan: when a node's identity changes, the plan shows source and dependency diffs. Unlike SQL models, the framework can't observe a Python node's external inputs (an API, a file, a service), so skip/run decisions are **user-controlled** via `ctx.skip()` - the node's own logic decides whether it needs to run. See [Python node pruning](#python-node-pruning).

### Audits

Audits that already passed for the same model version identity are not re-run. When a model's version changes, its audits are re-validated.

## Change reasons

The plan assigns a reason to each node that needs work:

| Reason                           | Meaning                                                                                                                |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| First run                        | No fingerprint exists in the target schema                                                                             |
| Query changed / checksum changed | The model's query SQL differs from the stored fingerprint (the plan can show a query diff)                             |
| Config changed                   | Version-identity config values differ                                                                                  |
| Schema changed                   | Upstream schema changes detected (column additions, removals, type changes)                                            |
| Upstream changed                 | An upstream model's change cascades downstream (see [Cascade propagation](/concepts/planning/cascade-propagation))     |
| Run despite unchanged            | The model is configured to run periodically even without changes (see [Run despite unchanged](#run-despite-unchanged)) |

By default, every selected node runs regardless of its reason. Under `--changes-only`, nodes with no pending work are pruned and show as current in the plan output.

## Changes-only mode

By default, SQLBuild executes all selected models regardless of whether they have changed. `--changes-only` narrows the scope to only models that are actually stale:

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

To make it the default for a project or target, set it in config instead of passing the flag every run:

```toml theme={null}
[settings]
changes_only = true

[targets.dev]
changes_only = true
```

The CLI flag takes precedence, followed by the selected target, explicit local settings, then project settings. When any source enables it, the planner removes models and functions from the selected scope if they have no pending work; models with any change reason, a pending backfill, or a changed upstream source are kept. Sources, seeds, and other non-model resources are always kept.

## On this topic

* [Cascade propagation](/concepts/planning/cascade-propagation) - how a change signal propagates downstream, and how each materialization type responds.
* [Source freshness](/concepts/planning/source-freshness) - observing whether external source data has actually changed between runs.
* [Selection and staleness](/concepts/planning/selection-and-staleness) - how `--select` interacts with change detection, and the stale warnings that prevent silent partial rebuilds.

## Reuse from production

When a model's version identity matches a relation already built in another target, the planner can reuse that relation instead of rebuilding it, and can clone the upstream inputs a partial build needs from production. This uses the same fingerprints described above. See [Reuse from production](/concepts/reuse-from-production).

## Run despite unchanged

Some models depend on external data that isn't tracked by source freshness, for example a table model that reads from an API-populated staging area. `run_despite_unchanged` forces a model to run periodically even when its version identity hasn't changed.

```sql theme={null}
MODEL (
  materialized table,
  run_despite_unchanged "always",
);
```

* **`always`** - run on every build regardless of state.
* **Duration** (e.g. `24h`, `30d`, `90m`) - run if at least the specified time has passed since the model's upstream source freshness was last observed. Requires at least one upstream source with timestamp freshness tracking.

Only table materializations support `run_despite_unchanged`. When triggered, downstream models are also marked as stale.

## Python node pruning

When unchanged SQL models are skipped, read-side Python nodes (tasks, assets, checks) that depend on those models are also skipped. Loaders always run regardless of pruning, since they populate sources that the SQL graph depends on.

Python nodes also have their own identity fingerprints: if a node's source code or dependencies change, it runs even if its SQL dependencies haven't.

## Warehouse-native state (standard mode)

In standard mode, all change-tracking state lives in the warehouse as append-only tables in the same schemas as your data:

* **`_sqlbuild_fingerprints`** - version identities for models, functions, seeds, and Python nodes. One row per successful build per identity.
* **`_sqlbuild_source_freshness`** - source freshness observations. One row per successful build per source identity.
* **`_sqlbuild_node_results`** - Python node runtime results (payload, metadata, status, errors). One row per execution per node.

There is no external state database, no manifest files, and no state machine with transitions that can corrupt. The planner reads the latest row per identity, compares it against the compiled project, and writes new rows after successful builds. Old rows are retained as immutable history.

State tables are read across all target schemas in the project, so fingerprints and freshness observations resolve consistently regardless of which schema a model targets.

Use `sqb janitor` to prune old state history rows while retaining the latest per identity.

## Virtual environments

Virtual environments store identities and change-tracking state in the VDE state backend (PostgreSQL or DuckDB) rather than in warehouse fingerprint tables, scoped per environment. See [Virtual Environments: Building](/concepts/virtual-environments/building).
