The Ticket Nobody Files
The enum problem is the one everybody recognizes, and it is not the whole of what changes. Once meaning is governed once and published back into dbt, a set of small recurring chores stop happening — and one of them has been costing time every month without ever being on a roadmap.
The Ticket Nobody Files
The enum problem is the one everybody recognizes, and it is not the whole of what changes. Once meaning is governed once and published back into dbt, a set of small recurring chores stop happening — and one of them has been costing time every month without ever being on a roadmap.
The premise is that dbt is an export target, not only an import source. You post manifest.json,
models and columns and tests become governed types, elements and taxonomies, and the governed
meaning publishes back out as dbt model property files with enforced contracts. Below is an ordinary
week with those chores removed, including the one part of it that costs you a morning up front.
Monday: the first generate, and the file you have to clear
The import alone gives you an estate description: what exists, what depends on what, which columns carry which native types. Useful, not transformative. The transformation starts when a human writes a definition on order_status, promotes its values into a governed vocabulary, and calls generate. What comes back is a set of files, one per model, each named for the path that model already lives at. models/staging/stg_orders.sql gets models/staging/stg_orders.yml. That colocation is not cosmetic; a single repo-wide contracts file is unreviewable past a few dozen models, and that is why the first version of this generator was replaced. If your repo organizes properties per directory instead, ask for the folder layout and get one _coremodels__models.yml per model folder.
Here is the honest friction. dbt refuses two property blocks for one model, and most of your models already have a block somewhere — in _stg__models.yml, or in the file some contractor wrote in 2024. CoreModels does not overwrite that file, because it may hold properties for models CoreModels does not govern. Instead the run returns a lossiness record naming the model and the exact file whose block has to go. You work that list once, in one pull request, and it does not come back. Budget a morning for a mid-sized project.
Tuesday: one vocabulary, every model that uses the field
With the migration done, the eleven copies collapse into one governed vocabulary. Every model whose order_status column is typed by it gets the same accepted_values test, generated, in its own file:
# Generated by CoreModels — governed model contracts.
# Meaning changes belong in CoreModels; regenerate this file rather than editing it.
version: 2
models:
- name: stg_orders
description: "One row per order, as received from the ordering system."
config:
contract:
enforced: true
materialized: view
columns:
- name: order_id
description: "Immutable identifier for the order."
data_type: varchar
constraints:
- type: not_null
data_tests:
- unique
- not_null
- name: customer_id
description: "The customer who placed the order."
data_type: varchar
data_tests:
- relationships:
to: ref('stg_customers')
field: customer_id
- name: order_status
description: "Where the order sits in fulfillment."
data_type: varchar
data_tests:
- accepted_values:
values: ["placed", "shipped", "delivered", "returned", "cancelled"]
Adding a sixth status is now one edit in one place followed by a regenerate, and the diff touches every file that needed touching — including the four nobody remembered. The relationships test comes from a governed reference rather than a naming convention, and when the target field genuinely is not known, the test is left out and a lossiness record says why. A guessed foreign key that fails on real data is worse than no test at all.
Wednesday: a term bound once, carried two ways
An analyst binds order_status to a published ontology term. The suggestion tools are advisory and persist nothing; the binding is a human decision written onto the governed node. From then on, that IRI rides out of every generate in two places:
- name: order_status
description: "Where the order sits in fulfillment. [schema.org: https://schema.org/orderStatus]"
data_type: varchar
data_tests:
- accepted_values:
values: ["placed", "shipped", "delivered", "returned", "cancelled"]
meta:
coremodels:
maps_to:
- standard: "schema.org"
uri: "https://schema.org/orderStatus"
vocabulary: "Order Status"
The meta block is for machines: a catalog, a lineage tool, or an agent reading the project can resolve the column to its governed term instead of inferring meaning from the column name. When the vocabulary itself or its individual terms are bound, they ride alongside as vocabulary_maps_to and term_maps_to. The bracketed suffix on the description is for the warehouse, and it is there for one blunt reason — description is the only slot persist_docs carries into the column comment. Bound once in CoreModels, the term ends up in your compiled project, in your dbt docs, and in the warehouse column comment where the next person actually looks. If you would rather keep descriptions clean, set iriInDescription to false; the structural meta always rides.
Thursday: the change reviews like a change
Because the output is per-model files sitting beside the models, a meaning change arrives as a normal pull request diff. Three lines in stg_orders.yml, four in fct_orders.yml, reviewed by the people who own those models, in the tool they already review in. Nobody scrolls a two-thousand-line central file to discover that a description moved. CI runs dbt build against the generated contracts exactly as it ran against the hand-written ones, because they are the same kind of file — data_tests: for dbt 1.8 and newer, and the legacy tests: key, with a note in the ledger, if you target an older 1.x. Enforced contracts need dbt 1.5 or above, which is also why every generated column carries a data_type.
Friday: the regenerate that produces nothing
Someone re-runs generate to be sure. Nothing changed in the governed model this week, so the diff is empty. That sounds like a non-event; it is the property that makes everything above safe. Deterministic output means regeneration can sit in a scheduled job or a pre-merge check without producing churn, and it means a non-empty diff is signal: meaning moved, and here is exactly where.
What did not change
CoreModels never wrote to your repository. It never opened a pull request, never ran dbt, never connected to your warehouse, and never called a dbt platform API. Generate returns files; your existing review and merge flow lands them. It does not scaffold projects, write sources, or generate staging models either — dbt-codegen occupies that ground and does it well. And an import on its own still only describes your estate. The week above happens because people wrote definitions, promoted vocabularies, and bound terms on top of it, then published the result back down.
Before: the same enum maintained in eleven files, descriptions that decayed because nothing read them, meaning that stopped at the YAML and never reached the warehouse. After: one definition per concept, published into every model that uses it, arriving as a reviewable diff and carried down to the column comment.
The generate call, its layout and version options, and the lossiness ledger are covered step by step in the CoreModels dbt quickstart in our docs.