Assemble a reviewable first version of an analytics agent's context from the dbt project, schema, dashboards and docs you already have. Evidence attached, unknowns filed as questions.
3
stars
1
commits
Python
primary language
Sep 1, 2026
updated
Assemble a reviewable first version of an analytics agent's context from the dbt models, warehouse schema, dashboards, query history, and documentation you already have.
The method behind it, measured end to end on GitLab's public analytics project (2,921 dbt models), is written up in this blog post.
The kit recovers what those sources already contain, preserves the evidence behind every claim, uses agents to organize, verify, and translate that evidence, and turns unresolved meaning into questions. It never treats model-written prose as evidence. Scripts do everything the inputs determine; you decide the genuine judgment calls at four checkpoints.
What comes out is an ontology: your domains, tables, columns, metrics and joins, described in the words your company already uses, in files you review like code. Point an agent at it and it knows what a row means, which metric is the defined one, and how two tables join, before it writes any SQL.
What also comes out is a list of defects in your own pipeline: documentation that contradicts the SQL, rules stated in one place that silently govern numbers somewhere else, metrics nobody can corroborate. The ontology is the point and the defect list is a byproduct, but it is one worth handing to whoever owns the pipeline, because every entry is proved from your own SQL.
It runs on your machine. No account, no key, no telemetry, no call home: the only thing that leaves your laptop is what the judgment stages send to whatever model you drive the run with.
The kit is how we bootstrap context on real projects today, and what we hand teams to run on their own. It is published standalone and not yet part of the Cassis CLI; when it graduates there, we will keep a standalone version available. Run it and tell us where it breaks.
| Input | Why it is needed |
|---|---|
| A schema export: every table and column, from your information schema | Mandatory. It is what makes an invented column name impossible rather than merely catchable |
| A dbt project, or a dbt docs export of one | Mandatory. Without defining SQL there is no grain, no join evidence, no unit |
A column glossary, your warehouse's own column comments, or dbt {% docs %} blocks | Optional, and the single biggest saving: these three go straight into the ontology in your own words, kept as written. A column glossary has to be handed over as one. Nothing harvests definitions out of prose, because a bold heading matching a column name is not evidence it defines that column |
| Free-form documentation: a wiki export, reference docs, PDFs | Optional. It answers the run's blocking questions, and never fills in a column: for each question where a wrong answer makes a number wrong, retrieval pulls the passages that might answer it, an agent decides whether any of them does, and the candidate reaches you at the fourth checkpoint with the page and line it came from. You accept it or you do not. Applying prose to a column by name instead was measured on a 4,700-page public handbook: 8 columns filled, 8 wrong |
| Dashboard or saved-question exports carrying SQL | Optional. Join evidence, usage ranking and metric corroboration at once |
| A query log, if you can export one | Optional. Ranks what people actually query |
Nothing here asks you to write documentation. Point the kit at what already exists.
If you cannot run SQL against the warehouse yourself, say so at intake and nothing will try to connect, ever. Questions only the warehouse could settle go into the open-questions file instead.
python3 -m pip install -r requirements.txt
Three pure-python packages. Test dependencies are separate (requirements-dev.txt) and you do not
need them to run the kit.
Drive it from a coding-agent session in this directory; the repo ships a CLAUDE.md the agent
reads. The phases below are scripts and cost nothing. Where the driver prints an enrichment
stage, that is the agent's evidence-backed drafting and it is where the tokens go.
python3 intake.py questions # nine questions, answered once
python3 intake.py write --name mywarehouse \
--schema ~/exports/schema.json \
--input ~/code/our-dbt-project --adapter dbt \
--docs-dir ~/exports/wiki --dashboards-dir ~/exports/dashboards \
--warehouse-access no --profile sample \
--top-question "how much revenue did we make last month"
python3 bootstrap.py prep --config configs/mywarehouse.yml # stops for the scope
python3 bootstrap.py build --config configs/mywarehouse.yml # stops for tree, then metrics
python3 bootstrap.py finish --config configs/mywarehouse.yml # stops for the blocking questions
--name just names the run and its config file; the phases read every path and every recorded
fact from that file, so they are stated once. Add --emit dbt to finish to also merge the
result back into the dbt project it came from.
Start with a sample. --profile sample hard-scopes the run to about ten tables carrying one
story end to end. It is the fastest way to see what the output looks like on your own data, and the
cheapest way to find out that a schema export is malformed or a docs directory holds nothing the
kit can read. The only things the profile changes are the scope target and the cost.
| Run | Scope | The model writes | Wall clock | At list API prices |
|---|---|---|---|---|
--profile sample | ~10 tables, one story | ~0.6M tokens, all Sonnet | ~40 min | roughly $30–60 |
--profile full | 20–30 tables, a first increment | ~1.6M tokens: an Opus driver plus ~20 Sonnet subagents | ~2 h | roughly $120–200 |
Both rows are one real warehouse each, recounted in full from the session transcripts rather than projected. Treat the dollars as an order of magnitude: most of a run's list price is agents re-reading their own context as cache reads, so the total moves with cache behavior and with how many turns the agents take, not with the ontology's size. On a Claude subscription the currency is your usage window rather than dollars, and cache reads are cheap there: a sample run is one sitting's worth of work, and a first increment is the largest thing you will run that day. Give it its own session.
What moves a run up its range: a large docs corpus (one packet agent per blocking question), corrections at the checkpoints (a corrected domain or metric redrafts its files), and the judge pass, an agent that re-reads every drafted claim against the evidence behind it. The judge is part of the run, not an option: it is the only stage that has caught a description contradicting its own SQL, on two different warehouses.
The scope is yours (it is checkpoint 1), and the cost follows the scope, not the warehouse. At 2,000 tables the shape is the same, not bigger: the deterministic phases read the whole export (they are scripts, and free), the classifier proposes the cut, and the modeled increment stays 20–30 tables per run. You grow the ontology increment by increment rather than paying for the warehouse in one sitting, and every deterministic stage is free to re-run.
The driver stops at each one and prints what to look at. Each stop is a generated file you read top to bottom, never a list of questions asked one at a time.
review. Which tables are worth modeling is a business
judgment, and it is yours: cut the file to the tables your top questions actually touch.Everything the run could not settle but that does not change a number stays in the open-questions file, with the assumption it shipped. A tool that needs every unknown answered before it produces anything does not survive first contact.
OUTPUT.md is the map: two copies of the ontology and a set of reports.
<run>/emit/cassis/ is the canonical tree, and the one to keep: domain READMEs, one file per
table, one per metric, and the joins. <run>/cassis/ is the working tree the run assembles and
the checks read; it carries per-column provenance that the canonical format does not accept.<run>/emit/CLAUDE.md is copied in beside the tree: what each file holds and the order to read
it in, for whoever you hand it to.This is what a table file looks like. It is real output, from a run over the four-model fixture project this repo ships:
schema_name: MAIN
table_name: ORDERS
domain_path: commerce
description: One row per order placed in the store.
grain:
- ORDER_ID
columns:
- name: ORDER_ID
description: Primary key of the order, assigned by the storefront at checkout.
data_type: VARCHAR
- name: STATUS
description: 'Order lifecycle status: completed or cancelled.'
data_type: VARCHAR
- name: TOTAL_AMOUNT
description: Order total in EUR, tax included. Cancelled orders keep their amount.
unit: EUR
data_type: DECIMAL
sample-output/ holds the rest of the excerpt (a metric with a mandatory
filter, a domain README, and the file tree of the whole emitted run), regenerable with
python3 tools/make_sample_output.py.
--emit dbt merges the ontology into your dbt project's own schema.yml files, in place:
relationships tests, with the grain and cardinality beside them under
meta.cassis.join.meta.cassis.*, one documented namespace. With persist_docs on, the descriptions reach the
warehouse itself as column comments, where an agent querying it directly can see them.Two limits, both measured:
semantic_models and metrics only if your project already has a
MetricFlow time spine. Without one, a project stops parsing the moment any metric exists, so
metrics ship under meta.cassis.metrics instead and your build keeps working.Verified by handing the merged project to dbt itself: dbt parse over real projects, including a
public one of 2,921 models, plus dbt run and dbt docs generate on this repo's fixture in CI.
dbt test suite may go red after an export: a relationships test that fails is a
finding about the data, not a bug in the export, but it is still your suite that turns red.python3 -m pip install -r requirements-dev.txt
python3 tests/test_kit.py
Runs on a fresh clone with no data of your own and no environment variables, and skips rather than fails what it cannot run.
Issues are on. What you get is best-effort: we read them, real failures get fixed, and there is no SLA. If a run fails: stop at the phase banner, keep the run directory, and open an issue with the banner and the phase name. The banner names the stage and what it was reading, which is most of the diagnosis.
The ontology you get here is a snapshot of what your warehouse means today, and warehouses move: a column changes meaning, a metric gains an exception, a definition turns out to be wrong the first time somebody asks a question it cannot answer. Keeping that context true as the warehouse moves is what Cassis does: the same ontology, enriched from real use, with every change reviewed by the data team before it becomes truth.
The output here is a Cassis ontology already, so there is nothing to migrate:
pip install cassis-cli
cassis ontology upload --project <id> --no-publish # review it in Cassis first
cassis ontology fmt # writes AGENTS.md into the checkout
MIT. See LICENSE. adapters/vendor/inventory.py is vendored from dbt-agent-readiness (MIT),
with its license and provenance beside it.
1 commits
Python
100.0%
Assemble a reviewable first version of an analytics agent's context from the dbt project, schema, dashboards and docs you already have. Evidence attached, unknowns filed as questions.
3
stars
1
commits
Python
primary language
Sep 1, 2026
updated
Assemble a reviewable first version of an analytics agent's context from the dbt models, warehouse schema, dashboards, query history, and documentation you already have.
The method behind it, measured end to end on GitLab's public analytics project (2,921 dbt models), is written up in this blog post.
The kit recovers what those sources already contain, preserves the evidence behind every claim, uses agents to organize, verify, and translate that evidence, and turns unresolved meaning into questions. It never treats model-written prose as evidence. Scripts do everything the inputs determine; you decide the genuine judgment calls at four checkpoints.
What comes out is an ontology: your domains, tables, columns, metrics and joins, described in the words your company already uses, in files you review like code. Point an agent at it and it knows what a row means, which metric is the defined one, and how two tables join, before it writes any SQL.
What also comes out is a list of defects in your own pipeline: documentation that contradicts the SQL, rules stated in one place that silently govern numbers somewhere else, metrics nobody can corroborate. The ontology is the point and the defect list is a byproduct, but it is one worth handing to whoever owns the pipeline, because every entry is proved from your own SQL.
It runs on your machine. No account, no key, no telemetry, no call home: the only thing that leaves your laptop is what the judgment stages send to whatever model you drive the run with.
The kit is how we bootstrap context on real projects today, and what we hand teams to run on their own. It is published standalone and not yet part of the Cassis CLI; when it graduates there, we will keep a standalone version available. Run it and tell us where it breaks.
| Input | Why it is needed |
|---|---|
| A schema export: every table and column, from your information schema | Mandatory. It is what makes an invented column name impossible rather than merely catchable |
| A dbt project, or a dbt docs export of one | Mandatory. Without defining SQL there is no grain, no join evidence, no unit |
A column glossary, your warehouse's own column comments, or dbt {% docs %} blocks | Optional, and the single biggest saving: these three go straight into the ontology in your own words, kept as written. A column glossary has to be handed over as one. Nothing harvests definitions out of prose, because a bold heading matching a column name is not evidence it defines that column |
| Free-form documentation: a wiki export, reference docs, PDFs | Optional. It answers the run's blocking questions, and never fills in a column: for each question where a wrong answer makes a number wrong, retrieval pulls the passages that might answer it, an agent decides whether any of them does, and the candidate reaches you at the fourth checkpoint with the page and line it came from. You accept it or you do not. Applying prose to a column by name instead was measured on a 4,700-page public handbook: 8 columns filled, 8 wrong |
| Dashboard or saved-question exports carrying SQL | Optional. Join evidence, usage ranking and metric corroboration at once |
| A query log, if you can export one | Optional. Ranks what people actually query |
Nothing here asks you to write documentation. Point the kit at what already exists.
If you cannot run SQL against the warehouse yourself, say so at intake and nothing will try to connect, ever. Questions only the warehouse could settle go into the open-questions file instead.
python3 -m pip install -r requirements.txt
Three pure-python packages. Test dependencies are separate (requirements-dev.txt) and you do not
need them to run the kit.
Drive it from a coding-agent session in this directory; the repo ships a CLAUDE.md the agent
reads. The phases below are scripts and cost nothing. Where the driver prints an enrichment
stage, that is the agent's evidence-backed drafting and it is where the tokens go.
python3 intake.py questions # nine questions, answered once
python3 intake.py write --name mywarehouse \
--schema ~/exports/schema.json \
--input ~/code/our-dbt-project --adapter dbt \
--docs-dir ~/exports/wiki --dashboards-dir ~/exports/dashboards \
--warehouse-access no --profile sample \
--top-question "how much revenue did we make last month"
python3 bootstrap.py prep --config configs/mywarehouse.yml # stops for the scope
python3 bootstrap.py build --config configs/mywarehouse.yml # stops for tree, then metrics
python3 bootstrap.py finish --config configs/mywarehouse.yml # stops for the blocking questions
--name just names the run and its config file; the phases read every path and every recorded
fact from that file, so they are stated once. Add --emit dbt to finish to also merge the
result back into the dbt project it came from.
Start with a sample. --profile sample hard-scopes the run to about ten tables carrying one
story end to end. It is the fastest way to see what the output looks like on your own data, and the
cheapest way to find out that a schema export is malformed or a docs directory holds nothing the
kit can read. The only things the profile changes are the scope target and the cost.
| Run | Scope | The model writes | Wall clock | At list API prices |
|---|---|---|---|---|
--profile sample | ~10 tables, one story | ~0.6M tokens, all Sonnet | ~40 min | roughly $30–60 |
--profile full | 20–30 tables, a first increment | ~1.6M tokens: an Opus driver plus ~20 Sonnet subagents | ~2 h | roughly $120–200 |
Both rows are one real warehouse each, recounted in full from the session transcripts rather than projected. Treat the dollars as an order of magnitude: most of a run's list price is agents re-reading their own context as cache reads, so the total moves with cache behavior and with how many turns the agents take, not with the ontology's size. On a Claude subscription the currency is your usage window rather than dollars, and cache reads are cheap there: a sample run is one sitting's worth of work, and a first increment is the largest thing you will run that day. Give it its own session.
What moves a run up its range: a large docs corpus (one packet agent per blocking question), corrections at the checkpoints (a corrected domain or metric redrafts its files), and the judge pass, an agent that re-reads every drafted claim against the evidence behind it. The judge is part of the run, not an option: it is the only stage that has caught a description contradicting its own SQL, on two different warehouses.
The scope is yours (it is checkpoint 1), and the cost follows the scope, not the warehouse. At 2,000 tables the shape is the same, not bigger: the deterministic phases read the whole export (they are scripts, and free), the classifier proposes the cut, and the modeled increment stays 20–30 tables per run. You grow the ontology increment by increment rather than paying for the warehouse in one sitting, and every deterministic stage is free to re-run.
The driver stops at each one and prints what to look at. Each stop is a generated file you read top to bottom, never a list of questions asked one at a time.
review. Which tables are worth modeling is a business
judgment, and it is yours: cut the file to the tables your top questions actually touch.Everything the run could not settle but that does not change a number stays in the open-questions file, with the assumption it shipped. A tool that needs every unknown answered before it produces anything does not survive first contact.
OUTPUT.md is the map: two copies of the ontology and a set of reports.
<run>/emit/cassis/ is the canonical tree, and the one to keep: domain READMEs, one file per
table, one per metric, and the joins. <run>/cassis/ is the working tree the run assembles and
the checks read; it carries per-column provenance that the canonical format does not accept.<run>/emit/CLAUDE.md is copied in beside the tree: what each file holds and the order to read
it in, for whoever you hand it to.This is what a table file looks like. It is real output, from a run over the four-model fixture project this repo ships:
schema_name: MAIN
table_name: ORDERS
domain_path: commerce
description: One row per order placed in the store.
grain:
- ORDER_ID
columns:
- name: ORDER_ID
description: Primary key of the order, assigned by the storefront at checkout.
data_type: VARCHAR
- name: STATUS
description: 'Order lifecycle status: completed or cancelled.'
data_type: VARCHAR
- name: TOTAL_AMOUNT
description: Order total in EUR, tax included. Cancelled orders keep their amount.
unit: EUR
data_type: DECIMAL
sample-output/ holds the rest of the excerpt (a metric with a mandatory
filter, a domain README, and the file tree of the whole emitted run), regenerable with
python3 tools/make_sample_output.py.
--emit dbt merges the ontology into your dbt project's own schema.yml files, in place:
relationships tests, with the grain and cardinality beside them under
meta.cassis.join.meta.cassis.*, one documented namespace. With persist_docs on, the descriptions reach the
warehouse itself as column comments, where an agent querying it directly can see them.Two limits, both measured:
semantic_models and metrics only if your project already has a
MetricFlow time spine. Without one, a project stops parsing the moment any metric exists, so
metrics ship under meta.cassis.metrics instead and your build keeps working.Verified by handing the merged project to dbt itself: dbt parse over real projects, including a
public one of 2,921 models, plus dbt run and dbt docs generate on this repo's fixture in CI.
dbt test suite may go red after an export: a relationships test that fails is a
finding about the data, not a bug in the export, but it is still your suite that turns red.python3 -m pip install -r requirements-dev.txt
python3 tests/test_kit.py
Runs on a fresh clone with no data of your own and no environment variables, and skips rather than fails what it cannot run.
Issues are on. What you get is best-effort: we read them, real failures get fixed, and there is no SLA. If a run fails: stop at the phase banner, keep the run directory, and open an issue with the banner and the phase name. The banner names the stage and what it was reading, which is most of the diagnosis.
The ontology you get here is a snapshot of what your warehouse means today, and warehouses move: a column changes meaning, a metric gains an exception, a definition turns out to be wrong the first time somebody asks a question it cannot answer. Keeping that context true as the warehouse moves is what Cassis does: the same ontology, enriched from real use, with every change reviewed by the data team before it becomes truth.
The output here is a Cassis ontology already, so there is nothing to migrate:
pip install cassis-cli
cassis ontology upload --project <id> --no-publish # review it in Cassis first
cassis ontology fmt # writes AGENTS.md into the checkout
MIT. See LICENSE. adapters/vendor/inventory.py is vendored from dbt-agent-readiness (MIT),
with its license and provenance beside it.
1 commits
Python
100.0%