Labsco
dbt-labs logo

working-with-dbt-mesh

605

by dbt-labs · part of dbt-labs/dbt-agent-skills

Implements dbt Mesh governance features (model contracts, access modifiers, groups, versioning) and multi-project collaboration with cross-project refs. Use…

🔥🔥🔥✓ VerifiedFreeQuick setup
🧩 One of 7 skills in the dbt-labs/dbt-agent-skills package — works on its own, and pairs well with its siblings.

This is the playbook your agent receives when the skill activates — you don't need to read it to use the skill, but it's here to audit before installing.

by dbt-labs

Implements dbt Mesh governance features (model contracts, access modifiers, groups, versioning) and multi-project collaboration with cross-project refs. Use… npx skills add https://github.com/dbt-labs/dbt-agent-skills --skill working-with-dbt-mesh Download ZIPGitHub605

Working with dbt Mesh

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.

When to Use

  • Making a potentially breaking change to a model — renaming, removing, or retyping a column — especially when other models, exposures, or BI tools depend on it. Assess the blast radius before changing it, and reach for model versions rather than editing in place.

  • Versioning a model (versions:, latest_version, latest_version_pointer, deprecation_date) — this applies in a single project, not just multi-project setups

  • Working in a dbt project that references models from other dbt projects

  • Resolving ambiguity when multiple upstream projects have similarly-named models (e.g. multiple stg_ models)

  • Adding model contracts, access modifiers, or groups

  • Setting up cross-project references with dependencies.yml

  • Splitting a monolithic dbt project into multiple mesh projects

Do NOT use for:

  • General model building or debugging (use the using-dbt-for-analytics-engineering skill)

  • Unit testing models (use the adding-dbt-unit-test skill)

  • Semantic layer work (use the building-dbt-semantic-layer skill)

Cross-Project Refs Require dbt Cloud Enterprise

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:

  • If dependencies.yml already has a projects: key and the project is actively using cross-project refs — Enterprise is already in place. Proceed.

  • Otherwise — ask the user to confirm they are on dbt Cloud Enterprise or Enterprise+ before adding 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.

Cross-Project ref() Syntax

-- 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) }}

For full cross-project setup details (dependencies.yml, prerequisites, orchestration), see references/cross-project-collaboration.md.

Governance Features

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

YAML placement rule

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.

# ✅ 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: int

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') }}.

Adoption order

1. Groups & Access → 2. Contracts → 3. Versions → 4. Cross-Project Refs
 (organize teams) (lock shapes) (manage changes) (split projects)
  • Groups & Access — no schema changes needed, start here

  • Contracts — require declaring every column and data type in YAML

  • Versions — needed when a model must introduce a breaking change that consumers need time to migrate to (an enforced contract is recommended alongside, but not required)

  • Cross-Project Refs — require dbt Cloud Enterprise or Enterprise+ and a successful upstream production job. Do not set up cross-project refs if you cannot confirm the plan level is Enterprise or higher.

Contracts vs. Tests

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.

Decision Framework

Should this model have a contract?

Use a contract when:

  • The model is access: public (especially if referenced cross-project)

  • Other teams depend on this model's schema stability

  • The model feeds an exposure (dashboard, ML pipeline, reverse ETL)

  • External consumers (other dbt projects, BI dashboards, reverse ETL) query the table directly and would break from column renames or removals

Do NOT add a contract when:

  • Staging models (stg_*) — these are internal implementation details, not consumer-facing APIs

  • The model is still evolving — if the user says they are iterating on the design, advise waiting until the schema stabilizes

  • No external consumers exist — in a single-project setup with no cross-project refs, no BI tools depending on the schema, and no exposures, contracts add maintenance overhead without benefit. Ask about consumers before recommending contracts.

  • Dynamic/pivot columns — models that use pivot(), unpivot(), or dynamically generate columns are poor candidates because the column list isn't fixed and the contract will break whenever the dynamic values change

  • Ephemeral models — contracts are not supported on ephemeral materializations

If 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).

Should this model be versioned?

Version a model when:

  • You need to make a breaking change (column removal, rename, or type change) to a model that consumers depend on — whether or not it has an enforced contract. A contract makes the break a build-time error; without one the break is silent and ships straight to downstream models and dashboards, so a migration window matters even more. Don't let "there's no contract" talk you out of versioning a breaking change.

  • Consumers need a migration window before the old shape goes away — including consumers you can't update atomically: other teams' models, exposures, dashboards, and BI tools that read the table directly.

Do NOT version a model:

  • For additive changes (new columns) — these are non-breaking

  • For bug fixes — fix in place

  • Preemptively "just in case" — version only when a breaking change is actually needed

  • Only skip versioning if nothing reads this model outside dbt — no exposures, no BI tools, no other projects. If even one exists, version it.

Versioning alone does NOT create the migration window — latest_version does

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

  • Deploy 1 — introduce: add the new version (e.g. 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).

  • Deploy 2 — promote (later): only after every external consumer confirms migration, bump 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:

dbt show --inline "select from {{ target.schema }}. "

A column does not exist error means latest_version was promoted too early and the consumer is already broken.

What access level should this model have?

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)

Best practice: Default new models to private and widen access only when needed. The default protected is permissive — be intentional.