dbt Models skill for Codex
Incremental models that stay correct on run 100. Codex structures dbt projects into staging, intermediate and mart layers with one stated grain per model, picks materializations by cost rather than habit, and writes incremental models with lookback windows and deduplicated unique keys. It previews with dbt show, proves parity between dev and prod, and asks before any full refresh or production build.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- dbt-models Installs to ~/.agents/skills/dbt-models/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install dbt-models The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project dbt-models Installs to .agents/skills/dbt-models/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/dbt-models.zipunzip -o dbt-models.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) dbt-models PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $dbt-models, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and refactors dbt models - staging, intermediate and mart layers, sources and ref(), materialization choice, incremental models with merge, delete+insert and microbatch strategies, Jinja macros, packages, and slim CI with state:modified and defer. Use when the user adds or changes SQL in a dbt project, asks to make a model incremental, sees duplicates or missing rows after an incremental run, writes a macro, restructures a messy models folder, or wants faster dbt builds in CI.
Use it when
- Adding or refactoring SQL models in a dbt project
- A full rebuild is too slow and a model should become incremental
- Incremental runs produce duplicates or miss late-arriving rows
- Repeated SQL should become a macro or a package helper
Use something else when
- The task is writing dbt tests, contracts, unit tests or freshness checks: use dbt-testing
- You are designing facts, dimensions and slowly changing dimensions: use dimensional-modeling
- The question is warehouse cost and clustering rather than model code: use snowflake-sql
How to check the result
dbt build passes for the model and its children in dev, and row counts plus key totals match production or the previous version.
The mistake it prevents
Filtering incremental runs with only updated_at > max(updated_at), which drops late-arriving rows forever.
Done well means: every model has one clear grain stated in its YAML, raw tables are touched
only through source() in staging models, incremental logic is correct on both the first
run and the hundredth, and the change was built and tested against a dev target before
anyone suggests production.
Before changing anything
- Read
AGENTS.md,dbt_project.yml,packages.ymland the adapter in use (profiles.ymltarget type: snowflake, bigquery, postgres, databricks, duckdb…). Incremental strategies and SQL dialect depend on the adapter. dbt --version. This skill targets dbt Core 1.8 to 1.10 and the same YAML in the Fusion engine. Older projects may usetests:instead ofdata_tests:.- Check the target:
dbt debugand confirm which schemadevwrites to. Never rundbt run/buildagainst a production target without explicit user confirmation. - Learn the conventions already in use (
dbt ls --resource-type model | head, folder names, prefixes). Follow them over the defaults below.
Layering
| Layer | Prefix | Materialization | Rules |
|---|---|---|---|
| Staging | stg_<source>__<entity> | view | One per source table. Rename, cast, light cleaning. Only place source() appears |
| Intermediate | int_<entity>_<verb> | ephemeral or view | Joins, pivots, re-graining. Not exposed to BI |
| Marts | fct_<process>, dim_<entity> | table or incremental | Business grain, documented, contract-enforced if consumed downstream |
A model reads from the layer below or the same layer, never from a later one. If two marts share logic, extract an intermediate model; do not copy SQL.
Staging model shape
-- models/staging/shop/stg_shop__orders.sql
with source as (
select * from {{ source('shop', 'orders') }}
),
renamed as (
select
id::varchar as order_id,
customer_id::varchar as customer_id,
lower(trim(status)) as status,
amount_cents / 100.0 as amount,
created_at::timestamp as created_at,
updated_at::timestamp as updated_at
from source
where _fivetran_deleted is not true
)
select * from renamed
# models/staging/shop/_shop__sources.yml
sources:
- name: shop
schema: raw_shop
tables:
- name: orders
loaded_at_field: _loaded_at
Choosing a materialization
| Situation | Choose |
|---|---|
| Cheap transform, small data, always fresh | view |
| Queried often by BI, rebuild under a few minutes | table |
| Large append-mostly fact, full rebuild too slow or costly | incremental |
| Helper CTE reused by a couple of models | ephemeral (debugging is harder; prefer view when in doubt) |
| Time-series fact with reliable event time, needs easy backfills | incremental with microbatch |
Start with table. Move to incremental only when build time or cost demands it; incremental
adds a class of correctness bugs.
Incremental models
{{ config(
materialized='incremental',
unique_key='order_id',
incremental_strategy='merge',
on_schema_change='append_new_columns'
) }}
select order_id, customer_id, status, amount, created_at, updated_at
from {{ ref('stg_shop__orders') }}
{% if is_incremental() %}
-- 3-day lookback catches late-arriving updates; merge on unique_key dedupes them
where updated_at > (select dateadd(day, -3, max(updated_at)) from {{ this }})
{% endif %}
| Strategy | Use when | Watch out |
|---|---|---|
merge | Rows update in place; you have a reliable unique_key | Duplicate keys in the new batch make the merge fail or duplicate rows |
delete+insert | Adapter merge is slow, or replacing whole slices | Deletes by key first; still needs a unique key |
insert_overwrite | BigQuery/Spark partition replacement | Replaces whole partitions; filter must cover full partitions |
append | Immutable events, duplicates acceptable or removed later | Re-running a window duplicates rows |
microbatch (1.9+) | Event-time data, backfills by date range | Needs event_time on the model and upstream refs |
{{ config(
materialized='incremental',
incremental_strategy='microbatch',
event_time='created_at',
batch_size='day',
begin='2024-01-01',
lookback=2
) }}
select * from {{ ref('stg_events') }} -- dbt filters each batch by event_time automatically
Backfill a range: dbt run -s fct_events --event-time-start 2025-09-01 --event-time-end 2025-09-08.
Incremental hard rules:
- The
unique_keymust actually be unique in each batch. Dedupe in the model withqualify row_number() over (partition by order_id order by updated_at desc) = 1. - Use a lookback window;
> max(updated_at)alone loses late rows. - Test both paths:
dbt build -s model --full-refreshand then a normaldbt build -s model. on_schema_changedefaults toignore: new columns silently never appear. Pickappend_new_columnsorfaildeliberately.
Macros
-- macros/cents_to_currency.sql
{% macro cents_to_currency(column, scale=2) -%}
round({{ column }} / 100.0, {{ scale }})
{%- endmacro %}
- Write a macro once logic repeats in three or more models, not before.
- Guard queries that run at parse time:
{% if execute %}{% set rows = run_query(sql) %}{% endif %}. - Cross-database logic: use
adapter.dispatchand checkdbt_utilsfirst (dbt_utils.star,dbt_utils.generate_surrogate_key,dbt_utils.date_spine). - Overriding
generate_schema_namechanges where every model lands; review it carefully. dbt depsdownloads packages and needs network; in a sandbox ask for approval.
Day-to-day commands
dbt parse # fast syntax and graph check
dbt compile -s fct_orders # inspect rendered SQL in target/compiled
dbt show -s fct_orders --limit 20 # preview results without materializing
dbt build -s +fct_orders # run and test the model and its parents
dbt build -s fct_orders --empty # schema-only dry run (1.8+), zero rows
dbt ls -s state:modified+ --state prod-artifacts/
Slim CI: keep production manifest.json as an artifact, then
dbt build -s state:modified+ --defer --state prod-artifacts/ builds only changed models and
reads unchanged parents from production.
Gotchas
- Hard-coding
analytics.raw.ordersinstead ofsource()orref()breaks lineage, environments and slim CI. select *in marts lets upstream column additions leak into BI; list columns explicitly.- Jinja renders before SQL runs;
{{ this }}in a non-incremental first run refers to a table that does not exist yet, which is why it sits insideis_incremental(). - Changing an incremental model’s logic does not rewrite history. Say so and propose a
--full-refresh(costly on big tables: confirm with the user first). - Ephemeral models never exist in the warehouse, so nobody can query them while debugging, and they inflate compiled SQL; switch to a view when investigating.
Verification
dbt build -s <model>+passes in dev, including downstream tests.- Compare against the old version: row counts and key metric totals using
dbt show --inline "select count(*), sum(amount) from {{ ref('fct_orders') }}"in dev vs prod. - For incremental changes: full-refresh result equals full-refresh-then-incremental result on
the same data (no duplicates on
unique_key). - Data tests for the new grain exist; hand off detail to
$dbt-testing.
PR description template
**Models:** fct_orders (table -> incremental merge, 3-day lookback), new int_orders_deduped
**Grain:** one row per order_id
**Built in dev:** dbt build -s +fct_orders+ (42 models, 118 tests passed)
**Parity:** dev vs prod row count 18,442,019 = 18,442,019; revenue diff 0.00
**Deploy note:** requires one --full-refresh of fct_orders after merge (approx 6 min on XL) What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "dbt Models"
short_description: "Incremental models that stay correct on run 100"
default_prompt: "$dbt-models Make fct_orders incremental without creating duplicates on late updates" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.