How to give Claude the context and guardrails to work safely in dbt with Dex skills
Agent skills shouldn't just be markdown files. Dex by Exmergo helps Claude use dbt with an open source analytics engineering CLI

Co-Founder & CEO

Agent skills shouldn’t just be markdown files. Hot take, I know.
This is especially true when dealing with the modern data stack. If you point a coding agent at a warehouse and ask it to model web events against customers, it will find web_events.customer_id, notice that customers.customer_id has the same name and the same type, and join them. The build passes. The model materializes. The numbers come out empty, or worse, they come out plausible and nobody looks again for a quarter.
The agent was not being careless. Name and type are the only two facts a schema holds about that column, and both of them said yes. Overlap is not in the schema. It is in the data, and somebody has to go and measure it.
That is the gap Dex is built to close. Dex is an open-source analytics-engineering toolkit for coding agents such as Claude. It sits between the agent, your warehouse, and your dbt project: it helps the agent learn what is in the data, governs how it can query it, and turns proposed dbt work into reviewable changes. Claude still does the reading, reasoning, and writing. Dex gives it the evidence and boundaries it would otherwise have to guess at.
To make that concrete, Dex ships with dex demo, a local DuckDB warehouse intentionally seeded with problems an agent cannot see from a schema. In it, the web_events.customer_id and customers.customer_id join has zero overlap—not partial, zero. Every value on the child side matches nothing on the parent.
Before you merge an agent-authored model, it needs facts about your warehouse that are not in the schema, a way to carry those facts into the next session, and guardrails that do more than tell it to be careful.
This cannot be solved with just a collection of markdown files (though they do help!). In most agent skills, that’s all you get. Dex gives you that + an open-source CLI to manage the complexity of your data workflow.
Facts don’t live in metadata
Dex builds its picture of a warehouse from aggregates (never from raw rows, which shouldn’t go into an agent’s context window). One catalog pass to see what exists, a ranking pass to decide what deserves attention, then column profiles assembled from batched aggregate queries.
What happens when an answer is not good enough? Distinct counts start approximate, because approximate is what scales. An approximate count can overshoot a genuinely unique column and turn a real key into a shrug, so any column whose approximate count lands close to its row count gets re-counted with an exact COUNT(DISTINCT). Only an exact count is allowed to confirm a key. Where no single column is unique, which is the shape of every fact table you have ever met, dex probes two-column composite keys, pruned down to the pairs that are arithmetically possible before it spends anything measuring them.
That machinery exists to answer one question honestly: what is one row of this table?
In the demo warehouse, order_item_id is not the answer. There are 13,000 distinct values across 14,000 rows, because a batch got loaded twice. Any join on it fans out silently. In the same warehouse, sku is 90% numeric and 10% thirty-two-character hexadecimal, left over from a catalogue merge, so casting it to a number drops a tenth of your rows and raises nothing. Add a table an interrupted load left empty and two columns whose declared type contradicts their contents, and that is a small seven-table warehouse.
None of that is visible from a schema. All of it changes what the correct model is.
The other thing dex refuses to do is hand the agent everything at once. An agent given 2,000 table definitions has the same problem an engineer given 2,000 table definitions has, and it solves it the same bad way: skim, pattern-match, commit. Ranking exists so that attention goes somewhere before it goes everywhere.
Then there is your dbt project, which is a different kind of input entirely. The warehouse tells you what is true right now. The project tells you what your team decided it means. The interesting cases are the ones where the two disagree, and dex is careful not to resolve them on its own. A relationship your project declares stays a declaration. If a probe finds that its parent side is mostly missing, that comes back as a finding, not as a correction to your project. Evidence and intent are allowed to conflict. Somebody who knows the business gets to say which one is wrong.
Writing into dbt
We got this architecture wrong the first time, and the correction is the most load-bearing decision in the product.
The original design made a dex-native semantic model the source of truth, with dbt as a projection of it. The reasoning was that the open standards were immature, so we would build something better. That is a bad instinct wearing a good instinct’s clothes. A format we invent has no spec, no tooling and no users, which makes it the least mature option on the table rather than the most stable. It was also wrong on the facts. MetricFlow already expresses nearly everything we thought we were missing: entities, categorical and time dimensions with granularity, measures, and metrics of kind simple, ratio, derived and cumulative. We had planned, in effect, to reimplement MetricFlow, worse.
So the dbt project is the source of truth and dex keeps no copy of it. The consequence is worth being precise about, because it is what makes human authority real rather than procedural: there is no version of your model inside dex that could overwrite yours. Your edits win because there is nothing for them to lose to.
The engine also does not author your dbt. Claude writes the proposed file contents; Dex validates them, computes the diff, and stores the result as a plan. Claude handles the investigation and implementation. The engineer keeps the business and semantic judgment. Dex handles the checks that should not depend on either party remembering every edge case.
That shows up most clearly in refactors. Dex indexes dbt calls rather than blindly matching text, reports an argument it cannot resolve as indeterminate, and says when its answer has limits. That lets transform rename refuse if it cannot prove the rewrite is complete, and transform remove stop while anything still reads the thing being removed. Semantic YAML is overlaid in a throwaway project copy and parsed by dbt before the plan is stored.
Each plan is pinned to the files it would change. Before applying it, Dex hashes them again. If someone edited a file in the meantime, nothing is written and the divergence comes back as a diff. That is what makes fast agent work safe on a project other people are editing.
What happens when I kill my agent session?
An agent that starts every session from nothing can only ever be lent a small amount of trust, because it re-earns that trust from zero each time and you are the one paying.
Dex keeps what it learns in .dex/, next to the project, in git. Three files doing three genuinely different jobs, and the separation matters more than any of the contents.
config.yml holds decisions a human already made. PII is the clearest case. Detection reads column names and aggregate shapes and never values, so it flags the city column on a distribution centre as location data. It should. A detector that never trips on that column is a detector that is going to miss a column called contact. The fix is not to make detection cleverer, it is a pii_overrides entry naming the column and a reason. That entry is re-applied on every profile, survives re-profiling, takes effect at query time immediately, keeps an audit trail of which category the detector had matched, and shows up in a pull request like any other decision your team made on purpose.
cache.json holds what was measured: profiles, PII flags, inferred relationships, candidate keys, grain evidence, quality findings, each dataset stamped with when it was profiled and each distinct count marked exact or approximate. This is the expensive artifact. On our own project it covers 201 objects across twelve datasets and cost roughly 22 minutes and 5.12 GB scanned to build. Committing it means the next engineer, the next Claude session and CI do not pay that again.
snapshot.json holds the state a human accepted as correct. That is what makes drift a meaningful word. Warehouses change constantly; change is not news. Drift is change measured against something your team agreed to, and it needs that agreement to exist somewhere durable and reviewable.
We have poisoned our own baseline, so this is not theoretical. Snapshotting from a partial exploration produced 1,548 columns reported as newly added and three critical findings for tables that had never been dropped. Nothing was wrong with the warehouse. Everything was wrong with what we had told dex to compare against. That is worse than a stale baseline, because false findings do not sit quietly in a corner; they bury the real ones. Past 50 objects explore map profiles the top 25 by rank and records the rest as metadata only, so the discipline is explore map --full before you snapshot anything large, and the envelope reports how many objects it has column detail for against how many exist.
Memory that cannot describe its own coverage is a confident guess with a timestamp on it.
Something that stops it
select email from customers is refused. select count(distinct email) from customers runs, because a statistic is not a value.
That is the query firewall, and it applies to SQL Claude wrote itself, which is the only place it matters. Aggregates over flagged columns have to be measuring rather than value-carrying, so AVG(LENGTH(...)) is fine and MIN is not. A PII flag is never removed by evidence: value-shape statistics computed during profiling can move its confidence in either direction, and missing evidence moves nothing. The single thing that clears a flag is a human's config entry.
Cost gets the same treatment. Nothing dex runs touches a metered warehouse without a ceiling. Billed work returns an estimate and waits, in that connector’s own unit: bytes on BigQuery, warehouse-seconds on Snowflake and Databricks, compute-seconds on Redshift, database-seconds on Postgres and ClickHouse. A server-side cap sits behind that as a second brake. The daily ceiling gets asked once, on the first billed command in a project that has never decided, and then recorded, so an unbounded day is something somebody chose rather than the default nobody noticed. The two connectors that bill no dollars are gated anyway, because an unbounded scan against a production Postgres primary costs somebody something even when no invoice appears.
There is one place the guarantee does not hold, and we say so on every affected result rather than letting you assume otherwise. When a hosted dbt Cloud Semantic Layer executes a query, dbt Cloud owns the warehouse connection and runs it server-side, so no estimate and no byte cap are available to us.
Builds run against a dev target. Production-looking targets are refused before the preflight, not after it.
All of this lives in the engine. A rule in a prompt is something Claude weighs against everything else in the prompt. A rule in the engine is not available to weigh.
Which is what actually buys you the twenty minutes where you are not watching.
Decisions made, information kept
When maintenance finds drift, dex tags what it proposes.
Some of it is mechanical. Schema drift on a staging model dex scaffolded gets re-scaffolded from the drifted source: high confidence, still a diff you read, because a scaffold cannot know about the logic you hand-added three months ago.
The rest is advisory, which means dex has found the problem and is handing you the decision rather than a fix. It cannot dedup your warehouse. It cannot tell you whether a new 'refunded' status belongs inside net revenue. Nobody should want it to.
Two findings from that pass look similar and are not, and the difference is exactly the kind of thing that decides whether the next hour is wasted. key_lost_uniqueness means a key that was measured unique is not any more: something changed in your data, and you go upstream. declared_grain_not_unique means a grain your project declares does not hold and never did: nothing changed at all, your project has been asserting something false, and the fix is to the declaration. Same symptom, opposite ends of the stack.
It goes further than you would expect in declining to guess. Where a model already declares a composite grain covering the column, dex will not propose a column-level uniqueness test on it, and the warning instead names the combination, so you can tell “re-baseline, this is still the grain” from “something was relying on that column by itself.”
What a metric means is a claim your company makes about itself. What counts as revenue, who counts as active, which timestamp defines the month. Those questions do not get easier as models get better, because they were never questions about data.
The work worth automating is not answering them. It is making sure they arrive well-posed, with the evidence attached, ranked by how much depends on them, and few enough that somebody actually reads them.
At team size
One engineer gets an obvious version of this: you open a session with evidence instead of a blank slate, and you spend your afternoon on the definition rather than on rediscovering the warehouse.
What changes with five is that the decisions stop living in people.
A PII adjudication is a file with a reason in it. A budget is a file. A convention somebody argued about and settled is a file. So the boundaries are identical regardless of who ran the agent, on which model, in which week, and a new hire’s first session inherits them without being told. The evidence works the same way: once one person has established what a key actually is, that answer is in the branch everybody else pulls.
Then the constraint moves, and this is the part teams tend to discover late. Once agents author faster than people can read, review is the scarce resource. A workflow that emits a notification per finding burns through it in about a week, after which the channel is muted and you are worse off than before you had the agent. What has to reach a reviewer is a short list, each item carrying the evidence that produced it, each already sorted into something mechanical or something that is theirs to decide.
Dex Enterprise
Dex Enterprise is the workflow around the open-source engine, and it picks work up from the two places analytics work actually starts: people who ask for something, and systems that change underneath you.
A request gets grounded in the current project and warehouse before anything is authored, written as model SQL, tests, documentation or semantic definitions, validated against a dev target, and delivered as a draft pull request with the evidence attached and the original ask preserved next to it.
Maintenance runs on a schedule against the baseline your team accepted. A changed schema, a broken key, a semantic definition that no longer matches becomes either a proposed change or an advisory naming the decision an owner has to make. Findings are grouped and fingerprinted, so a known issue does not arrive fresh every night until everyone stops reading.
The review contract is the product. It runs in the CI environment your organization already controls, on your identities, your warehouse permissions, your inference provider, your retention policy. It opens draft changes and it does not merge them. Repository rules, required reviewers, workflow configuration and access policy stay where they are.
The line around unattended work is drawn in the same place the open-source engine draws it. Model SQL, tests, documentation and semantic definitions can be proposed, in the paths you configure. Configuration, dependencies, governance and design judgment stay advisory. That boundary is not a phase-one limitation waiting to be lifted; it is the difference between an agent that accelerates your team and an agent operating independently inside your systems.
Dex Enterprise is being built with design partners and is not generally available yet. The engine underneath it is Apache-2.0 and readable, which means you can audit the analytics engineering that would run before you evaluate anything wrapped around it.
Where to start
If you want to see the workflow before connecting a real warehouse, start with dex demo. It creates a local DuckDB warehouse with no credentials or network connection, seeded with the bad join above, a doubled batch, and a broken cast. Then ask Claude to run dex explore map or dex explore relationships --verify; it will have evidence to work from before it proposes a model.
Then install it into whatever agent setup you already have:
npx skills add exmergo/dex
(Inside Claude Code, /plugin marketplace add exmergo/exmergo-agent-plugins then /plugin install dex@exmergo gets you the same three skills as /dex:explore, /dex:transform and /dex:maintain, triggering on intent.)
And if you are running a data team, where the problem is drift nobody catches until a dashboard breaks and review capacity you cannot expand by hiring, take a look at Dex Enterprise.
