dbt Sources Explained: Defining and Tracking Raw Data in Your Warehouse
Every dbt project starts somewhere, and that somewhere is never a dbt model โ itโs a raw table sitting in your warehouse, loaded by Fivetran, Airbyte, a custom pipeline, or a nightly cron job you inherited from someone who left the company two years ago. dbt sources are the mechanism that formally introduces those raw tables into your project, and skipping this step is one of the most common ways analytics engineering teams end up with fragile, undocumented pipelines.
This guide covers what sources actually are, why they matter more than they look like they do, and how to configure them correctly from day one.
What a Source Actually Is
A dbt source is a YAML declaration that tells dbt: โthis table exists, it lives in this schema, it was loaded by this process, and here is what I expect to be true about it.โ It doesnโt create anything in your warehouse โ sources point at tables that already exist, usually populated by an extraction/load tool outside of dbtโs control.
version: 2
sources: - name: raw database: analytics_raw schema: raw_app_data tables: - name: orders - name: customers - name: order_itemsThatโs the minimum viable source definition โ a name for the source group (raw), the physical location, and the tables within it. Once this exists, every raw table in that group becomes referenceable inside your models via the source() function, which is covered in detail in source() Function.
Why Not Just Hardcode the Schema Name?
You could write select * from analytics_raw.raw_app_data.orders directly in a model, and it would work โ right up until you deploy to a different environment where the raw schema has a different name, or the loader migrates to a new database. Declaring sources explicitly buys you three things hardcoded references never give you:
- Environment portability. The same source declaration resolves correctly whether youโre pointed at a dev, staging, or prod target.
- A single point of truth for raw table locations. If a loader tool renames a schema, you update one YAML file, not every model that touches it.
- A place to attach freshness checks, descriptions, and tests โ none of which are possible on a bare table reference.
Configuring Sources with Metadata
A well-documented source file does more than list table names. It captures the operational reality of where the data comes from and what โhealthyโ looks like.
version: 2
sources: - name: raw description: "Raw application data loaded via Fivetran from the production Postgres instance." database: analytics_raw schema: raw_app_data loaded_at_field: _fivetran_synced freshness: warn_after: {count: 12, period: hour} error_after: {count: 24, period: hour} tables: - name: orders description: "One row per order. Loaded incrementally every 15 minutes." columns: - name: id description: "Primary key" tests: - unique - not_null - name: customers description: "One row per registered customer account."Two things are worth calling out here. First, loaded_at_field is what makes freshness checks possible โ dbt uses that column to determine how recently the source was updated, which is explored fully in Source Freshness. Second, tests can be attached directly under a sourceโs columns, exactly like they can on a model โ this is what Source Testing builds on.
Multiple Source Blocks for Multiple Systems
Most real projects pull from more than one upstream system, and thereโs no reason to cram everything into a single raw source. Splitting by origin system keeps things organized and mirrors how your data actually flows in.
version: 2
sources: - name: stripe database: analytics_raw schema: stripe_billing tables: - name: charges - name: subscriptions
- name: salesforce database: analytics_raw schema: sfdc_sync tables: - name: accounts - name: opportunitiesNow source('stripe', 'charges') and source('salesforce', 'accounts') are both unambiguous, and each system can have its own freshness SLA โ Stripe webhooks might update every few minutes, while a nightly Salesforce sync only needs a 24-hour freshness window.
Where Source Files Should Live
Thereโs no dbt-enforced rule about file location, but the convention that scales well is one _sources.yml per staging subdirectory, colocated with the staging models that reference that source:
models/ staging/ stripe/ _sources.yml stg_stripe__charges.sql stg_stripe__subscriptions.sql salesforce/ _sources.yml stg_salesforce__accounts.sqlThis keeps the mapping between โwhat raw tableโ and โwhat staging model cleans it upโ obvious to anyone browsing the repo, without needing to jump between distant folders.
A Common Mistake: Referencing Sources Outside Staging
Itโs technically legal to call source() from any model in your project, but doing so from a mart or core model is a maintainability trap. If five different downstream models each reference source('raw', 'orders') directly, a schema change to that raw table means auditing five models instead of one. The convention that holds up at scale: only staging models call source(); everything downstream uses ref() against those staging models.
-- Good: only the staging model touches the source directly-- models/staging/stg_orders.sqlselect * from {{ source('raw', 'orders') }}
-- Downstream models reference the staging model, never the source-- models/core/customer_orders.sqlselect * from {{ ref('stg_orders') }}Sources vs. Seeds: A Quick Distinction
Itโs easy to conflate sources with seeds since both involve โexternalโ data, but they solve different problems. A source points at a table your pipeline already loaded into the warehouse. A seed is a small CSV file dbt itself loads into the warehouse from your repository โ covered in dbt Seeds. If your data changes daily and arrives via an ELT tool, itโs a source. If itโs a static lookup table (country codes, a manually maintained mapping), itโs a seed.
Summary
| Concept | Purpose |
|---|---|
sources.yml | Declares raw tables and where they physically live |
loaded_at_field | Column dbt uses to compute source freshness |
source('name', 'table') | The function used inside models to reference a source |
| Convention | Only staging models should call source() directly |
Sources are the least glamorous part of a dbt project and also one of the most important. A project with well-declared, well-tested sources fails loudly and immediately when upstream data breaks โ a project without them fails silently, three layers downstream, days later.