Field guide · 4~8 min
Add the YAML
The .yml beside a model is where what you know about the data becomes part of the pipeline — read by the docs site, by Snowflake, by CI, and by tests whenever the model is selected to build.
What this file actually does
The SQL file describes the rows to build. The YAML file records everything else worth knowing about them — and unlike a comment or a wiki page, what you write here is used. The descriptions publish to the dbt docs site and become column comments on the Snowflake object itself, so they appear on hover in Snowsight and in every tool that reads warehouse metadata. The owner block tells CI — and, later, a colleague at 2 am — who understands this model. And the tests compile into queries that run whenever the model is selected: in your local build, in conditional Snowflake PR validation and in the relevant production schedules.
That last audience is the one that changes how you write the file. Documentation elsewhere goes stale the day after it is written; this file is executed. Writing it is less like documenting and more like setting the terms the data must keep meeting after you have moved on.
Generate the skeleton
dbt run -s stg_reference_opening_hours
dbt run-operation generate_model_yaml --args '{"model_names": ["stg_reference_opening_hours"], "upstream_descriptions": true}'The generator reads the built model, emits the full column list, and copies descriptions from upstream where columns pass through unchanged — which is why the model is built first. Save the output beside the SQL with the same name.
Be clear about what you just got. The generator knows what the columns are; it cannot know what they mean. It does not know the grain, which columns carry the model's contract, what a null represents, or which source quirks the next analyst needs warning about. Everything that follows is the part only you can write.
Descriptions that earn their space
A description that restates the column name is worse than none — it occupies the place where help would go. Compare:
# adds nothing — the name already said this
- name: day_of_week
description: The day of the week
# adds what the name cannot say
- name: day_of_week
description: ISO day of week, 1 = Monday. The source uses 0 = Sunday; converted here.The useful material is always the same few things: units and coding (1 = Monday; mmol/mol; lowercase ODS code), what null means (never recorded? not applicable? awaiting a feed?), and anything the source does that would surprise someone. The test for a good description: would it answer the question a colleague hovers over this column with, six months from now, with you on leave?
The model's own description carries one obligation — state the grain. “Site opening hours, one row per site per weekday” tells a reader whether this table can answer their question before they open the SQL.
The tests: grain first, then contract
models:
- name: stg_reference_opening_hours
description: Site opening hours, one row per site per weekday
config:
meta:
owner:
name: Your Name
data_tests:
- dbt_utils.unique_combination_of_columns:
arguments:
combination_of_columns: [site_code, day_of_week]
columns:
- name: site_code
description: Standardised site identifier
data_tests: [not_null]Every test compiles to a query that hunts for rows breaking the rule — zero rows back means pass. That framing helps you choose: a test is worth writing when the rows it would find represent something genuinely wrong, worth waking the pipeline up over.
The grain test comes first because it catches the most damaging silent failure — a join, now or years from now, that fans the table out and multiplies every downstream count. Take the grain sentence from the description and assert it: unique for a single-column grain, unique_combination_of_columns when the grain is a combination. Then not_null — but only on columns where a null would genuinely break the contract, usually the grain columns themselves. A not_null on a column that is legitimately sometimes empty fails on the first real null, and a test that fails on valid data teaches everyone to ignore failures. If null is meaningful, the right place for that fact is the description, not a test.