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.

The dbt source() Function: Referencing Raw Data Correctly

Declaring a source in a sources.yml file, as covered in dbt Sources, only gets you halfway there. The other half is actually using that declaration inside a model โ€” and thatโ€™s what the source() function is for. It looks almost identical to ref() on the surface, but it points at a fundamentally different kind of object: a raw table your pipeline loaded, not a model dbt itself builds.


Basic Syntax

source() takes two arguments: the name of the source group, and the name of the table within it, both matching exactly what you declared in YAML.

-- models/staging/stg_orders.sql
select
id as order_id,
customer_id,
cast(created_at as date) as order_date,
amount,
status
from {{ source('raw', 'orders') }}
where status != 'test'

Given the source declaration:

sources:
- name: raw
schema: raw_app_data
tables:
- name: orders

source('raw', 'orders') compiles to a fully-qualified reference to raw_app_data.orders (plus whatever database is configured), resolved correctly for whatever environment the run is targeting.


Why source() Instead of a Direct Table Reference

You could write select * from raw_app_data.orders and it would run identically the first time. The difference shows up later:


The One-Model-Per-Source-Table Convention

The pattern that scales cleanly: exactly one staging model per source table, and that staging model is the only place source() is called for that table.

raw source: orders
โ”‚
โ–ผ
stg_orders.sql โ† the only model calling source('raw', 'orders')
โ”‚
โ–ผ
customer_orders.sql โ† calls ref('stg_orders'), never touches the source directly
โ”‚
โ–ผ
sales_summary.sql

If you later need to change how raw orders are cleaned โ€” a new status filter, a renamed column, a type cast fix โ€” thereโ€™s exactly one file to edit, and every downstream model inherits the fix automatically through its ref() chain.


What Staging Models Typically Do With source()

A staging model is rarely a bare select *. Itโ€™s the layer where light, mechanical cleanup happens โ€” renaming cryptic columns, casting types, filtering out test/internal records โ€” before anything business-logic-shaped happens downstream.

-- models/staging/stg_customers.sql
with source as (
select * from {{ source('raw', 'customers') }}
),
renamed as (
select
id as customer_id,
lower(trim(email)) as email,
first_name || ' ' || last_name as full_name,
cast(signup_date as date) as signup_date,
is_deleted
from source
where is_deleted = false
)
select * from renamed

This staging model is doing exactly one job: taking whatever shape the raw loader produced and normalizing it into something predictable that every downstream model can rely on without re-deriving the same cleanup logic repeatedly.


Multiple Sources in One Model โ€” When Itโ€™s Actually Fine

The โ€œonly staging models call source()โ€ rule has one common, legitimate exception: a staging model that needs to union near-identical tables from multiple regional or sharded sources before anything else touches the data.

-- models/staging/stg_orders_all_regions.sql
select *, 'us' as region from {{ source('raw_us', 'orders') }}
union all
select *, 'eu' as region from {{ source('raw_eu', 'orders') }}
union all
select *, 'apac' as region from {{ source('raw_apac', 'orders') }}

This is still consistent with the convention โ€” itโ€™s a staging model, itโ€™s the only place these particular sources are referenced, and everything downstream consumes the unioned result via ref().


source() Across Multiple Schemas in One Warehouse

Larger organizations frequently split raw data across several schemas or even several databases within the same warehouse account โ€” one for each upstream system, or one per region. source() handles this transparently as long as each source groupโ€™s database and schema are declared correctly.

sources:
- name: raw_us
database: analytics_raw
schema: us_east_app_data
tables:
- name: orders
- name: raw_eu
database: analytics_raw_eu
schema: eu_west_app_data
tables:
- name: orders
select *, 'us' as region from {{ source('raw_us', 'orders') }}
union all
select *, 'eu' as region from {{ source('raw_eu', 'orders') }}

Because both calls go through source(), the lineage graph correctly shows both raw tables as separate upstream nodes feeding this staging model, even though they live in physically different databases โ€” something a hardcoded cross-database reference would obscure entirely from dbtโ€™s dependency tracking.

Debugging a Broken source() Call

If source('raw', 'orders') fails to compile, the error is almost always one of three things:

Compilation Error
Source raw.orders not found
  1. The source group name is wrong. Check the name: field in your sources.yml matches exactly โ€” itโ€™s case-sensitive.
  2. The table name is wrong or missing. The table must be explicitly listed under tables: in the source block; dbt doesnโ€™t infer available tables from the schema automatically.
  3. The YAML file isnโ€™t where dbt expects it. Source files need to live somewhere dbt scans โ€” typically under models/, matching your model-paths config in dbt_project.yml.

source() vs ref(): A Quick Mental Model

source()ref()
Points atRaw table loaded outside dbtA model dbt itself builds
Used inStaging models only (by convention)Any downstream model
EnablesFreshness checks, source-level testsDAG dependency, environment portability
Declared insources.ymlJust the model file existing

Summary

The source() function is what turns a raw, unmanaged table into a first-class, trackable node in your dbt project. Used consistently โ€” one staging model per source, everything downstream using ref() โ€” it keeps your entire lineage graph accurate and your project resilient to upstream schema drift. Skipping it in favor of hardcoded table names saves a few minutes today and costs hours the first time something breaks three layers downstream.