Skip to main content
skill CX Codex

working-with-dbt-mesh — Claude Code skill: what it does

Evaluates blast radius when modifying dbt models and sets up model versioning, contracts, and cross-project dbt Mesh references.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add dbt-labs/dbt-agent-skills --skill working-with-dbt-mesh -a claude-code
Open on GitHub

What working-with-dbt-mesh does

The skill handles breaking changes in dbt models that feed downstream dashboards. It assesses blast radius before schema changes. Instead of editing in place, it implements model versioning using versions and deprecation dates.

It enforces correct configuration syntax. Model settings like access, group, and contract must reside under the config: block in YAML.

Cross-project references require dbt Cloud Enterprise. Without an Enterprise plan, the skill avoids cross-project ref() calls. It directs work toward single-project contracts and groups instead.

What it helps with

  • assess blast radius before renaming or removing dbt model columns
  • configure model versions with migration windows and deprecation dates
  • enforce schema constraints with contract: {enforced: true}
  • configure two-argument ref() calls for cross-project dependencies
  • resolve model name collisions across upstream dbt projects

Example prompts for the agent

  • Check the blast radius before I drop the customer_id column from orders
  • Add versioning to stg_payments with a deprecation date for v1
  • Set up a cross-project ref to core_platform.stg_customers in dependencies.yml

How to install working-with-dbt-mesh

1Skills CLI (Vercel)

npx skills add dbt-labs/dbt-agent-skills --skill working-with-dbt-mesh -a claude-code

2Manually: put the files in place

~/.claude/skills/working-with-dbt-mesh/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/working-with-dbt-mesh/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about working-with-dbt-mesh

How to install working-with-dbt-mesh in Claude Code?

Run npx skills add dbt-labs/dbt-agent-skills --skill working-with-dbt-mesh -a claude-code.

What does the working-with-dbt-mesh skill do?

It guides breaking changes across dbt models, helping developers configure model versions, access rules, data contracts, and cross-project references.

Do cross-project references require dbt Cloud Enterprise?

Yes, multi-project refs and the projects: configuration in dependencies.yml require dbt Cloud Enterprise or Enterprise+. Without this plan, the skill restricts governance to single-project contracts and groups.

Source text

SKILL.md · Apache-2.0
---
name: working-with-dbt-mesh
description: 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.
user-invocable: false
metadata:
  author: dbt-labs
---

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

## First: Orient Yourself in a Multi-Project Setup

Before writing or modifying any SQL in a project that uses dbt Mesh, follow these steps:

### 1. Read `dependencies.yml`

This file at the project root tells you which upstream projects exist:

```yaml
# dependencies.yml
projects:
  - name: core_platform
  - name: marketing_platform
```

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()`.

### 2. Understand how upstream data gets into this project

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.

### 3. Disambiguate similarly-named models

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()`:

```sql
-- Correct: explicit project name, no ambiguity
select * from {{ ref('core_platform', 'stg_customers') }}
select * from {{ ref('marketing_platform', 'stg_customers') }}

-- WRONG: dbt cannot determine which project's stg_customers you mean
select * from {{ ref('stg_customers') }}
```

### 4. Check existing patterns in the codebase

Before writing new SQL:
- Search for existing two-argument `ref()` calls to see which upstream projects and models are already in use
- Look at the upstream project's YAML for `access: public` models — only these are referenceable cross-project
- The first argument of `ref()` must exactly match the `name` field in the upstream project's `dbt_project.yml` (case-sensitive)

### 5. Know what you can and cannot reference

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

1. **If `dependencies.yml` already has a `projects:` key and the project is actively using cross-project refs** — Enterprise is already in place. Proceed.
2. **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

```sql
-- 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](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](references/model-contracts.md) |
| **Groups** | Organize models by team/domain ownership | `group: finance` | [references/groups-and-access.md](references/groups-and-access.md) |
| **Access Modifiers** | Control which models can `ref` yours | `access: public / protected / private` | [references/groups-and-access.md](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](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.

```yaml
# ✅ 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](references/model-versions.md#latest-version-pointer).

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

```bash
dbt show --inline "select <old_col> from {{ target.schema }}.<unsuffixed_relation>"
```

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.

## Common Mistakes

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

© dbt-labs · Apache-2.0 · dbt-labs/dbt-agent-skills

Similar skills

All skills for Data and databases →

From the same set13

troubleshooting-dbt-job-errors Walks an agent through a failed dbt Cloud job: run logs, git history, data checks. Useful when the error message says little or the failure comes and goes. CX Codex +1 1,000installs building-dbt-semantic-layer Guides AI agents in creating and modifying dbt Semantic Layer components, including metrics, entities, and time spines for MetricFlow across dbt Core versions. CX Codex +1 1,000installs configuring-dbt-mcp-server This skill writes the JSON config for a dbt MCP server, sorts out authentication, and checks that the connection actually works. CX Codex +1 1,000installs answering-natural-language-questions-with-dbt Answers business questions about metrics and data: checks the dbt Semantic Layer first, then writes and runs SQL. Useful when you want numbers, not model work. CX Codex +1 1,000installs using-dbt-state A skill that teaches the agent how dbt State decides to skip, clone or rebuild a node. Useful when models rebuild for no clear reason. CX Codex +1 554installs creating-mermaid-dbt-dag Turns dbt model lineage into a Mermaid flowchart with parents, children and a colour legend by resource type. Handy when the graph has to live in markdown. CX Codex +1 527installs migrating-dbt-project-across-platforms Guides dbt project migrations across data platforms using dbt Fusion real-time compilation. Catches SQL dialect issues early and verifies logic using unit tests. CX Codex +1 526installs maintaining-dbt-documentation This skill finds dbt models and declared columns without descriptions, then drafts YAML entries that follow the project’s conventions. Use it to check coverage or catch documentation gaps after SQL changes. CX Codex +1 328installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor