SOURCES.YML
A source is data you didn't build — CRM tables in Oracle, ERP extracts in Snowflake, files someone drops in a bucket. sources.yml declares that data once, so every model can reach it with {{ source('name', 'table') }} instead of a hardcoded schema path.
Here is a complete, working file:
# models/staging/sources.yml
version: 2
sources:
- name: crm
meta:
connection: oracle_ops # ← the profiles.yml connection this data lives on
tables:
- name: customers
- name: ordersThe only line dbt wouldn't recognize is meta.connection — and that one line is most of what this page is about.
ONE EXTRA LINE: META.CONNECTION
dbt assumes every source lives on the one warehouse it's connected to, so its sources.yml never has to say where data is. DVT talks to many engines at once, so it has to ask. connection: is the answer: it names the output in profiles.yml where this data actually lives.
It sits under the source's meta:block because that is dbt's sanctioned home for custom keys — plain dbt reads the file happily and simply ignores it.
Every source — including default-target ones
Give every source a meta.connection naming its profiles.yml output. dbt ignores meta entirely, so it is harmless for standard models — and it is what federated models use to find the data. Bonus: when you later switch the default target, sources that already carry their connection keep working without edits.
The fallback when meta.connection is absent
If a source has no meta.connection, DVT falls back to the source's own name: a source called oracle_ops binds to the oracle_ops output. If neither resolves, federated models reading that source fail at extraction (DVT011). Standard models are unaffected — dbt resolves them from database/schema alone.
A FULLER EXAMPLE
One file can mix external engines, bucket storages, and tables on your default target — each source simply says where it lives:
# models/staging/sources.yml
version: 2
sources:
# ─── External Sources (need connection:) ───
- name: crm
description: "CRM data from Oracle operational database"
meta:
connection: oracle_ops # ← REQUIRED: points to profiles.yml
tables:
- name: customers
description: "Customer master data"
- name: orders
description: "Sales orders"
- name: products
- name: warehouse
description: "ERP data from Snowflake"
meta:
connection: sf_warehouse # ← REQUIRED: points to profiles.yml
database: PROD_DB
schema: RAW
tables:
- name: invoices
- name: shipments
- name: vendors
- name: data_lake
description: "Raw files from S3"
meta:
connection: data_lake # ← works with bucket storages too
tables:
- name: web_events
- name: app_logs
# ─── Default Target Sources ───
- name: analytics
description: "Tables on the default PostgreSQL target"
schema: public
meta:
connection: pg_dev # ← recommended even here: dbt ignores
tables: # it, and a future target switch is free
- name: seed_categories
- name: dim_dates
- name: historical_metricsHOW DVT READS SOURCES
How a model runs is decided by the model — its materialized config — never by the sources it reads. What meta.connection decides is where a federated model extracts each source from:
| SOURCE CONFIG | WHAT HAPPENS | SQL DIALECT |
|---|---|---|
| Standard model (table / view / incremental) | Sources on the default target: dbt resolves database/schema and runs natively. Sources on a foreign connection: DVT coerces the model to f_table / f_incremental automatically, with a warning | Target's native SQL (DuckDB SQL if coerced) |
| Federated model (f_table / f_incremental) | Each source extracted from its meta.connection (or name-matched output) via Sling into DuckDB | DuckDB SQL |
The rule of thumb: a standard model should only read sources that live on the default target. The moment a model needs data from another engine, declare it federated yourself — DVT will coerce it for you rather than fail, but an explicit materialization states your intent and silences the warning. See f_table on the federated materializations page.
COMMON PATTERNS
Three shapes cover most projects.
Multi-source extraction model:
-- models/marts/cross_engine_report.sql
-- crm is on Oracle, warehouse is on Snowflake
-- Both get extracted → DuckDB joins them → result loads to default target
{{ config(materialized='f_table') }}
SELECT
c.customer_name,
i.invoice_date,
i.total_amount
FROM {{ source('crm', 'customers') }} c -- Oracle (extracted)
JOIN {{ source('warehouse', 'invoices') }} i -- Snowflake (extracted)
ON c.customer_id = i.customer_idDefault-target pushdown model:
-- models/staging/stg_categories.sql
-- analytics source lives on the default target → pushdown, no extraction
-- SQL dialect: PostgreSQL (native)
{{ config(materialized='view') }}
SELECT id, name, parent_id
FROM {{ source('analytics', 'seed_categories') }}Bucket storage source:
-- models/staging/stg_web_events.sql
-- data_lake source points to S3 → extracted via Sling
{{ config(materialized='f_table') }}
SELECT event_id, user_id, event_type, event_timestamp
FROM {{ source('data_lake', 'web_events') }}LET DVT WRITE IT — DVT GENERATE-SOURCES
Nobody enjoys transcribing an information schema by hand. dvt generate-sources <connection>reads the connection's live catalog and writes <model-root>/<connection>_sources.yml in exactly the canonical shape shown above — meta.connection on every source. No AI anywhere: it is a metadata query and a renderer.
dvt generate-sources # no argument: lists your connections dvt generate-sources pg_docker # one connection → pg_docker_sources.yml dvt generate-sources --all # sweep the whole profile, one yml each dvt generate-sources sf_dev -s analytics.* # only these schemas/tables dvt generate-sources sf_dev -s sales '*.orders' -x '*.tmp_*' dvt generate-sources oracle_docker --print # stdout only, write nothing dvt generate-sources dbx_dev -o models/staging/dbx.yml dvt generate-sources s3_lake -s 'raw/*.parquet' # buckets work too
Generated sources are genuinely external — nothing else
Anything DVT itself materialized — models, seeds, tests — is tracked in DVT's own ledger and left out of the generated file, along with any staging leftovers DVT recognizes by its own marker (recorded, never dropped). Every run prints the exclusion counts, even when they are zero, so sources.yml stays what it should be: a declaration of data you didn't build.
Idempotent by design
When <connection>_sources.yml already exists, the fresh render replaces it and the previous version is kept beside it. --print previews the YAML without writing; --all writes one file per connection with a per-connection ledger and a nonzero exit if any connection failed.
Bucket connections (S3, GCS, Azure, SFTP) are first-class here too. They have no information schema, so DVT lists the connection's objects and declares its data files as file sources instead. The -s/-x patterns match the object path (raw/*.csv, **/*.parquet) rather than schema.table.
| FLAG | WHAT IT DOES |
|---|---|
| --all | Sweep every connection in the profile — databases walk their information schema, buckets list their objects. One yml per connection. |
| -s, --select | Only these schemas/tables (schema or schema.table, fnmatch wildcards). On buckets, patterns match the object path. |
| -x, --exclude | Drop these schemas/tables (same pattern language). |
| -o, --output | Write here instead of <model-root>/<connection>_sources.yml. Must stay inside the project's model-paths; doesn't compose with --all. |
| Print the YAML to stdout, write nothing. |
Every run also records what it learned about each table — columns, types, constraints — into the project's source-metadata store. The dvt metadata family reads and maintains that store, and the data catalog is its visual sibling. The full command walkthrough lives on the dvt generate-sources page.
FROM SOURCES TO BRONZE — DVT GENERATE-BASE-VIEWS
Once sources are declared and captured, dvt generate-base-views takes the next step for you: one bronze model per source table, written to models/bronze/<connection>/bronze_<table>.sql. Each model selects the exact columns the source-metadata store recorded, in the engine's own spelling — zero engine traffic and zero AI.
dvt generate-base-views # everything the store knows dvt generate-base-views --select-connection pg_docker dvt generate-base-views --select-table orders customers dvt generate-base-views --dry-run # print the plan, write nothing
The materialization is chosen by physics, not preference: a live view when the source lives on the default target, federated_table (the long spelling of f_table) when it doesn't — a plain view cannot cross engines. The generated header states which was chosen and why.
Nothing is overwritten silently: existing files are reported as skipped, and --force replaces them while keeping a timestamped .bak. The generated models reference declared sources — run dvt parse to check them, then dvt run --select bronze_* to build.
COMMON MISTAKES
WRONG
sources:
- name: analytics
connection: pg_dev # ← top level: dbt REJECTS this
tables:
- name: dim_datesCORRECT
sources:
- name: analytics
meta:
connection: pg_dev # ← under meta: dbt allows, DVT reads
tables:
- name: dim_datesconnection: must live under meta:. dbt validates sources.yml against its schema and errors on unknown top-level keys — meta is dbt's sanctioned extension point, which is exactly why DVT (a wrapper, not a fork) uses it.
WRONG
sources:
- name: crm
# Missing connection!
tables:
- name: customers # lives on OracleCORRECT
sources:
- name: crm
meta:
connection: oracle_ops # external source
tables:
- name: customersExternal sources must have connection:. Without it, DVT warns and falls back to a profiles.yml output named after the source ('crm') — and if no such output exists, extraction fails with DVT011. Add meta.connection to every source: one rule, no exceptions.
REFERENCE: SOURCE PROPERTIES
| PROPERTY | REQUIRED | DESCRIPTION |
|---|---|---|
| name | yes | Unique name for this source group |
| meta.connection | no | profiles.yml output name. Recommended on every source; without it the source name itself must match an output for federated reads. |
| database | no | Database name (overrides the output's default) |
| schema | no | Schema name (overrides the output's default) |
| description | no | Human-readable description for documentation |
| tables | yes | List of table objects with at minimum a name field |
| tags | no | Tags for source selection (dvt run --select tag:...) |
| freshness | no | Freshness check configuration (standard dbt) |
This page is the working guide. For the full file anatomy — every accepted key, with dbt-compatibility notes — see the sources.yml reference.