Implementing and maintaining external dependencies: sample questions

3 free practice questions on implementing and maintaining external dependencies, one of the seven topics in the official v1.11 outline for the dbt Analytics Engineering Certification Exam — an estimated 6% of the exam, or about 4 of its 65 questions. Answers, reasoning and documentation links are all on this page.

Last updated . We revise these pages whenever dbt Labs revises the exam.

SHARE OF THE EXAM~6%our estimate
QUESTIONS HERE32 with runnable SQL
FORMATS SHOWN3of 6 on the exam

What this topic covers

Two subtopics, both about the edges of the project: implementing exposures, which declare the dashboards and downstream consumers that depend on your models, and implementing source freshness on the data arriving at the other end.

SUBTOPICS, FROM THE OFFICIAL OUTLINE · 2

  1. 01Implementing dbt exposures
  2. 02Implementing source freshness

Where the marks go

Freshness reduces to a single measurement — the age of the newest row, taken as max(loaded_at_field) — compared against two thresholds. The hotspot question asks you to find the table configured so that it can never report a warning, which is what happens when the thresholds are ordered so the error fires first.

Exposures produce documentation and a selector, not warehouse objects. Knowing that they are parsed from a .yml file under an exposures key, and what has to exist before the lineage renders, is the whole of the build-list question.

3 sample questions, with answers

Nothing is hidden. Each question shows the correct answer, why it is right, why every other option is wrong, and the documentation page that settles it. Where the answer is a claim about SQL behaviour, there is a query you can run in the browser against a small sample schema.

Q1Build listMediumRunnable proof

Your raw_billing source is already declared in sources.yml and models select from it, but dbt source freshness reports nothing for it. The project runs on Postgres, and the vendor stamps every row with synced_at. Arrange the steps to get a freshness state reported for the table, and to read what it measured. Two steps in the pool do not belong.

In the exam you drag the steps into order. The correct sequence is below.

  1. 1Under the raw_billing table's config: block, set loaded_at_field: synced_at and a freshness block with warn_after 12 hours and error_after 24 hours.
  2. 2Run dbt source freshness.
  3. 3Open target/sources.json and read the table's max_loaded_at and its status.
NOT USED
  • Run dbt build --select source:raw_billing+ so dbt reloads the vendor table before it is measured.
  • Run dbt compile to write the freshness result to target/sources.json.

WHY

Freshness is opt-in per source, and on Postgres it needs both halves of the config before anything can be measured. loaded_at_field is "Optional on adapters that support pulling freshness from warehouse metadata tables, required otherwise" — Postgres is not one of those adapters, so dbt has no timestamp column to measure without it. And the thresholds are what turn a timestamp into a verdict: "If neither is provided, then dbt will not calculate freshness for the tables in this source." Both live as sibling keys under the table's config: block, so they are one edit, not two ordered ones (s2). Running the command comes next and is genuinely separate work: dbt queries the table for the maximum loaded_at_field value and compares its age against the criteria (s4). Before the config exists there is nothing to calculate, which is exactly the state the question opens in — the command already runs today and reports nothing. Reading the artifact is last because the artifact does not exist until the command has run: "When dbt source freshness completes, a JSON file containing information about the freshness of your sources will be saved to target/sources.json", and sources.json is documented as produced by source freshness. It carries max_loaded_at ("Max value of loaded_at_field timestamp in the source table when queried"), snapshotted_at, and status — "one of pass, warn, or error" (s5). That is where the number behind the CLI verdict lives.

WHY THE OTHERS ARE WRONG

Run dbt build --select source:raw_billing+ so dbt reloads the vendor table before it is measured.
dbt does not load source data. A source is a declaration pointing at a table some other process populates — here the vendor's — so there is nothing for dbt to "reload" and no dbt command that refreshes the rows. dbt build --select source:raw_billing+ builds the MODELS downstream of the source, which changes nothing about the source table's own max synced_at and so cannot change its freshness state. Source: https://docs.getdbt.com/docs/build/sources
Run dbt compile to write the freshness result to target/sources.json.
dbt compile does not check freshness and does not write this artifact. sources.json is documented as produced by source freshness, and the freshness docs are specific that it appears "when dbt source freshness completes". compile renders model SQL into target/; it never queries the source for its maximum timestamp. Sources: https://docs.getdbt.com/reference/artifacts/sources-json · https://docs.getdbt.com/reference/commands/source
Q2HotspotMedium

Consider this exposures.yml block. You want dbt build --select +exposure:weekly_kpis to resolve to the models and sources the dashboard reads. Click the field that puts those upstream nodes in the exposure's lineage.

In the exam you click the region. The answer is highlighted below.

