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.
Projects
Section titled “Projects”A project is a dbt Core project scoped to one workspace. It holds:
- A pinned
dbtVersion(default1.9, informational for now). - A set of environments — each maps a name (e.g.
dev,prod) to a database connection + target schema, withthreads(1–16, default 4), optional dbtvars, and optional ownership (see Personal environments). - A
defaultEnvironment(defaultdev). 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.
Personal environments
Section titled “Personal environments”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 schemadbt_<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 showsPersonal — 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
defaultEnvironmentnor 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-environmentsaves your default development environment for the project (the editor/console env pickers persist here;""clears back to Auto). Project reads carry it back asmyDevEnvironment.
File IDE
Section titled “File IDE”Every file in the project is editable from the Transforms explorer:
dbt_project.yml # project config — name, model defaultsmodels/ staging/ # 1:1 source cleanup, materialized as views schema.yml # sources + staging model tests stg_<src>_<entity>.sql marts/ # business-facing models, materialized as tablesseeds/ # small CSV reference data (dbt seed)macros/ # Jinja macrossnapshots/ # SCD2 snapshotstests/ # singular SQL testsReads 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.
Studio-style editor
Section titled “Studio-style editor”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.
Running models
Section titled “Running models”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.
Local CLI runs
Section titled “Local CLI runs”The @makoai/cli can run dbt from a local checkout, including uncommitted files:
mako loginmako dbt run -s ordersmako dbt build -s +orders --env stagingmako dbt build -s orders --full-refresh --no-defermako dbt test -s ordersrun, 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.
Jobs & schedules
Section titled “Jobs & schedules”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.
Run history & artifacts
Section titled “Run history & artifacts”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 |
Access control (RBAC)
Section titled “Access control (RBAC)”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.
Runner security
Section titled “Runner security”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.
Lineage
Section titled “Lineage”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 |
Project rules (.makorules.md)
Section titled “Project rules (.makorules.md)”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:
- What you tell the agent in the conversation
.makorules.md- Workspace instructions (Settings → Prompt)
- The
dbtsystem skill - 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.
Example
Section titled “Example”# 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.