---
title: "Building an Ownership Register That Survives Contact With a Real Team"
slug: "t8-ownership"
type: article
locale: en-US
canonical: "https://www.coremodels.io/connector/dbt/t8-ownership"
updatedAt: "2026-09-04T05:04:27+03:00"
---

# Building an Ownership Register That Survives Contact With a Real Team

Ownership registers fail in a predictable way: someone fills a spreadsheet in a week of good
intentions, three people leave over the next year, and the file becomes an archaeological record of
who used to work here. This guide builds one that does not do that.

Prerequisite: a dbt project imported from `manifest.json`.

## Why it is columns, not a document

The register is a set of **mixin columns on the governed model** — not a separate artifact. That is
the whole design, and it is what makes it survivable.

A document about the model drifts from the model, because nothing connects them. Columns *on* the
model move with it: rename a concept and its owner follows, because the owner is a property of the
thing rather than a row in a file keyed by a name that just changed.

The recipe provisions five fields on every column and vocabulary:

| Field | What it holds | Why it is there |
|---|---|---|
| `termOwner` | The person who decides what this means | The question everyone actually asks |
| `stewardTeam` | The team accountable | Survives the person leaving |
| `decisionForum` | Where the decision gets made | Survives the *team* being reorganized |
| `reviewCadence` | How often it is revisited | Turns "is this still right?" into a date |
| `lastReviewed` | When it last was | Makes staleness visible instead of assumed |

Plus `businessDomain` and `accountableTeam` at model level, for the coarser cut.

## Step 1 — Set a default before you set anything else

Counterintuitive, and it is the step people skip.

Assign your whole estate to one team on day one — analytics engineering, or whoever imported it.
The register is now wrong in an interesting way rather than empty in a useless one, because **an
imperfect default makes the exceptions visible.** "This is ours" invites "no, that one is finance's"
in a way that a blank column never does.

The recipe's third wizard step takes that default so it is applied at setup rather than as a
thousand edits afterwards.

## Step 2 — Work the "no owner" filter

Filter the ownership grid to rows where `termOwner` is empty. **That list is the work.** It is also
usually longer than anyone in the room expects, which is the useful part — ambiguous ownership feels
like a vague cultural complaint right up until it is a filtered grid with a count on it.

Work it in this order:

1. **Vocabularies first.** They are where the disagreements live. Nobody argues about
   `customer_id`; people argue for weeks about what counts as an active customer.
2. **Columns that appear in more than one model.** Shared concepts, shared confusion.
3. **Everything else**, at whatever pace is honest.

## Step 3 — Prefer a forum to a person

`decisionForum` is the field that makes this durable, and it is the one that looks least necessary
until you have watched a register rot.

A named individual is a single point of failure: they change teams, go on leave, or leave entirely,
and the register silently becomes wrong while still looking authoritative. A forum — a weekly
review, a working group, a channel with a stated convention — survives all three.

It also lets you record the honest answer. Sometimes **nobody** decides a definition, and the right
entry is the forum where that conversation now has to happen, rather than a name picked to make the
field non-empty.

## Step 4 — Make the cadence real

`reviewCadence` plus `lastReviewed` is the difference between a register and a snapshot.

Quarterly is a reasonable default for most concepts, annual for stable ones, and "on change" for
things that only matter when they move. The pair matters more than either field: a cadence with no
last-reviewed date cannot tell you what is overdue, and a date with no cadence cannot tell you
whether it is late.

## What you get once it is populated

**A question that has an answer.** "Who decides whether this counts as an active customer?" resolves
to a name and a forum instead of a thread.

**A queryable register, at no extra cost.** Because CoreModels generates its GraphQL schema from the
model's mixins and relation groups, the ownership fields become queryable the moment the mixin
exists. Nobody builds an API for this; it follows from the columns being columns.

**The prerequisite for everything else.** Review workflows need to know who reviews. Impact answers
are only actionable when they name someone. Notifications need somewhere to route. Each of those is
a different feature, and all of them are inert on an estate where nothing has an owner — which is
the real argument for doing this unglamorous thing first.

## What this does not do

- **It does not enforce anything.** Nothing blocks a change because an owner did not approve it;
  that is a review workflow, and it is a different piece of work that depends on this one.
- **It does not notify the owner.** Change notification for governed-model edits is not built. Any
  claim otherwise is wrong today.
- **It does not know your org chart.** There is no directory sync — these are values you set, which
  is also why they can record a forum, a rotating role, or "unassigned, discuss on Thursday" when
  those are the true answers.

## Where to go next

With owners in place, the impact question becomes actionable — "this affects four contracts" is only
useful when it also says who to talk to about each. The dbt quickstart that ships with the CoreModels
integration docs covers the import this guide assumed.

## Recipes

- [Who decides what this column means?](https://www.coremodels.io/recipe/dbt/ownership-register): 41% of analytics teams say data ownership is ambiguous. dbt can tell you who owns a model. Nobody owns a definition.
