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:
- It resolves to the correct physical table name for whatever environment youโre running in โ
dev_yourname.stg_customerslocally,analytics.stg_customersin production,ci_pr_482.stg_customersin a CI job. - It registers a dependency edge in the projectโs DAG, so dbt knows
stg_customersmust finish building before the current model runs.
-- What you writeselect * from {{ ref('stg_customers') }}
-- What it compiles to, in productionselect * from analytics.stg_customers
-- What it compiles to, in your dev environmentselect * from dev_alice.stg_customersThis 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 undefinedContrast 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 foundThis 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 modelselect * 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 correctselect * 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
| Behavior | What It Means |
|---|---|
| Compile-time resolution | Table names resolve per-environment before any SQL executes |
| DAG edge registration | Every ref() call adds a dependency, shaping run order |
| Two-argument form | Enables safe, explicit cross-project references (dbt Mesh) |
| Materialization independence | Downstream 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.