> ## Documentation Index
> Fetch the complete documentation index at: https://docs.exmergo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# dbt projects: what dex reads and writes

> What dex reads from a dbt project, how it writes changes back as reviewable diffs, and how it runs dbt build, deps, and parse against a dev target only.

The dbt project is the source of truth for the transformation model. dex
maintains no parallel model: it loads the project, reasons over it together with
warehouse introspection and the `.dex/` cache, and writes changes back into the
source files as reviewable diffs.

It is not dex's only write surface. A semantic layer can own its own definitions
on the semantic axis, and `semantic ossie define|update|plan` followed by
`transform apply` writes the exact documents named in `semantic.ossie.files`,
confined to the repository and byte for byte. That surface shares this one's plan
and apply spine and none of the path families below, which is why it is described
in [�3](/dex/semantic-layers/overview) rather than here.

## What dex reads

* `dbt_project.yml`: the project name, profile name, and the six authored path
  families (`model-paths`, `macro-paths`, `snapshot-paths`, `seed-paths`,
  `test-paths`, `analysis-paths`), each defaulted the way dbt defaults it when
  the key is absent.
* The source files under those families, scanned for the suffixes each one can
  hold: `*.sql` / `*.yml` / `*.yaml` under the model, macro, snapshot, test and
  analysis paths, and `*.csv` / `*.yml` / `*.yaml` under the seed paths. Together
  they are the editing surface (model SQL, `schema.yml`, dbt semantic models,
  macros, snapshots, seeds, singular and generic tests, analyses). A file dex can
  author but does not load would hash as absent, so a later edit to it would
  register as a create and the apply after it would conflict on a file nobody
  touched; the scan covers every family for that reason, not for completeness.
* `target/manifest.json` when the project has been compiled; a fresh project
  loads fine without one.
* `profiles.yml` (searched the way dbt searches: `$DBT_PROFILES_DIR`, the project
  directory, `~/.dbt`), but only to resolve a target's **name and adapter type**.
  Connection fields, credentials included, never leave the engine.
* `dbt_packages/` when packages are installed, each package loaded as the dbt
  project it is, so a package that configures its own path families is read the
  way dbt reads it.

### The reference index

One projection over all of the above answers "where is this name used", and
`transform references` returns it. Three things about it are worth knowing before
relying on it.

**It reads calls, not text.** Jinja is scanned for the calls it contains, so a
`var()` read inside a macro body is a use of that var, a `{% if %}` block is not a
blind spot, and the two-argument `ref('package', 'model')` form resolves to the
model rather than to the package.

**An argument dex did not read is `indeterminate`, never absent.** A
`{{ ref(var('x')) }}` names a model only dbt can know, and a dimension whose
`expr` is a computed expression names a column dex will not guess at. Both are
reported by file and line with no name attached, because the fact is about the
call site and not about any one target. Collapsing "dex could not resolve this"
into "there is nothing here" is how a use goes missing, so the two stay apart.

**It states whether it believes itself complete.** `completeness` is `complete`
only when every reason to doubt the answer has been ruled out, and `limits` names
each remaining one: an unresolved reference, packages declared but not installed,
`sqlglot` absent on a column query, a file that would not parse. A completeness
claim that is not complete is worse than no claim, because it is the one a caller
acts on without checking.

Names are matched at the grain the question allows. A model, macro, var, source,
metric, entity, dimension or measure name is exact. A bare column name is matched
across the project and labelled `name_matched`; a qualified `model.column` is
resolved through the `ref()` graph, with occurrences outside that lineage still
listed and labelled `same_name_elsewhere` rather than dropped. A dotted name reads
as a source before it reads as a qualified column, since `raw.orders` is the more
common thing to ask about.

A seed contributes its header row and never a data row: the header names columns,
which is what an index is for, and the rows are project data, which never enters
agent context.

The project is discovered automatically (the repo root, or a unique child
directory holding a `dbt_project.yml`); `dbt_project_dir` in `.dex/config.yml`
pins it when discovery would be ambiguous. Absent a dbt project, explore still
works, writing only to the `.dex/` cache. dbt authoring and `maintain
reconcile`'s mechanical edits do need one, since dbt is what they edit and diff.
The semantic axis does not: a repository whose only declarations are native
semantic documents authors them through `semantic ossie`, and `maintain` still
fingerprints that layer and runs its free drift axes against it.

dbt is the format dex ships, not the only one it can read. `maintain`'s four
detection commands read whichever format `project.format` names, so a host whose
models are not a dbt project can still be a drift baseline; `transform` and
`maintain reconcile`'s mechanical edits are dbt throughout. See
[�4](/dex/reference/project-formats) for the seam and how to name another format.

