What you will be able to do
- Explain what the --empty flag changes about a dbt run and what it still validates
- Choose which commands to pair with --empty, including seeds from dbt v1.12
- Use --empty to create incremental models before running their unit tests
- Handle the flag's limits: Python models, hooks that need .render(), and macros that check flags.EMPTY
Key concept
Schema-only dry run (--empty) — A dbt invocation in which every ref and source resolves to zero rows. The model SQL still runs against the warehouse, so dependencies and build logic get checked, but no input data is read and the objects come out empty.
1.What a schema-only dry run validates
Building a model is often the slowest way to learn whether it is correct. Suppose a model joins three large staging tables. Before you change it, you want to confirm that the SQL resolves, its upstream relations exist, and the warehouse will accept the statement. A full build would scan every input row to tell you that. The --empty flag lets you check that models are semantically correct without paying the cost of building them in full.
The flag works by cutting off the inputs, not by skipping execution. Every ref() and source() resolves to zero rows. dbt then runs the model SQL against the target warehouse as usual, but there is no data to read, so the expensive part disappears. Because the warehouse still receives and executes the statement, a successful run validates the model's dependencies and shows that the model will build properly.
What you get is structure without content. The models are created in your development schema with the right shape and no rows. This makes --empty a validation tool, not a way to inspect results. You can't check a join's row counts or a filter's output against an empty table. When you need some real rows to look at, use the [time-based --sample flag](lesson:1.13). It takes the same idea one step further.
Checkpoint 1 of 7· Check yourself
A developer runs dbt run --select path/to/your_model --empty and the run succeeds. What does the warehouse contain afterwards?
The SQL runs against zero-row inputs, so the model is created with its structure and no data.
“dbt will build and execute the SQL, resulting in an empty schema in the data warehouse.”Source: docs.getdbt.com
Checkpoint 2 of 7· Exam question
An analytics engineer is preparing a pull request that adds a new staging model referencing three upstream sources. Before running the full pipeline against production-scale data, they want to confirm the model's SQL compiles, the DDL is valid, and the resulting table has the correct column types — without scanning any actual rows from the sources. Which action best achieves this?
Correct answer: C — Run `dbt run --select stg_new_model --empty` to build the model with zero-row inputs, producing the real schema without processing source data.
- A. Generating docs does not execute the model against the warehouse or verify that the compiled SQL is valid, so it cannot confirm DDL correctness or catch a broken `ref()`/`source()` dependency.
- B. A full refresh forces dbt to reprocess the entire dataset from the upstream sources, which is exactly the cost and time the engineer is trying to avoid before merging.
- C. This is correct: limiting every `ref()` and `source()` call to zero rows still runs the compiled SQL against the warehouse and creates the real table with the real column types, at a fraction of the cost of a full build.
- D. Tests assume a model has already been built and check the data it contains; running tests first does not validate that the model's SQL compiles or that its schema is correct.
2.Commands that accept --empty, including seeds
The flag works with run, build, snapshot and compile. build passes the same flags to run, test, snapshot and seed, so dbt build --empty gives you a dry run across a whole DAG in one command. To validate one model, combine --empty with a normal selector:
dbt run --select path/to/your_model --empty| Command or resource | Behaviour with --empty |
|---|---|
| dbt run | Supported: models build with refs and sources limited to zero rows |
| dbt build | Supported: schema-only dry run across the selected resources |
| dbt snapshot | Supported |
| dbt compile | Supported |
| dbt seed | Supported starting in dbt v1.12 |
| Python models | Not available: the flag is ignored |
Seeds gained support in dbt v1.12, and they behave differently from models. A seed has no refs or sources to empty. Instead, dbt seed --empty reads the CSV file to infer the column names and types, creates the table, and inserts nothing. This matters in development and CI. Downstream models and unit tests often just need the seed table to exist with the right columns, and loading the full file would be wasted work. You can target a single seed when only one table's structure matters:
dbt seed --select "country_codes" --emptyCheckpoint 3 of 7· Check yourself
A CI job on dbt v1.12 runs dbt seed --empty before building models. What does it produce for each seed?
Seed --empty keeps the structure dbt infers from the CSV and leaves out the data, so downstream nodes can compile and run against it.
“dbt infers column names and types from the CSV file but inserts zero rows.”Source: docs.getdbt.com
Checkpoint 4 of 7· Exam question
Which dbt commands support the `--empty` flag for schema-only dry runs? (Select 2)(Select 2)
Correct answers: B, D — `dbt run`; `dbt build`
- A. Tests operate on data that has already been materialized and are not among the commands that accept the `--empty` flag.
- B. This command supports the `--empty` flag: it executes the selected model's compiled SQL with every `ref()`/`source()` limited to zero rows.
- C. Generating documentation reads model metadata rather than executing SQL against the warehouse, so it has no use for a schema-only dry-run flag.
- D. This command supports the `--empty` flag: it can run and test the selected DAG slice with zero-row inputs in a single invocation.
- E. Freshness checks compare source table timestamps and do not execute model SQL, so a zero-row dry-run flag does not apply to it.
- F. Compilation alone does not execute SQL against the warehouse at all, so the flag is not needed here even though the command exists.
3.Creating incremental models before unit tests
Unit tests on incremental models have a prerequisite. The model must already exist in the database before its unit tests, or a dbt build, can run. That's a problem for a new incremental model or a fresh dev schema. Building it for real could mean processing a large history just to have a target table in place. A dry run solves this cheaply. You can select only the incremental models by their materialization config:
dbt run --select "config.materialized:incremental" --emptyThe empty table only satisfies the existence requirement. After it's in place, run a regular dbt build for that model, then run the unit test. How you write the test, including overriding is_incremental to cover both modes, belongs to [unit testing incremental models](lesson:5.1).
Checkpoint 5 of 7· Put it in order
Put these steps in order to unit test a brand-new incremental model without paying for a full initial build up front.
- 1.Perform a regular dbt build for that model
- 2.Run the unit test
- 3.Run dbt run --select "config.materialized:incremental" --empty
The model has to exist before unit tests or dbt build can run, and the empty run is the cheap way to create it. The regular build and the unit test come after.
“After running the command, you can then perform a regular dbt build for that model and then run your unit test.”Source: docs.getdbt.com
Sources5
4.Python models, .render() in hooks, and flags.EMPTY
Python models don't support the flag. dbt ignores --empty for a Python model and doesn't raise an error. In a mixed project, a dbt build --empty still runs every [Python model](lesson:1.8) normally, so those models aren't a cheap dry run. Leave them out of the selection if cost is the reason you're using the flag.
Hooks are the other common failure point. Under --empty, dbt may skip processing a ref() or source() as an optimization. That's fine in a model body, but it breaks a hook that has to name a real relation. One example is a pre-hook that refreshes an external table before the model reads it. Call .render() on the relation to tell dbt to process it explicitly, which avoids the compilation error:
{{ config(
pre_hook = [
"alter external table {{ source('sys', 'customers').render() }} refresh"
]
) }}Custom macros and materializations can check whether they are in empty mode with flags.EMPTY. It is True when --empty is passed and False otherwise. A macro might use it to return a zero-row version of a relation. Use it only in runtime logic like this, not as an input to parse-time configurations or ref and source dependencies. Flag values change between invocations, so they go stale under partial parsing. The wider set of [run-mode flags](lesson:3.5) follows the same rule.
Checkpoint 6 of 7· Fill the gap
Which flag completes this macro so it returns zero rows only when dbt runs in schema-only mode?
{% macro my_custom_ref(model_name) %}
{% if flags. ? %}
{# Return zero rows when running with --empty #}
(select * from {{ ref(model_name) }} where 1=0)
{% else %}
{{ ref(model_name) }}
{% endif %}
{% endmacro %}flags.EMPTY is True only when --empty is passed. FULL_REFRESH and STORE_FAILURES track other run modes, and WHICH holds the command name, not a boolean.
Source: docs.getdbt.comCheckpoint 7 of 7· Exam question
A team wants to add a lightweight CI check that catches broken `ref()`/`source()` dependencies and invalid column types in new models before merging, without waiting on a full warehouse build of every affected table. Which statements about using `--empty` for this purpose are correct? (Select 2)(Select 2)
Correct answers: B, D — Running `dbt build --empty` against the changed models still executes each model's compiled SQL against the warehouse, so genuine SQL syntax and type errors are still caught.; Because `ref()` and `source()` calls resolve to zero rows, the CI job avoids scanning or processing the full volume of production-scale data.
- A. Once the referenced tables are built with zero rows under `--empty`, generic tests running against them would see empty data and largely pass trivially, so this statement overstates what the tests would actually catch.
- B. This is correct: `--empty` still compiles and executes each model's SQL against the target warehouse, so a broken `ref()`, a typo, or an incompatible column type still surfaces as a real error.
- C. dbt does not skip Python models under `--empty`; the flag is simply ignored for them, so a Python model in the selection still runs against full data rather than being marked as passed without execution.
- D. This is correct: limiting every dependency to zero rows means the warehouse never has to scan or process the production-scale data behind those references, which is what keeps the CI check fast and cheap.
- E. The flag does not behave identically across materializations; it is ignored for Python models, so a CI script cannot assume uniform zero-row behavior without accounting for that exception.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.--empty only compiles the SQL locally, so it can't confirm that a model will actually build.Why is that wrong?
With run or build, dbt sends the model SQL to the target warehouse and runs it. Only the refs and sources are reduced to zero rows. That's why a successful empty run validates dependencies.
Covered in What a schema-only dry run validates
2.Running dbt build --empty produces empty tables for Python models too.Why is that wrong?
Python models don't support --empty. The flag is ignored, so they build normally.
Covered in Python models, .render() in hooks, and flags.EMPTY
3.A source() call inside a pre-hook always resolves normally during an --empty run.Why is that wrong?
dbt may skip processing ref() or source() under --empty. A hook that needs the relation should call .render() on it to avoid compilation errors.
Covered in Python models, .render() in hooks, and flags.EMPTY
4.flags.EMPTY is a good input for a model's parse-time config or for choosing which ref() to depend on.Why is that wrong?
Flags change with each invocation, and their parsed values go stale under partial parsing. Use them in runtime logic only.
Covered in Python models, .render() in hooks, and flags.EMPTY
Practise it for real
Dry-run part of a project with --empty, then use it to prepare incremental models for unit testing.
1.Run dbt run --select path/to/your_model --empty for a model you recently changed.
Why: This checks that the model's SQL and upstream dependencies resolve without reading any input data.
You should see: The run succeeds, and the model exists in your development schema with its columns and no rows.
2.Run dbt seed --select "country_codes" --empty (dbt v1.12 or later), substituting one of your own seeds.
Why: Downstream models and unit tests need the seed table's structure but not its data.
You should see: A seed table whose columns are inferred from the CSV, with zero rows.
3.Run dbt run --select "config.materialized:incremental" --empty.
Why: Incremental models must exist before their unit tests or a dbt build can run.
You should see: Empty versions of every incremental model in the dev schema.
4.Perform a regular dbt build for one incremental model, then run its unit test.
Why: The empty table met the existence requirement. The real build and the test come after it.
You should see: The model builds and its unit test runs.
Stuck? Get a nudge
If a model with a pre-hook fails to compile under --empty, check whether the hook's ref() or source() needs .render().
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.https://docs.getdbt.com/docs/build/empty-flagOfficial docs
“validate that your models are semantically correct without the time-consuming cost of building the entire model in the data warehouse”
↩︎ What a schema-only dry run validates“This validates dependencies and ensures your models will build properly.”
↩︎ What a schema-only dry run validates“The run, build, snapshot, and compile commands support the --empty flag.”
↩︎ Commands that accept --empty, including seeds“Starting in dbt v1.12, seed also supports the --empty flag for building schema-only dry runs.”
↩︎ Commands that accept --empty, including seeds“If the flag is used with a Python model, it will be ignored.”
↩︎ Python models, .render() in hooks, and flags.EMPTY“The --empty flag limits the refs and sources to zero rows.”
↩︎ Key concept“dbt will still execute the model SQL against the target data warehouse but will avoid expensive reads of input data.”
↩︎ Exam trap 1“If the flag is used with a Python model, it will be ignored.”
↩︎ Exam trap 2“dbt will still execute the model SQL against the target data warehouse but will avoid expensive reads of input data.”
↩︎ Prediction“dbt will build and execute the SQL, resulting in an empty schema in the data warehouse.”
↩︎ Checkpoint - 2.https://docs.getdbt.com/docs/build/sample-flagOfficial docs
“Sample mode takes the --empty flag's validation of semantic results a step further”
↩︎ What a schema-only dry run validates - 3.https://docs.getdbt.com/reference/commands/buildOfficial docs
“The build task supports all the same flags as run, test, snapshot, and seed.”
↩︎ Commands that accept --empty, including seeds“To avoid compilation errors and to explicitly tell dbt to process a specific relation”
↩︎ Python models, .render() in hooks, and flags.EMPTY“When using the --empty flag, dbt may skip processing ref() or source() for optimization.”
↩︎ Exam trap 3 - 4.https://docs.getdbt.com/reference/commands/seedOfficial docs
“dbt infers column names and types from the CSV file but inserts zero rows.”
↩︎ Commands that accept --empty, including seeds - 5.https://docs.getdbt.com/docs/build/unit-testsOfficial docs
“Incremental models need to exist in the database before running unit tests or doing a dbt build.”
↩︎ Creating incremental models before unit tests“Use the --empty flag to build an empty version of the models to save warehouse spend.”
↩︎ Creating incremental models before unit tests“After running the command, you can then perform a regular dbt build for that model and then run your unit test.”
↩︎ Checkpoint - 6.
“flags.EMPTY returns True when the --empty flag is passed to a dbt command, and False otherwise.”
↩︎ Python models, .render() in hooks, and flags.EMPTY“It is not recommended to use flags as an input to parse-time configurations, properties, or dependencies (ref + source).”
↩︎ Exam trap 4