npx skills add ...
npx skills add dbt-labs/dbt-agent-skills --skill working-with-dbt-mesh
npx skills add dbt-labs/dbt-agent-skills --skill working-with-dbt-mesh
Use when changing a dbt model in a way that could break its consumers — renaming, removing, or retyping a column, or changing a model that downstream models, exposures, dashboards, or BI tools depend on — to judge whether the change is breaking and who it affects. Also use when versioning a model (model versions, latest_version, latest_version_pointer, deprecation_date, migration windows), enforcing contracts, setting access or groups, or doing multi-project dbt Mesh work (cross-project refs via dependencies.yml, disambiguating similarly-named models, splitting a monolith). Covers single- and multi-project, and planning or advising as well as implementing.
Core principle: In a mesh project, upstream data comes through ref(), not source(). Every cross-project reference requires the project name. When in doubt, read dependencies.yml first.
versions:, latest_version, latest_version_pointer, deprecation_date) — this applies in a single project, not just multi-project setupsstg_ models)dependencies.ymlDo NOT use for:
using-dbt-for-analytics-engineering skill)adding-dbt-unit-test skill)building-dbt-semantic-layer skill)Before writing or modifying any SQL in a project that uses dbt Mesh, follow these steps:
dependencies.ymlThis file at the project root tells you which upstream projects exist:
If this file has a projects: key, you are in a multi-project mesh setup. Every model you reference from those upstream projects must use cross-project ref().
In a mesh setup, upstream project models replace what would alternatively be sources:
| Alternative | Mesh multi-project |
|---|---|
{{ source('stripe', 'payments') }} | {{ ref('core_platform', 'stg_payments') }} |
| Data comes from raw database tables | Data comes from another dbt project's public models |
Defined in sources.yml | Declared in dependencies.yml |
The upstream project has already staged and transformed the raw data. Your project builds on top of their public models, not their raw sources.
When multiple upstream projects have models with the same name (e.g. stg_customers in both core_platform and marketing_platform), you must use the two-argument ref():
Before writing new SQL:
ref() calls to see which upstream projects and models are already in useaccess: public models — only these are referenceable cross-projectref() must exactly match the name field in the upstream project's dbt_project.yml (case-sensitive)| Upstream model access | Can you ref() it cross-project? |
|---|---|
access: public | Yes |
access: protected (default) | No — only within the same project |
access: private | No — only within the same group |
If you need a model that isn't public, coordinate with the upstream team to widen its access.
Cross-project ref() and the projects: key in dependencies.yml are only available on dbt Cloud Enterprise or Enterprise+ plans. Before setting up any cross-project collaboration, verify plan eligibility:
dependencies.yml already has a projects: key and the project is actively using cross-project refs — Enterprise is already in place. Proceed.projects: to dependencies.yml or writing new two-argument ref() calls.If the user cannot confirm the plan level, or confirms they are on a plan below Enterprise, do not set up cross-project refs. Explain that this feature requires upgrading to Enterprise or Enterprise+ and suggest they use the intra-project governance features (groups, access modifiers, contracts) instead.
ref() SyntaxFor full cross-project setup details (dependencies.yml, prerequisites, orchestration), see references/cross-project-collaboration.md.
dbt Mesh includes four governance features. These work independently and can be adopted incrementally:
| Feature | Purpose | Key Config | Reference |
|---|---|---|---|
| Model Contracts | Guarantee column names, types, and constraints at build time | contract: {enforced: true} | references/model-contracts.md |
| Groups | Organize models by team/domain ownership | group: finance | references/groups-and-access.md |
| Access Modifiers | Control which models can ref yours | access: public / protected / private | references/groups-and-access.md |
| Model Versions | Manage breaking changes with migration windows | versions: with latest_version: and latest_version_pointer (v1.12+) | references/model-versions.md |
In model property YAML files, access, group, and contract are configs and must always be nested under the config: key — never placed as top-level model properties. Placing them at the top level may appear to work in dbt Core but causes parse errors in dbt's Fusion engine.
This applies to property YAML files only. In dbt_project.yml, use the + prefix for directory-level assignment (e.g. +group: finance, +access: private). In SQL files, use {{ config(access='public', group='finance') }}.
| Contracts | Data Tests | |
|---|---|---|
| When | Build-time (pre-flight) | Post-build (post-flight) |
| What | Column names, data types, constraints | Data quality, business rules |
| Failure | Model does not materialize | Model exists but test fails |
| Use for | Shape guarantees for downstream consumers | Content validation and anomaly detection |
Contracts are enforced before tests run. If a contract fails, the model is not built, and no tests execute.
Use a contract when:
access: public (especially if referenced cross-project)Do NOT add a contract when:
stg_*) — these are internal implementation details, not consumer-facing APIspivot(), unpivot(), or dynamically generate columns are poor candidates because the column list isn't fixed and the contract will break whenever the dynamic values changeIf the user asks for a contract on a model that matches the "do NOT add" criteria above, advise against it and explain why. Do not simply comply — the user may not realize the contract is inappropriate. Suggest alternatives (e.g., data tests for staging models, waiting for schema stability, or switching materialization for ephemeral models).
Version a model when:
Do NOT version a model:
latest_version doesAdding the new version and promoting it to latest_version are two separate deploys, separated by the migration window — never the same change. This is the single most common way a "safe" versioned change still breaks a dashboard.
External consumers (BI tools, reverse ETL, dashboards) read a physical relation by name — almost always the unsuffixed model_name (the latest-version pointer view, or the plain model when unversioned). That name resolves to whatever latest_version points at. So:
v2) with the new shape, but keep latest_version on the OLD version (v1). So external consumers keep reading the old columns by name, maintain a canonical (unsuffixed) relation that tracks the latest version — with latest_version still on v1, that canonical relation serves the old shape (mechanism table below). The new shape is available at model_v2 for consumers to migrate against; internal consumers you control can migrate early by pinning ref('model', v=2).latest_version to 2 and set deprecation_date on v1. The canonical relation then auto-re-points to the new shape — no relation rename needed.Keep the canonical relation serving the old shape — pick ONE mechanism (never two):
| dbt version | Default mechanism | Result |
|---|---|---|
| ≥ 1.12 / Fusion | built-in latest_version_pointer (currently beta) | model_v1, model_v2, pointer view model (3 relations) |
| ≤ 1.11 | create_latest_version_view() macro + project post-hook (dbt's officially recommended pattern) | model_v1, model_v2, canonical view model (3 relations) |
| either — fallback only | config.alias pinning the OLD version to the unsuffixed name | 2 relations; forces manual un-aliasing + rewiring at Deploy 2 |
Always prefer the version-appropriate pointer; reach for config.alias only when neither pointer is available or the user explicitly declines. Never combine a pointer/view with alias on the same name (collision error). See model-versions.md.
Bumping latest_version is itself the breaking release for unsuffixed consumers. If you introduce the new version as latest (or bump latest in the same change), the migration window is zero and the dashboard breaks immediately. The new version always starts as non-latest.
After Deploy 1, verify the window is actually open — the consumer's relation must still return the old columns:
A column does not exist error means latest_version was promoted too early and the consumer is already broken.
Best practice: Default new models to private and widen access only when needed. The default protected is permissive — be intentional.
| Mistake | Why It's Wrong | Fix |
|---|---|---|
Using single-argument ref() in multi-project setups | Ambiguous — dbt may not resolve to the intended project | Always use ref('project_name', 'model_name') for cross-project refs |
Using source() for upstream project data | In mesh, upstream data comes through public models, not raw sources | Use ref('upstream_project', 'model_name') instead |
Not reading dependencies.yml first | You won't know which upstream projects exist or what they're called | Always read dependencies.yml before writing cross-project SQL |
Making all models public | Exposes internal implementation details cross-project | Only mark models public that are intentional APIs for other teams |
| Skipping contracts on public models | Downstream consumers can break silently when schema changes | Always enforce contracts on access: public models |
| Versioning for non-breaking changes | Creates unnecessary maintenance burden and warehouse cost | Only version for breaking changes (column removal, type change, rename) |
Introducing the new version as latest_version (or bumping latest in the same change) | The unsuffixed/pointer relation immediately serves the new shape, breaking BI tools and reverse ETL that read it by name — the migration window is zero | Introduce the new version with latest_version still on the OLD version; bump only after consumers confirm migration |
Reaching for config.alias to hold the unsuffixed name when a version-appropriate pointer is available | Collapses to 2 relations and forces manual un-aliasing + rewiring at promotion instead of an automatic re-point | Default to latest_version_pointer (≥1.12) or the create_latest_version_view post-hook (≤1.11); use alias only as a fallback |
Forgetting dependencies.yml | Cross-project refs fail without declaring the upstream project | Add upstream project to dependencies.yml before using two-argument ref() |
| Referencing non-public models cross-project | Only public models are available to other projects | Set access: public on models intended for cross-project consumption |
Placing access, group, or contract as top-level model properties in YAML | Breaks Fusion engine parsing; top-level placement is not valid config | Always nest under config: — e.g. config: { access: public } |
| Adding contracts to staging models | Staging models are internal — contracts add friction without protecting external consumers | Advise against it; suggest data tests instead |
| Adding contracts to models with dynamic/pivot columns | Column list changes with data, breaking the contract | Advise against it; explain why the column list isn't fixed |
| Adding contracts without establishing external consumers | Contracts protect a schema boundary — no consumers means no boundary to protect | Ask who depends on this model before adding a contract |
Making a model private that is already referenced outside its group | Existing refs break with a DbtReferenceError | Widen access to protected or refactor callers into the same group first |
| Setting up cross-project refs without confirming dbt Cloud Enterprise | Cross-project ref() is unavailable on lower plan tiers | Confirm the plan level before adding projects: to dependencies.yml or writing two-argument ref() calls |
Adding dependencies.yml without a successful upstream production job | dbt Cloud resolves cross-project refs via the upstream manifest.json — no job run means no manifest | Run at least one successful production deployment in the upstream project first |
-- Reference an upstream model (latest version)
select * from {{ ref('upstream_project', 'model_name') }}
-- Reference a specific version
select * from {{ ref('upstream_project', 'model_name', v=2) }}# ✅ CORRECT — all governance configs under `config:`
models:
- name: fct_orders
config:
group: finance
access: public
contract:
enforced: true
columns:
- name: order_id
data_type: int
# ❌ WRONG — governance configs as top-level properties (breaks Fusion)
models:
- name: fct_orders
access: public # WRONG — not under config:
group: finance # WRONG — not under config:
contract: # WRONG — not under config:
enforced: true
columns:
- name: order_id
data_type: int1. Groups & Access → 2. Contracts → 3. Versions → 4. Cross-Project Refs
(organize teams) (lock shapes) (manage changes) (split projects)dbt show --inline "select <old_col> from {{ target.schema }}.<unsuffixed_relation>"Is it referenced cross-project?
└─ Yes → public (with contract recommended)
└─ No
Is it referenced outside its group?
└─ Yes → protected (default)
└─ No
Is it internal to a small team?
└─ Yes → private
└─ No → protected (default)