Implementing and Maintaining External Dependencies
This domain covers everything your dbt project consumes but does not itself build: sources (raw tables loaded by EL tools), packages (reusable dbt code installed from the dbt Hub, git, tarballs, or local paths), and cross-project dependencies in dbt Mesh. It is weighted at roughly 14% of the exam — about one question in seven. Questions are heavily YAML-syntax-driven: you will be shown a sources: block, a packages.yml, or a dependencies.yml and asked what it resolves to, why it errors, or which of four near-identical snippets is correct. Expect a cluster of questions on freshness configuration, version pinning semantics, and the difference between installing a project as a package versus referencing it across projects.
Sources: declaring, documenting, and testing raw source tables
A source is a declaration of a table that already exists in the warehouse and that dbt never creates, never builds, and never drops. Declaring it gives you three things: the {{ source() }} function, lineage from raw data into your DAG, and a place to hang documentation, tests, and freshness rules.
Declaration syntax
Sources are defined in .yml files inside a configured path (model-paths, by default models/). There is no way to define a source in dbt_project.yml other than a handful of configs (+enabled, +meta, +tags, +freshness).
version: 2
sources:
- name: jaffle_shop # the name used in source('jaffle_shop', ...)
description: Raw app data landed by Fivetran
database: raw # defaults to target.database
schema: jaffle_shop_prod # defaults to the source `name`
loader: fivetran # documentation only
loaded_at_field: _etl_loaded_at
tags: [raw, pii]
meta:
owner: data-engineering
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}
filter: _etl_loaded_at > dateadd(day, -3, current_date)
tables:
- name: orders
identifier: raw_orders # actual relation name in the warehouse
description: One row per order
columns:
- name: id
description: Primary key
data_tests:
- unique
- not_null
- name: customers
freshness: null # opts this table OUT of freshness checks
Key resolution rules you must memorize:
| Property | Default if omitted | Notes |
|---|---|---|
database | target.database | Cross-database sources are legal on adapters that support it |
schema | the source name | This is why name and schema are so often confused |
identifier | the table name | Use it when the warehouse name is ugly: name: orders, identifier: _RAW_ORDERS_V2 |
loaded_at_field | none | Required for freshness on adapters without metadata-based freshness |
quoting | project/adapter default | Set at source or table level for case-sensitive relations |
Reference it in a model as {{ source('jaffle_shop', 'orders') }} — source name first, table name second. It compiles to raw.jaffle_shop_prod.raw_orders given the YAML above.
How a source declaration resolves to a physical relation and flows into the DAG
Testing sources
Source tests are written exactly like model tests, under columns: data_tests: or table-level data_tests:. In dbt 1.8+ the key is data_tests; the legacy key tests still parses but is deprecated. Source tests are the only automated contract you have against a table you do not control.
- Run them alone:
dbt test --select source:jaffle_shop - One table:
dbt test --select source:jaffle_shop.orders - All sources:
dbt test --select source:* dbt builddoes execute source tests when they are in the selection; it does not run freshness.
Freshness
dbt source freshness runs a select max(loaded_at_field) per table and compares it to the current warehouse timestamp.
| Setting | Meaning | Result |
|---|---|---|
warn_after | Staleness threshold for a warning | Yellow WARN status |
error_after | Staleness threshold for failure | ERROR status, non-zero exit code |
filter | Predicate injected into the freshness query | Used to prune huge partitioned tables |
freshness: null at table level | Disables inherited freshness | Table reported as skipped |
period accepts minute, hour, day only. Results are written to sources.json in the target directory. From dbt 1.9, on supporting adapters (Snowflake, BigQuery, Databricks/Spark, Redshift), loaded_at_field can be omitted and dbt uses warehouse metadata for last-modified time; dbt 1.10 added loaded_at_query for a custom freshness expression. If neither a loaded_at_field, loaded_at_query, nor metadata support exists, the table is skipped.
Selection using freshness state: dbt build --select source_status:fresher+ --state path/to/prod-artifacts rebuilds only models downstream of sources that got new data since the stored sources.json.
Worked scenario
Requirement: raw.finance.gl_entries is loaded hourly on weekdays, but raw.finance.exchange_rates is loaded once a week. Both live in the same source. CI must fail if GL data is more than 6 hours old, but must never fail on exchange rates.
Reasoning: Put freshness with error_after: {count: 6, period: hour} at the source level so it is inherited, then set freshness: null on the exchange_rates table to opt it out. Do not delete loaded_at_field — that would silently skip the table with a warning rather than deliberately disabling it, and it would break the day someone adds source-level freshness back.
Common traps
source()takes the source name, not the schema name. Ifname: jaffle_shopbutschema: raw_jaffle, the call is stillsource('jaffle_shop', 'orders'). Distractors swap these.ref()cannot point at a source, andsource()cannot point at a model. A "source" that dbt builds is not a source — it is a model.- Freshness is not a test.
dbt testdoes not check freshness;dbt builddoes not check freshness. Onlydbt source freshnessdoes. identifiervsname:nameis the dbt-facing alias used insource();identifieris the physical table.- Sources cannot be materialized, snapshotted, or given a
+materializedconfig. Configs available are essentiallyenabled,meta,tags,freshness,quoting,database/schema/identifier,overrides. - Tags set at source level apply to all its tables; table-level tags add to, not replace, them.
- A package can define sources, and you override them in your project with the
overrides:key naming the package — not by redefining the same source name (which raises a duplicate-source error). - Freshness warnings do not fail a job; only
error_afterbreaches produce a non-zero exit code.
Unlock with a paid plan
7 more sections (about 3,019 words) continue below. Every paid plan unlocks all dbt Analytics Engineering Certification study guides, the full question bank and mock exams, from $4.97/month.