Skip to content

Transforms (dbt)

The Transforms section runs dbt Core projects directly inside your Mako workspace — a self-hosted dbt Cloud replica. Project files live in the workspace’s git repo (a dbt/ folder, same repo as consoles, notebooks, and apps) and execute as dbt subprocesses against your existing database connections. Job definitions (dbt/jobs/*.yml) and environments (dbt/environments.yml) are authoritative in git; Mongo holds only a derived, SHA-checked index for fast reads.

You get a file IDE, saved jobs with cron schedules, run history with artifacts, a DAG lineage view, and the AI agent’s Transforms mode that writes and verifies models for you.

A project is a dbt Core project scoped to one workspace. It holds:

  • A pinned dbtVersion (default 1.9, informational for now).
  • A set of environments — each maps a name (e.g. dev, prod) to a database connection + target schema, with threads (1–16, default 4), optional dbt vars, and optional ownership (see Personal environments).
  • A defaultEnvironment (default dev). Ad-hoc and agent-triggered builds resolve their target as: explicit selection → your saved per-user dev environment → your personal environment (when provisioned) → the project default. Production targets require an explicit job or selection.

Project names are unique per workspace. New projects are scaffolded with a standard dbt_project.yml, models/staging, models/marts, seeds/, macros/, and snapshots/ layout.

A personal environment is a per-developer build target so iteration never lands in a shared schema:

  • Auto-provisioned — POST .../environments/personal (also triggered from the editor) idempotently creates yours: same connection as the prod-like environment, private schema dbt_<slug> (slug = your email local-part, lowercased, [a-z0-9_]; name/schema collisions get a numeric suffix).
  • Claim / release from settings — each environment row in the project settings drawer has an Ownership dropdown (Shared, Personal — only me, or shows Personal — another user), so an existing environment can be claimed as personal or released back to shared.
  • Rules (validated on create, update, and GitHub import): at most one personal environment per user, and neither the defaultEnvironment nor the production (prodEnvironment) environment can be personal — both are resolved for other users too, so a private schema there would leak one developer’s scratch data into everyone’s builds.
  • Member+, not admin-only — provisioning your own personal environment only affects your own iteration target. It requires a user session (not an API key). Shared environment config stays admin-gated as usual.
  • Per-user dev preference — PUT .../projects/:projectId/my-environment saves your default development environment for the project (the editor/console env pickers persist here; "" clears back to Auto). Project reads carry it back as myDevEnvironment.

Every file in the project is editable from the Transforms explorer:

dbt_project.yml # project config — name, model defaults
models/
staging/ # 1:1 source cleanup, materialized as views
schema.yml # sources + staging model tests
stg_<src>_<entity>.sql
marts/ # business-facing models, materialized as tables
seeds/ # small CSV reference data (dbt seed)
macros/ # Jinja macros
snapshots/ # SCD2 snapshots
tests/ # singular SQL tests

Reads come from the workspace repo at your session branch (git listTree/readBlob); writes are commits on that branch, and every write is also captured in the shared version history. There’s one dbt project per workspace, scaffolded into dbt/ on first use — or imported by pointing at an existing dbt/dbt_project.yml in the repo.

Beyond the file IDE, the editor mirrors dbt Studio:

  • Live auto-compile of the model you’re editing.
  • Toolbar mirroring dbt Cloud: Preview (Cmd+Enter) · Compile · Build split-button. The Build menu carries the Build / Run / Test × graph-operator matrix: model, model+, +model, +model+.
  • Preview runs dbt show --select <model> --limit 500 --output json — a bounded, read-only SELECT that materializes nothing — and fills the Results grid with real rows. Because it never writes to the warehouse, Preview is allowed on every environment and is never recorded to run history. CSV / NDJSON export included.
  • Persistent bottom panel — Commands · Problems · Results · Compiled code · Lineage tabs and a status bar. Node outcomes (Build / Run / Test) live in Commands next to the invocation that produced them; Results is the data grid filled by Preview.
  • jinja-sql Monaco language support, a dbt version selector, and project create/import/settings drawers.

Three ways to execute dbt, all routed through the same validated runner:

Action What it runs Where
Preview dbt show --select <model> --limit 500 --output json Bounded read-only SELECT; renders rows in the Results grid. Allowed on every environment, never written to run history
Compile / Parse dbt parse or dbt compile --select <model> Renders Jinja, validates refs/sources without touching the warehouse
Run selection dbt build --select <model> Builds the model and its tests on the chosen environment
Command bar Any allow-listed dbt command dbt Cloud parity — free-form command bar

The command bar accepts a free-form command (an optional leading dbt is stripped), but every command is tokenized and validated against the same allowlist as saved jobs before it reaches the runner. The subcommand must be on the allowlist (run, build, test, seed, snapshot, compile, parse, source freshness, docs generate, deps, retry, show), and unknown flags are rejected. Commands are executed with spawn (no shell), and --select selectors on compile / run-select are pattern-checked.

The @makoai/cli can run dbt from a local checkout, including uncommitted files:

Terminal window
mako login
mako dbt run -s orders
mako dbt build -s +orders --env staging
mako dbt build -s orders --full-refresh --no-defer
mako dbt test -s orders

run, build, and test upload the checkout’s dbt/ folder to Mako and stream the run log; with git, only the files that differ from git merge-base HEAD origin/main are sent (staged, unstaged, untracked and deleted), laid over that commit on the server. The command exits with dbt’s exit code. Runs use your personal environment by default (created on first use); use --env <name> to select a shared development environment. The production environment is refused (403) even with --env — it is only built from main by a job — and so is another person’s personal environment. --no-defer disables deferral to production; --full-refresh applies to run and build (not test). Local runs need a mako login and at least the member role, because the uploaded project executes with the selected environment’s warehouse credentials. See also Working from a local checkout.

The API equivalent is POST /api/workspaces/:workspaceId/dbt/local-runs, followed by polling GET /api/workspaces/:workspaceId/dbt/local-runs/:runId; cancel with POST .../:runId/cancel. A local run is scoped to the requesting user.

A job is a saved list of dbt commands (build, test, seed, snapshot, source freshness, docs generate, with --select / --exclude / --full-refresh flags; --select and --exclude accept several space-separated selectors, e.g. run --select a b c --full-refresh) bound to an environment. Jobs can run:

  • Manually — trigger from the UI or the agent (after explicit user confirmation).
  • On a schedule — set a cron expression; the scheduler picks up due jobs.

Every execution produces a run record with per-node status, timing, and logs. Runs can be cancelled while in flight or retried. dbt’s standard artifacts are captured and downloadable per run:

Artifact Contents
manifest Full project graph (nodes, refs, sources)
runResults Per-node execution results and timing
catalog Column-level metadata from docs generate
sources Source freshness results

Transforms access is enforced by a pure policy (api/src/dbt/rbac.ts):

  • Reads (GET) — open to any member, including viewers.
  • File/run mutations (edit files, trigger runs, ad-hoc compile/run) — require member or above (viewers excluded).
  • Deployment-config changes (job create/edit/delete, project create/delete, PR merges) — require admin or owner. Connecting the workspace’s GitHub repo is workspace-level infrastructure now (/workspaces/:workspaceId/github/*), not a dbt-specific route.

dbt model code can call env_var(), so the runner does not inherit the API’s process environment. The dbt subprocess runs with an allowlisted base env (buildDbtBaseEnv) forwarding only what uv/python/dbt need; per-connection secrets are layered on top. This stops workspace members from exfiltrating server secrets (e.g. ENCRYPTION_KEY, AI_GATEWAY_API_KEY, DATABASE_URL) through a one-line model.

The lineage view renders the model DAG (nodes + edges) from the latest run that produced a manifest.json, overlaid with each node’s last run status from that same run. It’s the same dependency graph dbt builds from {{ ref() }} and {{ source() }}, so undeclared sources and broken refs show up here.

dbt routes are mounted under /api/workspaces/:workspaceId/dbt. Highlights (full schema in the REST API reference under the sidebar):

Method Endpoint Description
GET / POST /projects List / create projects
GET / PATCH / DELETE /projects/:projectId Get / update / delete a project
GET /projects/:projectId/files List project files
GET / PUT / DELETE /projects/:projectId/files/:path Read / write / delete a file
POST /projects/:projectId/files/rename Rename / move a file
POST /projects/:projectId/environments/personal Idempotently provision the caller’s personal environment
PUT /projects/:projectId/my-environment Save the caller’s default dev environment ("" = Auto)
GET / POST /projects/:projectId/jobs List / create jobs
PATCH / DELETE /projects/:projectId/jobs/:jobId Update / delete a job
POST /projects/:projectId/jobs/:jobId/trigger Run a job now
GET /projects/:projectId/runs List runs
GET /projects/:projectId/runs/:runId Get a run
POST /projects/:projectId/runs/:runId/cancel Cancel a running run
POST /projects/:projectId/runs/:runId/retry Retry a run
GET /projects/:projectId/runs/:runId/artifacts/:kind Stream an artifact (manifest, runResults, catalog, sources)
POST /projects/:projectId/compile Parse / compile a selection
POST /projects/:projectId/run-select dbt build --select a selection
POST /projects/:projectId/preview dbt show --select — bounded read-only row preview
POST /projects/:projectId/command Run an allow-listed free-form command
GET /projects/:projectId/lineage DAG nodes + edges from the latest manifest

Drop a .makorules.md file at the root of a dbt project and Mako’s agent will follow it on every turn — the same idea as .cursorrules, scoped to how your team writes SQL. .makorules (no extension) works too; .makorules.md wins if both exist.

It is an ordinary project file, so it is versioned with the project: it syncs from your repo, commits and pushes with the rest of your changes, and lives on the branch you wrote it on. Your uncommitted edits apply to your own agent turns straight away, so you can tune the rules and re-prompt without committing.

Precedence, highest first:

  1. What you tell the agent in the conversation
  2. .makorules.md
  3. Workspace instructions (Settings → Prompt)
  4. The dbt system skill
  5. Mako’s built-in dbt conventions

When a rule blocks what you asked for, the agent says so and cites the file rather than silently picking a side.

Size limit: the first 16,000 characters (~4k tokens) are injected. Past that the agent is told the file was truncated.

# SQL conventions for this project
- Every model starts with import CTEs (`with source as (select * from {{ ref(...) }})`),
one per upstream, then transform CTEs, then a single `select` at the bottom.
- Never `select *` outside an import CTE.
- Columns are `snake_case`; booleans are prefixed `is_` or `has_`.
- Money is stored in minor units and suffixed `_cents`.
- Every mart model needs a `unique` + `not_null` test on its primary key in
`schema.yml`, in the same PR.
- Never hardcode a schema or table name — always `{{ ref() }}` or `{{ source() }}`.

You don’t have to write it yourself: tell the agent a convention (“we never use select *”) and it will offer to record it for you.