exposures:
- name: weekly_kpis
label: Weekly KPIs
type: dashboard
maturity: high
url: https://bi.example.com/dashboards/42
description: Weekly revenue and retention tiles.
depends_on:
- ref('orders')
- ref('customers')
- source('salesforce', 'accounts')
owner:
name: Data Team
email: data@example.com
Correct region: lines 8–11.

WHY

depends_on is the only field that creates graph edges. Each ref() or source() entry under it registers that node as a parent of the exposure, so the exposure becomes a leaf hanging off orders, customers, and the salesforce.accounts source. That lineage is what the exposure selection method walks: exposure:weekly_kpis selects the exposure node itself, and the + prefix expands to its ancestors — the three declared parents plus everything upstream of them. dbt never inspects the dashboard or its queries, so lineage is exactly what you list here and nothing more. (One caveat on the example itself: the docs note that "While possible, it is highly unlikely you will ever need an exposure to depend on a source directly" — the source() entry here is valid and makes the selection concrete, but a real exposure usually depends on models.)

WHY THE OTHERS ARE WRONG

Line 4
type classifies what the downstream tool is (dashboard, notebook, analysis, ml, application). It drives grouping and display on the docs site; it adds no parents and changes no selector result.
Line 5
maturity is an enumerated trust signal — "One of high, medium, or low" — shown in the docs. dbt validates it, so an invented value is a parsing error rather than an accepted label. Either way it annotates how reliable the exposure is, not what it reads from, and it adds no parents and changes no selector result.
Line 6
url is just a link to the dashboard, rendered on the exposure's docs page so a reader can open the tool. dbt does not fetch it or derive dependencies from it.
Lines 12–14
owner records the name and email of the person or team accountable for the exposure, surfaced in the docs. It is contact metadata, not a graph edge.
Q3MatchingMediumRunnable proof

Consider this sources.yml. You check each table with its own dbt source freshness --select run. Match each table to what dbt reports and the exit code that run returns. Two descriptions on the right will remain unused.

sources:
- name: raw_shop
config:
loaded_at_field: _loaded_at
freshness:
warn_after: {count: 6, period: hour}
error_after: {count: 12, period: hour}
tables:
- name: orders
- name: payments
- name: shipments
- name: raw_ref
tables:
- name: regions

In the exam you pair each item yourself. The correct pairings are below.

ITEMMATCHES
raw_shop.orders, newest row 2 hours oldReports pass; the run exits 0
raw_shop.payments, newest row 8 hours oldReports warn; the run exits 0
raw_shop.shipments, newest row 20 hours oldReports error; the run exits 1
raw_ref.regions, newest row 30 days oldReports no state; the table is left out of the check

WHY

dbt source freshness takes the newest loaded_at_field value in each selected table and measures how old it is, then compares that age to warn_after first and error_after second. orders at 2 hours is under both thresholds, so the table passes and the invocation exits 0. payments at 8 hours is past warn_after but short of error_after, so dbt reports warn — and a warning is not a failure, so the invocation still succeeds and exits 0. The command reference's line that a source table 'in a stale state' makes dbt exit non-zero is about the error state, not the warn state: the warnings reference settles it by listing 'tests and freshness checks that are configured to return warnings' among the things WARN_ERROR converts INTO errors, which they therefore are not by default. shipments at 20 hours is past error_after, so dbt reports error and the invocation exits 1; that non-zero code is what lets the command gate a downstream job. raw_ref.regions has no freshness block on the table, on its source, or in the project, so dbt calculates no freshness snapshot for it at all — omitting the block is the documented way to keep a source out of the check, and the 30-day age never gets evaluated.

WHY THE OTHERS ARE WRONG

Reports warn; the run exits 1
This is --warn-error behaviour, not the default. On a normal run a warn state prints the WARN line and lands in target/sources.json, but the invocation still succeeds and exits 0. The warnings reference is explicit that freshness checks configured to return warnings are among the warnings that WARN_ERROR promotes to errors — promotion is what produces exit code 1 here, and it is opt-in.
Reports pass; dbt falls back to default thresholds
dbt ships no built-in freshness thresholds, so an unconfigured table cannot 'default' to pass. The documented way to exclude a source from freshness calculations is precisely to leave the freshness block off — impossible if a default existed.

Know whether implementing and maintaining external dependencies is actually costing you marks.

The free readiness check is weighted like the real exam across all seven topics, so it tells you where you stand on this topic relative to the rest — which is the only version of that question worth answering before you book.

Take the free readiness check20 questions · ~15 minutes · no card

The other six topics

Keep reading

SOURCES

Every source above was read in full and last checked on .