Data Engineering  /  dbt

๐Ÿ”„ dbt โ€” Data Build Tool 60 guides ยท updated 2026

Analytics engineering with SQL โ€” models, tests, sources, and Jinja macros that turn raw warehouse tables into trustworthy, documented data products.

dbt ref() Function: How Model References Actually Work

If you strip dbt down to a single feature that justifies its existence over plain SQL scripts, itโ€™s ref(). Everything else โ€” the DAG, environment promotion, incremental strategies, documentation lineage โ€” is built on top of what this one function does. Understanding it properly changes how you write every model afterward.


The Two Jobs ref() Does at Once

When you write {{ ref('stg_customers') }} inside a model, dbt does two separate things during compilation, and itโ€™s worth separating them mentally:

  1. It resolves to the correct physical table name for whatever environment youโ€™re running in โ€” dev_yourname.stg_customers locally, analytics.stg_customers in production, ci_pr_482.stg_customers in a CI job.
  2. It registers a dependency edge in the projectโ€™s DAG, so dbt knows stg_customers must finish building before the current model runs.
-- What you write
select * from {{ ref('stg_customers') }}
-- What it compiles to, in production
select * from analytics.stg_customers
-- What it compiles to, in your dev environment
select * from dev_alice.stg_customers

This is why teams that migrate from raw SQL scripts to dbt almost always describe the environment portability as the single biggest quality-of-life improvement โ€” the same codebase runs correctly everywhere without a single line changing.


ref() Is Resolved at Compile Time, Not Runtime

A subtlety that trips people up: ref() isnโ€™t a runtime lookup against a live registry. dbt parses every model file, builds the full dependency graph in memory, and only then compiles each modelโ€™s SQL by substituting the resolved table name. This is why a typo in a ref() call fails immediately at compile time with a clear error โ€” dbt already knows every model that exists in the project before it runs anything.

Compilation Error in model customer_orders (models/core/customer_orders.sql)
'stg_custmers' is undefined

Contrast this with a hardcoded table name, where a typo doesnโ€™t surface until the query actually executes against the warehouse โ€” often much later, and with a far less helpful error message.


ref() With Two Arguments: Cross-Project References

Since dbt Mesh matured, ref() supports a second form that pulls a model from an entirely separate dbt project:

select * from {{ ref('finance_project', 'monthly_revenue') }}

This only works if monthly_revenue has been explicitly exposed as a public model in the finance projectโ€™s configuration โ€” private models in another project arenโ€™t referenceable this way, which is a deliberate boundary that keeps teams from silently depending on each otherโ€™s internal implementation details.


Using ref() Inside Jinja Logic

Because ref() is a Jinja function, not special dbt syntax, it composes naturally with loops, conditionals, and variables โ€” something that trips up people coming from plain SQL who expect it to behave like a static keyword.

{% set regional_tables = ['us_orders', 'eu_orders', 'apac_orders'] %}
select * from (
{% for table in regional_tables %}
select * from {{ ref(table) }}
{% if not loop.last %} union all {% endif %}
{% endfor %}
)

This pattern is common in projects with regionally-sharded staging models that need to be unioned together in a single downstream model, and itโ€™s a preview of the kind of dynamic SQL generation covered fully in Jinja Templating.


What Happens When You Reference a Model That Doesnโ€™t Exist Yet

A common early mistake: writing ref('new_model') before new_model.sql exists anywhere in the project. dbt fails at compile time with a clear message rather than a mysterious runtime SQL error:

Compilation Error
Model 'my_model' (models/core/my_model.sql) depends on
a node named 'new_model' which was not found

This is a feature, not friction โ€” it means broken dependencies are caught before any warehouse compute is spent, which matters when youโ€™re running against a billed-by-the-second warehouse.


ref() and Materialization Independence

One of the underrated benefits: ref() doesnโ€™t care whether the upstream model is materialized as a table, a view, or is ephemeral (compiled inline as a CTE with no physical object at all). The calling modelโ€™s SQL looks identical regardless.

-- Works identically whether stg_orders is a view, table, or ephemeral model
select * from {{ ref('stg_orders') }}

This means you can change a modelโ€™s materialization strategy โ€” say, from a view to an incremental table, because query volume grew โ€” without touching a single downstream model that references it. That decoupling is explored in depth in the dbt Materializations guide already on this site.


Common Mistakes with ref()

Referencing by filename instead of model name. The string inside ref() must match the modelโ€™s configured name, which is the filename without .sql โ€” not a display name or an alias set elsewhere.

-- If the file is stg_orders.sql, this is correct
select * from {{ ref('stg_orders') }}
-- This fails even if you think of the model as "Staging Orders"
select * from {{ ref('Staging Orders') }}

Creating a circular reference. If model_a calls ref('model_b') and model_b calls ref('model_a'), dbt raises a cycle error at compile time. The fix is almost always extracting the shared logic both models need into a third, upstream model they can both depend on without depending on each other.

Mixing ref() and hardcoded tables in the same model. Doing this defeats the purpose โ€” the model becomes broken in any environment where the hardcoded table doesnโ€™t exist, and itโ€™s invisible in the lineage graph, making debugging much harder than it needs to be.

Summary

BehaviorWhat It Means
Compile-time resolutionTable names resolve per-environment before any SQL executes
DAG edge registrationEvery ref() call adds a dependency, shaping run order
Two-argument formEnables safe, explicit cross-project references (dbt Mesh)
Materialization independenceDownstream SQL never needs to know how upstream models are built

ref() is deceptively simple to type and enormously consequential in what it enables. Treat every occurrence of it in your models as a real, load-bearing dependency declaration โ€” because thatโ€™s exactly what it is.