## How dex writes

Every proposed change is a **plan**: the agent-authored file contents, validated
by the engine, pinned to the sha256 of each file they would change, and stored
under `.dex/plans/`. Applying a plan re-hashes every file first:

* Hash matches (or the file is a clean create): apply, all-or-nothing.
* File already carries the proposed content: a no-op, not a conflict.
* Anything else means a human edited the file after the plan was made. That is a
  **conflict**: nothing is written, the divergence is surfaced as a diff, and the
  caller either re-plans against current state or overrides with an explicit
  `--confirm`.

Each edit carries an `op`: `upsert` (create or update, the default) or `delete`.
A delete removes a file as a first-class reviewable diff, pinned to the file's
hash like any other edit, so it obeys the same conflict rule: a file a human
touched after planning is never silently removed. Deletes are guarded as a unit:
a plan is refused if any file that survives it still `ref()`s a deleted model, so
the post-change project is proven to have no dangling reference before the plan
is stored (and, when dbt is available, the same post-deletion tree is confirmed
by dbt's own parser). The guard reads the reference index described under
[What dex reads](#what-dex-reads), not a text search, so it resolves a
two-argument `ref()`, does not mistake a seed's data row for source, and warns
rather than refusing on a reference it could not resolve. A rename is expressed as one plan: delete the old model,
create the new one, and update every referrer, validated together.

Human edits to dbt are authoritative by construction; dex holds no competing copy
to overwrite them from. Writes are confined to the six authored path families
plus the project-root manifests dbt keeps there (`dbt_project.yml`,
`profiles.yml`, `packages.yml`, `dependencies.yml`); path escapes are refused. A
native semantic document is not in this surface and is never reached through it:
it carries its own confinement rule, the exact paths the semantic axis declares.
Within the surface, an edit's kind and its location have to agree: a snapshot
belongs under the snapshot paths and nowhere else, a seed under the seed paths, a
macro under the macro paths, a singular or generic test under the test paths, an
analysis under the analysis paths, and model SQL and semantic YAML under the
model paths. `schema.yml` is the one kind several families admit, because dbt
expects a seed's column types, a snapshot's tests, a singular test's severity and
an analysis's description declared beside the thing they describe. Filing a kind
in the wrong family is refused at plan time naming both fixes (move the file, or
relabel the kind), since dbt would otherwise parse a snapshot or a test as a
model, or never load a seed or an analysis at all.

A model, a snapshot and a seed each build a relation dbt names after the file and
each is `ref()`-able, so all three count as nodes: deleting one is guarded
against surviving references exactly like deleting a model, and all three are
what `maintain` fingerprints as the transformation layer. A macro is not a node
and never was one.

**Neither a singular test nor an analysis is a node here, and the word is worth
being careful with.** dbt calls a singular test a node (`test.<project>.<name>`)
and it is one in dbt's graph, but it builds no relation and nothing can `ref()`
it, so it is not one of "the things this project builds": counting it would put a
name into the drift baseline that no warehouse table will ever back. An analysis
is further out still, compiled by `dbt compile` into `target/compiled/`, never
built, and absent from a `dbt build` entirely. Both are loaded so they can be
authored and hashed like anything else; neither enters the transform-layer model
list, and deleting either raises no dangling-reference guard because there is
nothing that could dangle.

dex never builds to a non-dev target, and a delete only ever removes a file from
the repo, never a relation from the warehouse.

## Running dbt (build, deps, parse)

Every dbt subprocess runs with its working directory pinned to the project dir, so
relative paths in `profiles.yml` and anywhere else resolve against the project, not
the caller's shell cwd. When the project declares packages (`packages.yml`, or a
`dependencies.yml` with a `packages:` key) and `dbt_packages/` is missing or empty,
`transform build` runs `dbt deps` automatically before building; `transform deps`
is the explicit install/refresh. Package installation writes only `dbt_packages/`
and the lockfile inside the project and spends nothing against the warehouse, so it
runs without the cost gate.

Semantic plans are validated up to and including dbt's own parser before they are
stored: dex copies the project (minus warehouse files, target, and logs) into a
throwaway directory, overlays the proposed YAML, and runs `dbt parse` there, so
nothing the parser writes touches the real project. When dbt is unavailable the
parse degrades to a warning rather than a hard failure.

Source: [`references/dbt-project.md`](https://github.com/exmergo/dex/blob/v1.14.1/references/dbt-project.md) ([edit on main](https://github.com/exmergo/dex/edit/main/references/dbt-project.md)). This page is generated; change the source, not the site.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.