SOURCES.YML
sources.ymldeclares the raw data your project reads — the tables you didn't build. Once a table is declared here, any model can reference it with {{ source('name', 'table') }} and DVT knows exactly which engine to fetch it from.
This page is the file reference — every key, with examples. For the concept (how sources drive federation) see the sources page.
A MINIMAL SOURCES.YML
The file lives anywhere under your model-paths (conventionally models/ or models/staging/). The smallest useful file is a version, one source with a name, its meta.connection, and a list of tables:
# models/staging/sources.yml
version: 2
sources:
- name: crm
meta:
connection: pg_prod # a named output in profiles.yml
schema: public
tables:
- name: customers
- name: ordersModels reference it by source name and table name — never by raw schema.table:
-- models/staging/stg_customers.sql
select customer_id, customer_name, created_at
from {{ source('crm', 'customers') }}VS PLAIN DBT
In plain dbt a source can only describe data your single target can already reach. In DVT, meta.connection binds each source to any named connection in profiles.yml — Postgres, Oracle, Snowflake, an S3 bucket — and federated models pull from all of them in one query.
META.CONNECTION — THE KEY THAT BINDS
meta.connection is DVT's one extension to the file. It names the profiles.yml output this data lives on. It sits under meta: because that is dbt's sanctioned custom-key slot — dbt ignores it, DVT enforces it, and your project stays a valid dbt project.
sources:
- name: warehouse
meta:
connection: sf_finance # ← source-level: applies to every table below
tables:
- name: invoices
- name: legacy_invoices
meta:
connection: sf_archive # ← table-level: overrides the source for this one tableResolution order, exactly as the engine applies it: table-level meta.connection → source-level meta.connection→ the source's own name as a fallback. If a source called crm has no connection, DVT assumes an output named crm and warns:
Source crm.customers has no meta.connection — assuming a profiles.yml output named 'crm'. Add meta.connection to every source (one rule, no exceptions).
If the fallback doesn't name a real output either, federated models reading that source fail at extraction with a connection error (DVT011). The rule that avoids all of this is simple: every source gets a meta.connection — including sources on the default target, where it costs nothing today and makes a future target switch free.
You may still meet meta.dvt_connection in older projects — it is kept as a legacy alias and means the same thing. Write connection in new files.
COLUMNS AND DATA TESTS
Columns document each table and carry yml data tests — standard dbt syntax, nothing new to learn. dvt test runs them.
sources:
- name: crm
meta:
connection: pg_prod
schema: public
tables:
- name: customers
description: "One row per customer, maintained by the CRM team"
columns:
- name: customer_id
description: "Primary key"
tests:
- unique
- not_null
- name: segment
tests:
- accepted_values:
values: ['smb', 'mid', 'enterprise']One honest caveat: dbt compiles source tests against the defaultengine. A test on a source bound to a non-default connection can never run there — the relation doesn't exist on that engine — so DVT skips it loudly instead of erroring:
Skipping 2 source test(s) on non-default connections (the engine cannot reach them from 'pg_prod'): source_unique_crm_customers_ customer_id, source_not_null_crm_customers_customer_id
Put your tests on the models that read those sources — they materialize on engines the tests can reach.
FRESHNESS
Freshness thresholds are standard dbt: declare loaded_at_field (the timestamp column that says when a row arrived) and how stale is too stale. dvt source freshness checks every declared source against them.
sources:
- name: crm
meta:
connection: pg_prod
schema: public
loaded_at_field: created_at
freshness:
warn_after: { count: 12, period: hour }
error_after: { count: 24, period: hour }
tables:
- name: orders # inherits the source-level freshness
- name: countries
freshness: null # static reference data — opt out per tabledvt source freshness dvt source freshness --select source:crm
A COMPLETE EXAMPLE: TWO ENGINES, ONE FILE
This is the shape of a real project: operational data on Oracle, finance marts on Snowflake, both declared in one file, each bound to its own connection.
# models/staging/sources.yml
version: 2
sources:
- name: crm
description: "Operational CRM on Oracle"
meta:
connection: oracle_ops
schema: CRM
loaded_at_field: created_at
freshness:
warn_after: { count: 12, period: hour }
error_after: { count: 24, period: hour }
tables:
- name: customers
description: "Customer master data"
columns:
- name: customer_id
tests: [unique, not_null]
- name: orders
- name: finance
description: "Finance marts on Snowflake"
meta:
connection: sf_finance
database: PROD_DB
schema: FINANCE
tables:
- name: invoices
- name: paymentsOne federated model now joins across both engines — the sources' connections tell DVT where to extract each side from:
-- models/marts/customer_revenue.sql
{{ config(materialized='f_table') }}
select
c.customer_id,
c.customer_name,
sum(i.total_amount) as lifetime_revenue
from {{ source('crm', 'customers') }} c -- Oracle
join {{ source('finance', 'invoices') }} i -- Snowflake
on c.customer_id = i.customer_id
group by 1, 2See f_table & f_incremental for what happens at run time.
BUCKET SOURCES: FILES AS TABLES
S3, GCS, Azure Blob, SFTP, and local-filesystem connections are declared the same way — except a bucket has no schemas, so the table nameis the object's path relative to the connection's root. The file's own extension names its format: csv, parquet, json, and jsonl (.ndjson is an accepted spelling; .gz/.bz2/.zst compression may trail it). Any other extension is refused by name before a byte moves — there is no format key to set, ever.
sources:
- name: data_lake
description: "Raw exports on S3"
meta:
connection: s3_lake
tables:
- name: raw/events.parquet
quoting:
identifier: true
- name: exports/customers.csv
quoting:
identifier: true
- name: logs/app.jsonl
quoting:
identifier: trueThe quoting: identifier: true matters: an object path contains slashes and dots, and SQL engines disagree about quoting bare identifiers. Quoted, the source compiles the same on every engine instead of depending on which one is your default. (dvt generate-sources adds it for you automatically.)
-- models/staging/stg_events.sql
{{ config(materialized='f_table') }}
select event_id, user_id, event_type, event_timestamp
from {{ source('data_lake', 'raw/events.parquet') }}DON'T WRITE IT BY HAND — DVT GENERATE-SOURCES
Here is the part that changes the workflow: dvt generate-sources introspects a connection live and writes this entire file for you — every schema, every table, meta.connection already on every source. Zero AI involved: it is a metadata query and a renderer.
VS PLAIN DBT
In plain dbt you'd hand-type every source and table name, or bolt on a codegen package. DVT ships it in the core CLI: one command per connection, and the file is written in canonical form — with your own dvt-built models automatically excluded, so nothing you materialized ever re-enters as a "source".
$ dvt generate-sources pg_prod pg_prod (postgres): 14 table(s) across 2 schema(s) excluded 3 dvt-managed relation(s) (3 from ledger) excluded 0 DVT staging leftover(s) — recognized by DVT's own marker, recorded in the metadata store, never dropped Wrote models/pg_prod_sources.yml
And the file it wrote — canonical form, ready to prune and commit:
# generated by /generate-sources from connection 'pg_prod' (postgres)
# 14 table(s) across 2 schema(s) — prune what you don't need
version: 2
sources:
- name: pg_prod__public
meta:
connection: pg_prod
database: devdb
schema: public
tables:
- name: customers
- name: orders
- name: payments
- name: pg_prod__sales
meta:
connection: pg_prod
database: devdb
schema: sales
tables:
- name: order_items
- name: regionsRe-run it any time — the previous file is kept beside it as a timestamped .bak. Bucket connections work too: DVT lists the objects and declares the data files as sources, paths quoted. --all sweeps the whole profile, -s/-x narrow the selection, --print previews without writing — the full flag reference is on the sources page.
FIELD REFERENCE
| KEY | LEVEL | REQUIRED | DESCRIPTION |
|---|---|---|---|
| version | file | yes | Always 2. |
| sources | file | yes | The list of source groups. |
| name | source | yes | Unique name for the group — the first argument to source(). |
| meta.connection | source | no | The profiles.yml output this data lives on. Effectively required: without it DVT falls back to the source name, with a warning. (meta.dvt_connection is a legacy alias.) |
| database | source | no | Database/catalog, when it differs from the connection's default. |
| schema | source | no | Schema the tables live in. Not used for bucket sources — object paths carry their own location. |
| description | source | no | Human-readable description, shown in docs and the catalog. |
| loaded_at_field | source / table | no | Timestamp column used by freshness checks. Table-level overrides source-level. |
| freshness | source / table | no | warn_after / error_after thresholds ({ count, period }); period is minute, hour, or day. Set to null on a table to opt out. |
| tables | source | yes | The tables this group declares. On buckets: one entry per data file, name = object path. |
| tables[].name | table | yes | Table name — the second argument to source(). On buckets, the object path (raw/events.parquet). |
| tables[].identifier | table | no | The relation's actual name in the database, when it differs from name. |
| tables[].quoting.identifier | table | no | Quote the identifier in compiled SQL. Required in practice for object-path names; generate-sources sets it automatically. |
| tables[].meta.connection | table | no | Per-table connection override — wins over the source-level value. |
| tables[].columns | table | no | Column list: name, description, tests. |
| columns[].tests | column | no | Standard dbt data tests (unique, not_null, accepted_values, relationships). Tests on non-default-connection sources are skipped loudly. |
| tags | source / table | no | Tags for selection (dvt run --select tag:...). |