dbt logo

Finding the `accepted_values` List That Drifted

Two files in your project test the same column against different value lists. Both tests pass, so nothing has ever told you. This guide finds them, and turns the list into something that can only be defined once.

Finding the accepted_values List That Drifted

Two files in your project test the same column against different value lists. Both tests pass, so nothing has ever told you. This guide finds them, and turns the list into something that can only be defined once.

Prerequisite: a dbt project imported from manifest.json. Nothing else.

What the import already did

Every accepted_values test in the manifest became a governed vocabulary — a first-class object with a name, terms, and somewhere to put a definition — rather than a string array sitting inside a test.

That conversion is what makes the comparison possible. Eleven copies of a list are eleven unrelated arrays as far as dbt is concerned. Once each is an object referenced by the columns that use it, the question "do these agree?" finally has something to compare.

The recipe opens on the Vocabularies grid, which is that view.

Step 1 — Find the ones that matter

Sort the grid by how many columns reference each vocabulary. The single-reference ones are usually fine and always low-stakes; a list used by one model can only disagree with itself.

The multi-reference rows are the whole exercise. A vocabulary referenced by four columns is four chances for someone to have updated three of them.

For each one, open it and compare the terms against what the column should permit. Two failure shapes turn up almost every time:

  • A value that exists in the warehouse and in no list. Somebody added a status and updated the model they were working in. This is the common one.
  • A value in the list that the warehouse has not produced in two years. Harmless, and worth removing, because a permitted value nobody has seen is a value nobody has thought about.

If you have catalog.json imported as well, the column types are warehouse-real, which makes the second check considerably less speculative.

Step 2 — Make it a definition, not a list

The value of a vocabulary over an array is that it can hold things an array cannot. Three worth filling in:

A name that is the concept, not the column. order status values is better than fct_orders_status_accepted_values, because the same vocabulary is about to govern stg_orders too, and a name derived from one model reads as wrong everywhere else.

A description per term. What does return_pending actually mean — the customer has requested a return, or the warehouse has received it? Teams argue about this and then encode one interpretation into a dashboard. Writing it down is often the first time the argument gets resolved rather than relitigated.

Structure, where the concept has it. return_pending and returned are not peers; one is a stage of the other. A vocabulary can hold that nesting and the grid renders it as a tree.

That last point comes with an honest caveat: dbt's accepted_values is a flat list, so when the list is published back the hierarchy cannot cross the boundary. The values go; the structure stays here. The loss report says so explicitly rather than dropping it quietly — and the structure still earns its place, because it is what an agent reads and what a new analyst learns from.

Step 3 — Give it an owner

The recipe provisions ownership columns on the vocabulary grid for a reason: a definition without an owner is a definition that drifts again in six months, and you will be doing this exercise a second time.

Vocabularies are the right place to start assigning ownership across the whole estate, because they are what people actually argue about. Columns rarely cause disputes; the permitted values of a status column cause them constantly.

Step 4 — Publish it back

Once the corrected list is the governed one, it can be generated into every model that uses the field — so the eleven copies become eleven outputs of one definition and can no longer disagree.

This step is Early access today: the generation is built and tested, and we run it with you while the self-serve path is finished. The reason to say that plainly here rather than in a footnote is that steps 1–3 are worth doing on their own. Finding the drifted list and writing down what the values mean is most of the value, and it is available now.

What you will notice afterwards

The specific relief is small and repeats: nobody types an enum into YAML again, and the question "which list is right?" stops being asked, because there is one.

The larger one takes a month to show up. Once the vocabulary has descriptions, the arguments about what return_pending means stop recurring — they were never really about the column, they were about the fact that nobody had written the answer down anywhere durable.

What this does not do

  • It does not detect values in your data that are missing from the list. This reads your project metadata, not your warehouse rows. A dbt test run is still what catches an unexpected value in production, and that job is dbt's.
  • It does not merge two vocabularies for you. If order status and order_state turn out to be the same concept modelled twice, deciding that is a human call — the crosswalk workspace is where that adjudication happens.

The dbt quickstart that ships with the CoreModels integration docs covers the import this guide assumed.