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: orders

Models 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 table

Resolution 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 table
dvt 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: payments

One 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, 2

See 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: true

The 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: regions

Re-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

KEYLEVELREQUIREDDESCRIPTION
versionfileyesAlways 2.
sourcesfileyesThe list of source groups.
namesourceyesUnique name for the group — the first argument to source().
meta.connectionsourcenoThe 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.)
databasesourcenoDatabase/catalog, when it differs from the connection's default.
schemasourcenoSchema the tables live in. Not used for bucket sources — object paths carry their own location.
descriptionsourcenoHuman-readable description, shown in docs and the catalog.
loaded_at_fieldsource / tablenoTimestamp column used by freshness checks. Table-level overrides source-level.
freshnesssource / tablenowarn_after / error_after thresholds ({ count, period }); period is minute, hour, or day. Set to null on a table to opt out.
tablessourceyesThe tables this group declares. On buckets: one entry per data file, name = object path.
tables[].nametableyesTable name — the second argument to source(). On buckets, the object path (raw/events.parquet).
tables[].identifiertablenoThe relation's actual name in the database, when it differs from name.
tables[].quoting.identifiertablenoQuote the identifier in compiled SQL. Required in practice for object-path names; generate-sources sets it automatically.
tables[].meta.connectiontablenoPer-table connection override — wins over the source-level value.
tables[].columnstablenoColumn list: name, description, tests.
columns[].testscolumnnoStandard dbt data tests (unique, not_null, accepted_values, relationships). Tests on non-default-connection sources are skipped loudly.
tagssource / tablenoTags for selection (dvt run --select tag:...).