CertSafari
    DBT Analytics Engineering· Lessons

    Domain 1 · Lesson 12/31

    Schema-only dry runs with the dbt --empty flag

    Validating model logic and schema definitions in dry-runs using the --empty flag

    12 min read
    3.23% of exam
    6 sources
    Published 12 Oct 2026
    Docs as of 12 Oct 2026

    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?

    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?

    Sources12

    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:

    Dry-run a single model: the SQL executes, but no input rows are readbash
    dbt run --select path/to/your_model --empty
    Where the --empty flag applies
    Command or resourceBehaviour with --empty
    dbt runSupported: models build with refs and sources limited to zero rows
    dbt buildSupported: schema-only dry run across the selected resources
    dbt snapshotSupported
    dbt compileSupported
    dbt seedSupported starting in dbt v1.12
    Python modelsNot 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:

    Create one seed table with the CSV's columns and zero rowsbash
    dbt seed --select "country_codes" --empty

    Checkpoint 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?

    Checkpoint 4 of 7· Exam question

    Which dbt commands support the `--empty` flag for schema-only dry runs? (Select 2)(Select 2)

    Sources134

    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:

    Build empty versions of every incremental model to save warehouse spendbash
    dbt run --select "config.materialized:incremental" --empty

    The 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. 1.Perform a regular dbt build for that model
    2. 2.Run the unit test
    3. 3.Run dbt run --select "config.materialized:incremental" --empty

    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:

    A pre-hook that forces dbt to resolve a source under --emptysql
    {{ 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 %}

    Checkpoint 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)

    Sources136

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 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. 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. 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. 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. 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. 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. 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. 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. 1.
      “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. 2.
      “Sample mode takes the --empty flag's validation of semantic results a step further”
      ↩︎ What a schema-only dry run validates
    3. 3.
      “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. 4.
      “dbt infers column names and types from the CSV file but inserts zero rows.”
      ↩︎ Commands that accept --empty, including seeds
    5. 5.
      “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. 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

    Spotted a mistake, or was something unclear? Tell us.