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

The 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

SHOULD have meta.connection:

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

source name must match an output name

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_metrics

HOW 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 CONFIGWHAT HAPPENSSQL 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 warningTarget'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 DuckDBDuckDB 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_id

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

every dvt-managed relation is excluded, and the count is stated

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

re-run any time — the previous file is kept as a timestamped .bak

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.

FLAGWHAT IT DOES
--allSweep every connection in the profile — databases walk their information schema, buckets list their objects. One yml per connection.
-s, --selectOnly these schemas/tables (schema or schema.table, fnmatch wildcards). On buckets, patterns match the object path.
-x, --excludeDrop these schemas/tables (same pattern language).
-o, --outputWrite here instead of <model-root>/<connection>_sources.yml. Must stay inside the project's model-paths; doesn't compose with --all.
--printPrint 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_dates

CORRECT

sources:
  - name: analytics
    meta:
      connection: pg_dev         # ← under meta: dbt allows, DVT reads
    tables:
      - name: dim_dates

connection: 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 Oracle

CORRECT

sources:
  - name: crm
    meta:
      connection: oracle_ops      # external source
    tables:
      - name: customers

External 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

PROPERTYREQUIREDDESCRIPTION
nameyesUnique name for this source group
meta.connectionnoprofiles.yml output name. Recommended on every source; without it the source name itself must match an output for federated reads.
databasenoDatabase name (overrides the output's default)
schemanoSchema name (overrides the output's default)
descriptionnoHuman-readable description for documentation
tablesyesList of table objects with at minimum a name field
tagsnoTags for source selection (dvt run --select tag:...)
freshnessnoFreshness 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.