dbt logo

dbt Owns the Transformation. Nobody Owns the Definition.

Pick one column — `status` in your orders mart — and count the tools that hold an opinion about it. dbt tests it and, if the model is contracted, refuses to build when its type changes. A catalog lists it, draws its lineage, and lets someone attach a note. An observability tool watches its distribution and raises an alert when yesterday looks unlike last week. A semantic layer exposes it as a dimension. Outside the warehouse, an API team ships a field meant to mean the same thing, and a streaming team keeps an enum on a topic meant to mean it again.

dbt Owns the Transformation. Nobody Owns the Definition.

Pick one column — status in your orders mart — and count the tools that hold an opinion about it. dbt tests it and, if the model is contracted, refuses to build when its type changes. A catalog lists it, draws its lineage, and lets someone attach a note. An observability tool watches its distribution and raises an alert when yesterday looks unlike last week. A semantic layer exposes it as a dimension. Outside the warehouse, an API team ships a field meant to mean the same thing, and a streaming team keeps an enum on a topic meant to mean it again.

Every one of those tools is doing its job. None of them is the place where the meaning of status is authored. That layer usually lives in a wiki page, a chat thread, and the memory of the analytics engineer who chose the values in the first place.

What dbt owns, and what this does not touch

dbt owns transformation: the ref graph, the build order, its generic tests, its own model contracts, its own documentation site. Those are good primitives, and this integration competes with none of them. It does not scaffold a project, does not generate sources.yml, and does not write staging models — dbt-codegen occupies that ground. It never runs dbt, never connects to a warehouse, and never writes into your repository; generation returns files, and your own pull-request flow lands them.

What it adds sits one step upstream of the YAML: the definitions the YAML is generated from. The output is ordinary dbt — one property file per model, colocated beside that model's own .sql, each block carrying config.contract.enforced: true, a data_type on every column, not_null constraints, and tests. Nothing in the file is a CoreModels format. It is dbt's format, written from a source dbt does not have.

The neighbors, and where each one stops

Describe the categories around dbt by their boundaries rather than by their logos and a gap appears in the middle. Modeling tools produce a schema for a target system; the deliverable is that schema, and when the target changes the model is re-authored against the new one. Catalogs discover, search, and display — the estate is their input, harvested after it exists, and annotations are decoration applied on top of what was found. Observability tools watch production and report change after it has shipped: necessary work, and after the fact by construction. Semantic layers define metrics and dimensions over already-modeled tables, mostly for reporting consumers.

Each of those is well-shaped for its job, and each takes the estate as given. What is left unowned is the step before all of them: one definition of what a field means, authored once by a human and published into many consumers, of which dbt is one.

Why an annotation is weaker than a binding

Say a governance team decides that status corresponds to a specific term in a published vocabulary. Recorded as a catalog annotation, that decision is downstream of the thing it describes — the note hangs on an asset some pipeline produced, so rebuild the asset under a new name and the note dangles. It is out of band: the dbt property file does not carry it, the warehouse column comment does not carry it, the JSON Schema handed to the API team does not carry it, and only a consumer that queries the catalog ever sees it. It is per-asset, so the same decision applied across nine models becomes nine independently editable notes. And it enforces nothing, because no build fails when it stops being true.

In CoreModels that decision is a binding on the governed node. Term suggestions come from a public ontology lookup service, are advisory, and are persisted nowhere; an Admin binds one deliberately, and the binding becomes an assertion on the model or the column itself rather than a note about it. Because it is part of the definition, it rides out with the definition — into dbt's own vocabulary, in the file dbt already reads:

      - name: status
        description: "The order's lifecycle state. [schema.org: https://schema.org/orderStatus]"
        data_type: "varchar(20)"
        constraints:
          - type: not_null
        data_tests:
          - not_null
          - accepted_values:
              values: ["placed", "shipped", "completed", "returned"]
        meta:
          coremodels:
            maps_to:
              - standard: "schema.org"
                uri: "https://schema.org/orderStatus"
            vocabulary: "Order Status"

Two carriage paths, deliberately. The meta.coremodels.maps_to block is structural, so an agent or a catalog resolves the column to a governed term instead of guessing from its name. The trailing [standard: uri] in the description is there because description is the only slot persist_docs carries into the warehouse column comment, which puts the bound term in front of someone reading the physical table with no CoreModels access at all. Teams that want their descriptions kept clean switch the second path off; the structural one always rides.

The tests obey the same rule. accepted_values is generated from the governed vocabulary named in meta.coremodels.vocabulary, so one vocabulary produces the identical list in every model that uses the field, and widening it is one edit rather than nine. relationships tests come from governed references — and when the target field cannot be determined, the test is omitted with a lossiness record saying why, rather than guessed into a form that fails against a real warehouse.

One definition, several dialects

The dbt property file is one serialization, not the destination. The same governed model exports JSON Schema, Avro, SQL DDL, LinkML, JSON-LD, ShEx, and OWL, which matters because most of the people who depend on status never open the warehouse: the API team wants JSON Schema, the streaming team wants an Avro record, a modeling group wants LinkML, semantic tooling wants OWL or ShEx. Under a catalog-annotation model, each of those consumers reconstructs the meaning by hand. Under a publication model, each receives it in its own dialect, from one source, with the same bound term attached.

The reverse direction is where most teams start, and it deserves an honest description. Importing a manifest gives you a faithful inventory — models, columns, tests, taxonomies from accepted_values, references, and lineage — and an inventory is not governance. The value appears on the second move, when a human writes the descriptions, consolidates near-duplicate value lists into one vocabulary, binds the terms that matter, and publishes the result back into the project as contracts.

Being format-neutral is the point

Neutral specifications for data contracts and semantic models are emerging, consolidating, and passing to independent stewards. That trend is good, and it is also a reason not to bet the definition on any single carrier: we implement published specifications rather than inventing a private one, and the governed model is shaped like none of them. If a neutral format becomes the common currency of your industry, it is one more export, not a migration.

That is what makes dbt an export target rather than a home. A definition that lives inside a transformation project's YAML can only be published where that project reaches. A definition held outside every vendor can go into the transformation project, the warehouse comment, the event schema, and the API specification, and still be one definition afterward.

The honest edges

CoreModels has no version control of its own, no diff between two points in time, and no notification when a definition changes on its side. Transformation, mapping, and publication are HTTP and MCP surfaces today, not a user interface. When a model already has properties in another file, generation does not rewrite that file — it names the file and says which block to remove, because that file may hold properties for models CoreModels does not govern. Ephemeral models are skipped, since they cannot carry contracts. Each of those is stated on the run that hits it, rather than discovered later.

The division of labor

dbt keeps building the tables and failing the build when a contracted model changes shape. The catalog keeps helping people find things, observability keeps watching production, and the semantic layer keeps serving the reports. The definition — what the field means, which term it corresponds to, which values are permitted — sits outside all of them, governed by people, and published into each in the dialect that tool already speaks.

The publication path end to end — import a manifest, govern the meaning, generate the contract files — is in the CoreModels dbt quickstart in our docs.