dbt logo

The Fourth Option for a Column Named status

This is no longer a hypothetical audience. dbt Labs' [2026 State of Analytics Engineering](https://www.getdbt.com/resources/state-of-analytics-engineering-2026) (n=363) reports **72% of teams prioritizing AI-assisted coding** and **71% concerned about incorrect data reaching stakeholders** — the same teams, describing both halves of the problem below.

The Fourth Option for a Column Named status

This is no longer a hypothetical audience. dbt Labs' 2026 State of Analytics Engineering (n=363) reports 72% of teams prioritizing AI-assisted coding and 71% concerned about incorrect data reaching stakeholders — the same teams, describing both halves of the problem below.

An agent gets a plain question: how many orders were returned last quarter? It finds stg_orders, and it finds a column called status. Before it can write a single predicate, it has to know what goes in the filter.

It has three options, and all three are bad.

Guess from the name. status probably takes 'returned'. Or 'RETURNED', or 'return', or 'R', or 4. The SQL parses, runs, and returns zero rows — or, worse, a number small enough to look plausible in a summary nobody re-checks.

Read the SQL. The model's .sql shows where the column came from, one CASE expression at a time, through however many staging layers sit under it. Sometimes that works. It also spends an agent's whole context window rediscovering a decision somebody made two years ago, and what it recovers is mechanics, not intent: case when s = 4 then 'returned' tells you what the pipeline does, not whether returned includes cancellations.

Ask a human. Correct, slow, and unavailable at two in the morning inside a CI job.

The fourth option is that the column answers for itself, in a form the agent can read without inference and quote back with provenance.

dbt gets close, and then stops

This is not a complaint about dbt's metadata. dbt is one of the few places in a data stack where column documentation is a first-class habit: descriptions live in YAML next to the model, and an accepted_values test even enumerates the legal strings. But look at what an agent actually gets from that. A list of four bare strings, hand-typed into one model's property file. If three models constrain the same business concept, there are three lists, maintained independently, with nothing in the artifacts asserting they are the same vocabulary — and nothing at all saying what any of the values mean or which shared concept the column represents. The list is enforcement without identity. It stops a bad row from landing; it does not tell a reader what a good row means.

The publication contract closes that gap by generating the property file from governed meaning rather than asking someone to type it. Governance lives in CoreModels; the model contract files are its output, one YAML beside each model's own .sql.

What the column says when it can speak

Here is models/staging/stg_orders.yml, generated:

# 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 customer order, staged from the order system."
    config:
      contract:
        enforced: true
      materialized: view
    columns:
      - 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"
            vocabulary_maps_to:
              - standard: "schema.org"
                uri: "https://schema.org/OrderStatus"
            term_maps_to:
              - value: "returned"
                uri: "https://schema.org/OrderReturned"

Three things changed for a machine reading this. The accepted values are no longer a local list: they come from a governed vocabulary named in meta.coremodels.vocabulary, and every other governed column bound to that same vocabulary generates the identical list from the identical source. The column carries an ontology IRI under maps_to — an address, not an adjective. And the term itself is bound, so returned resolves to a published concept rather than to a string an agent has to interpret. Nothing there is invented at generation time; every line is a governed assertion a person made, projected into dbt's own vocabulary.

Two carriage paths, because agents read in two places

An agent grounded in the repo reads the artifacts: the structured meta.coremodels block rides into the compiled manifest, where a tool can resolve it without parsing prose. An agent grounded in the warehouse never sees the YAML. It sees columns and comments. dbt's persist_docs carries exactly one field into a warehouse column comment — the description — which is why bound IRIs are also appended to the description as [standard: uri]. That is a deliberate redundancy: it puts the IRI in the only slot that survives the trip into the warehouse catalog, so describing the table returns something citable. Teams that want the description kept clean set iriInDescription to "false"; the structural meta block rides either way.

Guessing and citing are different acts

A guess and a governed fact can produce the same SQL on a good day. Only one of them can be checked.

An agent that guesses says: I filtered on status = 'returned'. An agent that cites says: I filtered on status = 'returned', which is a term of the governed vocabulary Order Status, bound to a published concept, declared on this column under an enforced contract. A reviewer can falsify the second sentence in under a minute. The first can only be trusted or ignored. That is the whole difference: not that the agent becomes smarter, but that its answers become auditable.

The MCP surface, stated honestly

The same governed model is served over MCP, so an agent calls the same operations directly rather than reading a description of them. With the vendor set to dbt, an agent can call import_vendor_project with a compiled manifest.json, audit_vendor_project against a fresh one, generate_vendor_artifacts to produce the contract files, and get_vendor_integration_status to learn when the estate was last imported and how much of it is governed.

The boundaries are real, not stylistic. Auditing and generating are Viewer-role reads. Importing an estate — and binding an ontology term to a governed node — requires Admin membership on the admin endpoint. An agent can inspect anything, audit anything, and draft anything; changing what the business means still passes through a person holding the role. Generation returns files as content, never a commit: CoreModels does not write into your repo, does not open your pull request, and never touches your warehouse. Your own review flow lands the change, which is the point at which a human sees it.

Ontology suggestions sit on the same side of that line. The lookup tools propose candidate terms for a governed element, and they are advisory — persisted nowhere, authoritative over nothing. A suggestion becomes an IRI in a generated contract only after an Admin binds it. An agent that reports "three candidate terms were suggested" is describing a shortlist; an agent that reports a maps_to binding is describing a decision.

An honest ledger beats a confident file

Generation returns a lossiness ledger alongside the artifacts, and for a machine consumer it is the more interesting half. When a governed reference's target field is not recorded and the target has no unique-tested column, the relationships test is omitted and the ledger says why — a guessed join key would produce a test that fails against a real warehouse and teaches everyone to ignore test failures. When a model already carries properties in another file, the ledger names that file rather than overwriting it. Hierarchical vocabularies flatten into accepted_values and say so. Ephemeral models are skipped, because they cannot carry contracts at all.

The audit reads the same estate from the other end, with stable codes an agent can branch on instead of English it has to interpret: contract-not-enforced, contract-column-missing-type, key-column-untested, source-no-freshness. Run against a fresh manifest, that list is a map of exactly where an agent would still have had to guess.

None of this arrives from importing a manifest. An import alone yields an accurate description of the estate: models, columns, native types, tests, lineage. The meaning that makes a column citable — the description, the shared vocabulary, the bound term — is added by people on top of that skeleton, and the publication contract is what carries it back into the project where both dbt and the agent can read it. Which turns "what does status mean?" from a research task into a lookup, and the agent's answer from fluent into checkable.

The import, audit, and generation calls — including the MCP tool arguments and the layout options — are covered in the CoreModels dbt quickstart in our docs.