Definitions API
The whole authoring surface as files, and the safety-railed way to apply an edited copy of it back. The three endpoints the CLI is a thin client of.
| Method & path | What it does |
|---|---|
GET /api/v1/definitions | The whole surface as { files: { path: contents } }. |
POST /api/v1/definitions/diff | What applying these files would change. Changes nothing. |
POST /api/v1/definitions/apply | Diff, validate, rehearse, apply — all or nothing. |
The file formats are documented at definitions repo layout. Authoring artifacts only: people, events, runs, sends and money are runtime and never appear, and neither do credentials.
Pull
GET /api/v1/definitions
{ "files": {
"flows/trial_journey.json": "{ … }",
"emails/0042-welcome-aboard.md": "---\nid: 42\n…",
"settings.yml": "tunables:\n …" } }
Canonically formatted — one key order, one indent, one trailing newline — so a pull straight after a pull is byte-identical, and an unrelated push doesn't churn every file in the repo with reordered keys.
Diff and apply
POST /api/v1/definitions/apply
{ "files": { "flows/trial_journey.json": "…", … },
"prune": false,
"force": false }
Send the whole managed tree, not a subset — absences are how
removals are expressed, and a partial tree would read as a mass deletion. (With
prune: false, the default, absences are only reported.)
The response
{ "changes": [
{ "path": "flows/trial_journey.json", "kind": "update", "type": "flow",
"label": "trial_journey",
"summary": ["added 12-send-recap (id 12 minted)"],
"errors": [], "warnings": [] } ],
"changed": 1,
"dry_runs": [ { "flow": "trial_journey", "triggered": true, "steps": 6, "problems": [] } ],
"notices": [],
"parse_errors": [],
"blocked": false,
"ok": true,
"applied": 1 }
| Field | Meaning |
|---|---|
kind | create, update, unchanged, or absent (on the instance, no file in the tree). |
summary | Human-readable lines. Steps are named by handle; an email re-point is described in the chooser's terms. |
dry_runs | One per changed flow, rehearsed against a real person with details that satisfy that flow's own trigger. |
notices | Definitions already on the instance that don't validate. Not caused by this push, and not blocking it. |
parse_errors | Files that couldn't be read at all. Always blocking, never forcible. |
blocked | Whether apply would refuse. |
Matching, not filenames
Records match by identity — a key, or the id in a file's frontmatter
— never by filename. Renaming an email changes its filename, and a rename is not
a delete.
What blocks
- Any error in something this push changes. Never forcible.
- A file that can't be parsed. Never forcible.
-
node_id_freedornode_id_repurposed— a step wearing an id that belonged to a different step. Warnings in the validator, blocking here, because the failure is silent: whoever is parked on that id moves with it.force: trueoverrides these and only these.
A definition already broken on the instance appears in notices and
doesn't block — you can't fix it by not pushing, and one broken record shouldn't
wall off every unrelated change from then on.
Order of operations
Templates, fields and products first; then emails; then segments, guards and sequences; then flows; then settings. So a commit that adds an email and the step using it lands in one push. Everything happens in one transaction — a push that half-landed would leave the instance in a state no file describes.
Pruning
prune: true also removes what's on the instance but not in the tree.
Emails are archived rather than destroyed — an email that has
been sent is part of the record of what went out — and a flow people have run
through refuses regardless, because their history is theirs.
Live updates
A successful apply pings every surface an admin page watches, so changes appear on an open page without a refresh.
See also: CLI · File layout · MCP server