What Is dbt Source Freshness?
dbt source freshness is a built-in mechanism that checks whether the raw data in your source tables is recent enough for your pipelines to be trusted. It works by querying a loaded_at_field — typically a timestamp column like _airbyte_emitted_at or updated_at — and comparing the most recent value against configurable warn and error thresholds.
If the latest row in your source table is older than the threshold you defined, dbt marks that source as warn or error. This check runs as part of dbt source freshness (or dbt build with freshness enabled) and surfaces in your dbt Cloud job logs or CLI output.
Here is what a typical freshness result looks like in the CLI:
Found 4 sources. Freshness of 4 sources will be determined.
Source 'jaffle_shop.orders'
warn after: 12 hours
error after: 24 hours
Max loaded_at: 2026-06-07 08:33:00 UTC
Age: 26h 14m
Status: errorThe distinction between warn and error matters for pipeline control: an error status causes dbt source freshness to exit with a non-zero code, which can block downstream models from running if you wire your orchestrator accordingly. A warn is advisory — it shows up in the output but does not fail the job by default.
Freshness checks are evaluated at the source level, not at the model level. That means if three models all depend on jaffle_shop.orders, a single stale source can silently propagate through all three before anyone notices.
Configuring Freshness in schema.yml
Freshness is declared in your schema.yml (or any sources: YAML file in your dbt project). You define thresholds at either the source level (applies to all tables in that source) or at the individual table level.
version: 2
sources:
- name: jaffle_shop
database: raw
schema: public
freshness:
warn_after:
count: 12
period: hour
error_after:
count: 24
period: hour
loaded_at_field: _etl_loaded_at
tables:
- name: orders
description: Raw orders from the transactional database.
- name: customers
description: Customer master data.
freshness:
warn_after:
count: 6
period: hour
error_after:
count: 12
period: hour
loaded_at_field: updated_at
- name: events
freshness: null # skip freshness check for this tableA few things worth noting:
loaded_at_fieldmust be a timestamp column that reliably reflects when data was loaded or updated. Using a business timestamp likeorder_dateis usually wrong — it reflects event time, not ingestion time.- Setting
freshness: nullon a table disables the check entirely for that table, even if the parent source has a default configured. periodacceptsminute,hour, orday.- If
loaded_at_fieldis missing or the column contains NULLs, dbt will raise a compilation or runtime error.
Once configured, run the check with:
dbt source freshnessOr target a specific source:
dbt source freshness --select source:jaffle_shop.ordersThe results are also written to target/sources.json, which downstream tooling (including MetricSign) can consume to correlate freshness state with downstream impact.
In dbt Cloud, freshness checks appear as a separate step in job logs. You can configure the job to continue on warn but fail on error, giving you a clear signal when data is genuinely late.
Downstream Impact: Stale Data in Power BI and Tableau
A freshness violation in dbt is not just a dbt problem. The real damage happens downstream — in the BI layer where stakeholders are making decisions.
Here is a typical failure chain:
- Your Airbyte pipeline from Salesforce misses a sync window due to an API rate limit.
- The
salesforce.opportunitiessource table stops receiving new rows. - dbt runs on schedule and flags
salesforce.opportunitiesas stale (error after 6 hours). - If your orchestrator is not wired to block downstream models on freshness errors,
fct_pipeline_revenuerefreshes anyway — from stale data. - The Power BI dataset backed by
fct_pipeline_revenuerefreshes successfully. No error. No alert. - Your sales director opens the revenue dashboard and sees numbers that are 8 hours old — without any indication that the data is stale.
This is the most dangerous failure mode: silent staleness. The BI layer reports a successful refresh because, from its perspective, the data warehouse query succeeded. It has no way to know that the upstream source was stale when dbt ran.
In Tableau, the same pattern occurs with published data sources connected to Snowflake or BigQuery views. The extract refresh completes with a green checkmark, but the underlying view materializes from stale dbt models.
For ADF pipelines that trigger Power BI dataset refreshes via the Power BI REST API, there is no built-in mechanism to check dbt freshness state before triggering the refresh. The pipeline runs, the dataset refreshes, and the staleness is invisible.
The gap: dbt knows the data is stale. Power BI does not. There is no native bridge between them.
Quantifying the risk: if your fct_pipeline_revenue model runs every hour and your Salesforce sync is delayed by 4 hours, you will serve stale data to 4 consecutive dashboard refreshes before anyone is alerted — assuming someone is watching dbt logs at all.
Limits of dbt's Native Freshness Checks
dbt source freshness is a detection mechanism, not an alerting or impact-mapping system. Understanding what it does and does not do helps you avoid building false confidence into your data quality layer.
What dbt freshness does:
- Queries the loaded_at_field and computes staleness against your thresholds
- Emits warn or error status in CLI output and target/sources.json
- Exits non-zero on error, enabling orchestrators to block downstream runs
- Surfaces results in dbt Cloud job logs
What dbt freshness does not do: - Alert your team via Slack, PagerDuty, or email without additional tooling - Map which downstream dbt models depend on the stale source - Know which Power BI datasets, Tableau workbooks, or ADF pipelines will be affected - Track historical freshness trends over time (only the latest run is visible) - Distinguish between a source that is 1 hour stale versus 8 hours stale in its downstream impact
For example, target/sources.json contains a snapshot of the last freshness run:
{
"metadata": {"dbt_schema_version": "..."},
"results": [
{
"unique_id": "source.jaffle_shop.jaffle_shop.orders",
"status": "error",
"timing": [...],
"thread_id": "Thread-1",
"execution_time": 0.84,
"max_loaded_at": "2026-06-07T08:33:00+00:00",
"snapshotted_at": "2026-06-08T10:47:00+00:00",
"age_in_seconds": 94440.0
}
]
}This file tells you the source is stale. It does not tell you that three Power BI Premium datasets refreshed from that stale source in the meantime, that your CFO's financial dashboard is now showing yesterday's numbers, or that the ADF pipeline that triggered those refreshes completed without errors.
To close that gap, you need something that reads dbt freshness state and correlates it with downstream dataset refresh activity in real time.
Linking dbt Freshness to Downstream Staleness
MetricSign connects dbt source freshness violations directly to the downstream BI and pipeline events that were affected. Instead of checking dbt logs in one tab and Power BI refresh history in another, you get a single incident timeline.
How it works:
MetricSign ingests target/sources.json after each dbt run (via API push or polling) and cross-references it with the refresh activity it collects from the Power BI REST API, Tableau Server/Cloud REST API, and ADF run history. When a source enters error or warn state, MetricSign traces the lineage forward to identify which datasets were refreshed during or after that freshness window.
For Power BI, the relevant API call MetricSign uses is:
GET https://api.powerbi.com/v1.0/myorg/groups/{groupId}/datasets/{datasetId}/refreshesThis returns a refresh history with startTime, endTime, and status. MetricSign compares the startTime of each refresh against the max_loaded_at from dbt's freshness output. If a dataset refresh started after the source went stale and before the source recovered, that refresh is flagged as a stale refresh in the incident log.
In the MetricSign incident view, a dbt freshness violation surfaces as:
[INCIDENT] Source freshness error — jaffle_shop.orders
Detected: 2026-06-08 10:47 UTC
Stale since: 2026-06-07 08:33 UTC (26h 14m)
Affected downstream refreshes:
- Power BI: Sales Pipeline Report [workspace: Revenue] ← refreshed at 10:00 UTC (stale)
- Power BI: CFO Dashboard [workspace: Finance] ← refreshed at 09:00 UTC (stale)
- ADF Pipeline: trigger_pbi_refresh ← completed at 10:02 UTCAlert routing is configurable per source. For salesforce.opportunities, you might route error alerts to the #data-reliability Slack channel and create a PagerDuty incident if staleness exceeds 12 hours.
For Fabric and KQL users, MetricSign also supports querying refresh history via the Fabric REST API and correlating it with dbt freshness state in the same incident timeline. If you use Fabric eventhouse and want to query freshness violations in KQL:
FreshnessViolations
| where source_name == "jaffle_shop.orders"
| where status == "error"
| join kind=inner (
DatasetRefreshes
| where refresh_start > ago(48h)
) on $left.snapshotted_at >= $right.refresh_start
| project source_name, max_loaded_at, snapshotted_at, dataset_name, refresh_start, refresh_status
| order by refresh_start descThis query returns every dataset refresh that ran while the source was in a freshness error state — giving you the exact blast radius of a stale source without manually correlating timestamps across systems.
Practical Setup: End-to-End Freshness Monitoring
Here is a concrete setup that covers the full loop from dbt configuration to downstream alerting.
Step 1: Configure freshness in dbt
Add thresholds to your highest-risk sources first — typically transactional data from Salesforce, Hubspot, or Stripe where latency has business impact:
version: 2
sources:
- name: salesforce
database: raw
schema: salesforce
loaded_at_field: _airbyte_emitted_at
freshness:
warn_after: {count: 4, period: hour}
error_after: {count: 8, period: hour}
tables:
- name: opportunities
- name: accounts
- name: contacts
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}Step 2: Run freshness as part of your CI/CD or orchestration
In Airflow or Prefect, add a freshness check task before your dbt model runs:
# Fail fast if sources are stale
dbt source freshness --select source:salesforce
# Only proceed if freshness passes
dbt run --select tag:dailyIn dbt Cloud, enable the "Run source freshness" option in your job settings and configure the job to "Error on source freshness error" to block downstream model execution.
Step 3: Push freshness results to MetricSign
After each dbt run, post target/sources.json to the MetricSign ingest API:
curl -X POST https://app.metricsign.com/api/dbt/freshness \
-H "Authorization: Bearer $METRICSIGN_API_KEY" \
-H "Content-Type: application/json" \
-d @target/sources.jsonOr use the MetricSign dbt package (adds a post-hook that handles this automatically):
# packages.yml
packages:
- package: metricsign/dbt_metricsign
version: [">=0.3.0", "<0.4.0"]Step 4: Configure downstream dataset mapping
In MetricSign, map each dbt source to the Power BI workspaces and datasets that depend on it. This is a one-time setup that enables automatic blast-radius detection when freshness violations occur.
Step 5: Set alert thresholds
Configure separate alert channels for warn (Slack notification) and error (PagerDuty + Slack) so your team is not woken up for advisory warnings but is immediately paged for confirmed staleness that has affected downstream consumers.
With this setup, the time between a freshness violation occurring and your team knowing which dashboards are affected drops from hours (if anyone notices at all) to under 5 minutes.