A curated collection of Agent Skills for working with dbt, to help AI agents understand and execute dbt workflows more effectively.
70
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
Model versioning lets you introduce breaking changes to a contracted model while giving downstream consumers a migration window. Multiple versions coexist in the same codebase and data environment simultaneously — similar to API versioning.
Version a model only for breaking changes to a contract:
Do NOT version for:
models:
- name: fct_orders
latest_version: 1
config:
access: public
contract:
enforced: true
columns:
- name: order_id
data_type: varchar
- name: customer_id
data_type: varchar
- name: order_total
data_type: number
- name: tax_paid
data_type: number
- name: ordered_at
data_type: timestamp_ntz
versions:
- v: 1
config:
alias: fct_orders # Without this, resolves to fct_orders_v1When you need to rename order_total to order_amount and change a data type:
models:
- name: fct_orders
latest_version: 1 # Keep pointing to v1 until consumers migrate
config:
access: public
contract:
enforced: true
columns:
- name: order_id
data_type: varchar
- name: customer_id
data_type: varchar
- name: order_total
data_type: number
- name: tax_paid
data_type: number
- name: ordered_at
data_type: timestamp_ntz
versions:
- v: 1
config:
alias: fct_orders
- v: 2
columns:
- include: all
exclude: [order_total] # Remove old column name
- name: order_amount # Add new column name
data_type: number
- name: ordered_at # Change data type
data_type: dateWithin a version's columns key:
include: all — inherit all columns from the parent model definitionexclude: [col1, col2] — remove specific columns from the inherited setEach version needs its own SQL file:
| Version | SQL File |
|---|---|
| v1 (latest) | fct_orders.sql or fct_orders_v1.sql |
| v2 (prerelease) | fct_orders_v2.sql |
| Old version | fct_orders_v1.sql |
Use defined_in to specify a custom file name:
versions:
- v: 1
defined_in: fct_orders_v1 # Points to fct_orders_v1.sql
config:
alias: fct_orders
- v: 2| Version State | Default Relation Name |
|---|---|
| Latest version | fct_orders_v{N} (or fct_orders via alias) |
| Non-latest version | fct_orders_v{N} |
| Latest version pointer (opt-in) | fct_orders — view resolving to the latest version (built-in latest_version_pointer on v1.12+, currently beta; create_latest_version_view post-hook on ≤1.11) |
To keep consumers querying an unsuffixed name (e.g. fct_orders) that always resolves to the latest version, use a latest version pointer — see Latest version pointer below. This is the recommended default for breaking-change migrations, in both version ranges. (config.alias is a different tool: it pins one specific version to a fixed relation name, e.g. anchoring a non-latest version at the unsuffixed name during a migration. It is not a moving "latest" pointer, so it's a manual fallback only — reach for it only when neither pointer mechanism is available or the user explicitly declines.)
A latest version pointer is an unsuffixed relation (e.g. fct_orders) that always resolves to the model's latest version (e.g. fct_orders_v2). It gives consumers querying outside dbt the same "latest unless pinned" behavior that ref() gives inside dbt: no suffix → latest; _vN suffix → that specific version. How you create it depends on your dbt version.
⚠️ The pointer always tracks
latest_version— it does NOT shield unsuffixed consumers from a breaking shape change. Enabling the pointer re-points every unsuffixed consumer to the new shape the instant you bumplatest_version. It protects against version suffixes changing, not against the shape changing. During a migration, keeplatest_versionon the old version so the pointer keeps serving the old shape; promote to the new version only after consumers have migrated. See "Versioning alone does NOT create the migration window" in SKILL.md.
Three objects from two files: a versioned model defined by two SQL files — fct_orders_v1.sql and fct_orders_v2.sql (latest) — produces three database relations once a pointer exists: fct_orders_v1, fct_orders_v2, and the pointer relation fct_orders.
latest_version_pointer⚠️ Lifecycle:
latest_version_pointeris currentlybetain dbt's documentation. It is the recommended mechanism on v1.12+, but confirm it's available and behaving as expected for the user's exact version before relying on it in production.
On v1.12+ (and the Fusion engine), use the built-in latest_version_pointer config — there is no reason to hand-roll a post-hook. After the latest version (the one whose v matches latest_version:) materializes successfully, dbt automatically creates the pointer view. The feature is opt-in (default off).
The config is named "pointer" rather than "view" because future adapter-specific optimizations may use a different relation type. In v1.12 the implementation is always a view.
Enable per model:
models:
- name: fct_orders
latest_version: 2
config:
latest_version_pointer:
enabled: true
versions:
- v: 1
- v: 2Enable project-wide one of two ways (use whichever fits — they are alternatives, not both required):
# dbt_project.yml — Option A: turn it on for the whole project
flags:
latest_version_pointer_enabled_by_default: true# dbt_project.yml — Option B: turn it on for a directory of models (overridable per model)
models:
my_project:
marts:
+latest_version_pointer:
enabled: trueCustomize the pointer name. By default the pointer uses the unsuffixed model name (fct_orders). Either set alias under latest_version_pointer for a single model, or override the dispatched generate_latest_version_pointer_alias macro for a project-wide convention (the alias sub-field is passed in as custom_alias_name):
models:
- name: fct_orders
# ... latest_version: and versions: omitted for brevity (see the full example above)
config:
latest_version_pointer:
enabled: true
alias: fct_orders_current-- macros/generate_latest_version_pointer_alias.sql
-- Override example. The default implementation returns node.name (the unsuffixed name);
-- this version appends a "_latest" suffix instead.
{% macro generate_latest_version_pointer_alias(custom_alias_name=none, node=none) %}
{%- if custom_alias_name -%}
{{ custom_alias_name | trim }}
{%- else -%}
{{ node.name ~ "_latest" }}
{%- endif -%}
{% endmacro %}The pointer view is created only when the latest version materializes successfully. If the latest version's own alias already equals the pointer name, dbt raises a clear collision error — pick a distinct pointer alias, or rely on the default unsuffixed name.
create_latest_version_view post-hook (dbt's recommended pattern)On ≤1.11 there is no built-in pointer. dbt's own docs recommend this pattern ("Configuring database location with alias" in the model-versions docs): create the canonical-name relation yourself with a custom macro run as a post-hook. Prefer this over config.alias — it gives consumers the same "no suffix → latest, _vN → pinned" behavior outside dbt that ref() gives inside dbt. The macro is a no-op except on the latest version, where it creates (or replaces) a view at the unsuffixed name pointing to the current relation. Note this is wired as a project-wide post-hook, so it runs (as a no-op) on every model, not just the versioned one:
-- macros/create_latest_version_view.sql
{% macro create_latest_version_view() %}
-- applied as a project-wide post-hook, this macro runs on every model, but the CREATE VIEW
-- below executes only for the latest version of a versioned model; otherwise it's a no-op
{% if model.get('version') and model.get('version') == model.get('latest_version') %}
{% set new_relation = this.incorporate(path={"identifier": model['name']}) %}
{% set existing_relation = load_relation(new_relation) %}
{% if existing_relation and not existing_relation.is_view %}
{{ drop_relation_if_exists(existing_relation) }}
{% endif %}
{% set create_view_sql -%}
-- this syntax may vary by data platform
create or replace view {{ new_relation }} as select * from {{ this }}
{%- endset %}
{% do log("Creating view " ~ new_relation ~ " pointing to " ~ this, info = true) if execute %}
{{ return(create_view_sql) }}
{% else %}
-- no-op
select 1 as id
{% endif %}
{% endmacro %}# dbt_project.yml
models:
+post-hook:
- "{{ create_latest_version_view() }}"-- Reference the latest version (resolves to latest_version)
select * from {{ ref('fct_orders') }}
-- Reference a specific version explicitly
select * from {{ ref('fct_orders', v=2) }}
-- Cross-project reference with version
select * from {{ ref('upstream_project', 'fct_orders', v=1) }}Unpinned ref() calls resolve to latest_version. When you bump latest_version, all unpinned refs automatically point to the new version.
# Run all versions of a model
dbt run --select fct_orders
# Run a specific version
dbt run --select fct_orders_v2
# Run only the latest version
dbt run -s fct_orders,version:latestIntroducing the new version and promoting it to latest_version are two separate deploys, separated by the migration window — never the same change. The new version always starts as non-latest.
columns changes but keep latest_version pointing to the old version — do NOT make the new version latest yet. This is what keeps the unsuffixed relation (and the pointer view) serving the old shape so external consumers don't break.
latest_version_pointer; on ≤1.11 wire the create_latest_version_view post-hook. With latest_version still on the old version, the pointer serves the old shape now and auto-re-points when you bump it in step 6 — no relation rename. Use config.alias on the old version only as a fallback (manual rewiring required at promotion).dbt show --inline "select <old_col> from {{ target.schema }}.<unsuffixed_relation>"column does not exist error means latest_version was promoted too early and the consumer is already broken.ref() calls to the new version (or pin to the old one)latest_version to the new version once consumers have migrated — this is a breaking release for any unsuffixed consumer that hasn't migrated, so confirm migration firstversions:
- v: 1
deprecation_date: 2025-06-01 00:00:00.00+00:00By default, unit tests run against all versions of a model. To target a specific version:
unit_tests:
- name: test_order_amount_calculation
model: fct_orders
versions:
include:
- 2 # Only test v2| Mistake | Fix |
|---|---|
| Versioning for additive changes | New columns are non-breaking — just add them to the contract |
Bumping latest_version before consumers migrate | Keep latest_version on the old version until migration is complete |
| Leaving no pointer to the latest version | Consumers querying the unsuffixed name break when you bump latest_version. ≥1.12: enable latest_version_pointer (currently beta); ≤1.11: use the create_latest_version_view post-hook (see Latest Version Pointer). config.alias pins one version to a name — it is not a moving pointer. |
Using config.alias as the default canonical-name mechanism | It collapses to 2 relations and forces manual un-aliasing + rewiring when you bump latest_version. Default to the version-appropriate pointer (latest_version_pointer ≥1.12, create_latest_version_view post-hook ≤1.11); use config.alias only as a fallback. |
| Not creating a SQL file for the new version | Each version needs its own SQL file (or a defined_in reference) |
| Removing old version too quickly | Set a deprecation date and give consumers a migration window |
.changes
.claude
skills
auditing-skills
.claude-plugin
.cursor-plugin
.github
ISSUE_TEMPLATE
scripts
skills
dbt
.claude-plugin
.cursor-plugin
skills
adding-dbt-unit-test
references
answering-natural-language-questions-with-dbt
building-dbt-semantic-layer
configuring-dbt-mcp-server
fetching-dbt-docs
scripts
maintaining-dbt-documentation
running-dbt-commands
troubleshooting-dbt-job-errors
references
using-dbt-for-analytics-engineering
using-dbt-state
working-with-dbt-mesh
dbt-extras
.claude-plugin
skills
creating-mermaid-dbt-dag
dbt-migration
.claude-plugin
skills
migrating-dbt-core-to-fusion
migrating-dbt-project-across-platforms
upgrading-dbt-core
references
scripts