action field selects the operation.
add, update, delete, restore, batch, classify. Phase actions (addIteration, updateIteration, deleteIteration, reorderIterations) are on the Iterations page. Project actions (addProject, renameProject) are on the Projects page.
Send an Idempotency-Key header on every write so a retry never double-applies. See Idempotency.
Add a node
Creates a node with a generated ID._meta.focus.viewCommand can be passed straight to POST /view-command to show the user what you just did. A job or task created without an iteration lands in Later and the response says so in _meta._tip.
Parent rules
A task is standalone work that does not fit the tree (bugs, polish, captures). It is created without a
kind, and the API classifies it as memo or do-it asynchronously. Re-read the node, or call { "action": "classify", "nodeId": "task-1" } to classify it on demand.
Update a node
PATCH semantics. Include only the fields you want to change. The rest are preserved.parentId can be sent alongside data to reparent a node. The new parent must be the right type.
Arrays are replaced, not merged. To change one entry of externalRefs, serves, demand or blockedBy, send the whole new list. Moving a bet under a need it already serves also removes that need from serves.
Status
done means live in production. That is the whole gate. There is no separate verification step.
- To close a job or task:
{ "status": "done" }. - To reopen one:
{ "status": "open" }. It is stored asnullandcompletedAtis cleared. An explicit{ "status": null }does the same. "delivered"is deprecated. It is still accepted, but treat it as “not done yet” and do not set it for new work.
done returns 400 with a _meta._tip that names the phase and both ways out: reopen into a live phase in the same call ({ "status": "open", "iteration": "Later" }), or reopen the phase first. If an update leaves a job open inside a done phase by another route, the job is moved to Later and _meta._tip says so.
An unknown nodeId returns 400 { "success": false, "errors": ["Node with id 'job-999' not found"] }.
Delete a node
Hard delete.- Everything beneath the node is removed too. The database enforces the parent link, so the children and grandchildren go with it. The response lists the direct children in
orphanedChildren, but they are no longer in the project. - Only the node you named is written to the changelog. Its descendants are not, so they cannot be restored. If you want a subtree recoverable, delete the leaves first, one call each, or use a batch.
ON DELETE CASCADE), and the guide is being corrected. This page documents observed behaviour on this point.
Restore a node
Recover a hard-deleted node from the changelog. It returns with its originalid, type, parentId, and data. Identify it by nodeId (restores the most recent deletion of that node) or by a specific changelogId.
409if a node with that id already exists (nothing to restore).404if no deletion is on record, which is also what you get for a node that was removed as a descendant of another delete.- The original parent must still exist. If it was deleted too, restore the parent first. Restoring under a missing parent currently fails with
500 { "success": false, "errors": ["Failed to restore node"] }. - Restore does not cascade. It brings back the single node, not its former children.
Batch mutations
Several operations in one request. A batch may mixadd, update, delete, and addIteration.
- Validation is all-or-nothing. Every mutation is validated before anything is written. One invalid mutation rejects the whole batch with
400 { "success": false, "errors": ["mutations[1]: ..."] }. - Phases first.
addIterationentries are applied before any node mutation, so a job can reference a phase created in the same batch. - Temporary IDs. To reference a node created earlier in the same batch, give it an
idstarting withdraft-and use that as theparentIdof a later mutation. It is remapped to the real ID on apply. Do not send other client IDs. Anyidthat does not start withdraft-is used as the real primary key. - Apply is sequential, not a single transaction. If a write fails part-way, the API compensates where it safely can: it deletes the nodes and phases this batch created, newest first, and the
500reports what it undid.
partial before you retry:
- Absent: nothing from the batch survives.
rolledBacksays how much was undone. Fix the cause and retry the whole batch. - Present: nothing was rolled back (
rolledBackis{ "nodes": 0, "iterations": 0 }) and the batch is partially applied. Reconcile fromGET /nodesand do not retry.
update or a delete has run, undoing the creates could take pre-existing rows with them, so the API stops, changes nothing further, and tells you. Two cases it cannot cover: a function timeout mid-compensation (you see no response at all, so treat it like partial), and a timeout after the write succeeded (this is what the Idempotency-Key fixes). Smaller batches shrink the blast radius.
Idempotency
Make anymutate call (single or batch) safe to retry by sending an Idempotency-Key header: an opaque string you generate per logical operation (a UUID is ideal).
- Replay. A second request with the same key and the same body returns the original response unchanged, with an
Idempotent-Replayed: trueresponse header, instead of applying again. - Conflict. The same key with a different body returns
409 { "success": false, "errors": ["Idempotency-Key already used with a different request body"] }. Generate a fresh key per operation. - In flight. If the first request is still running, a retry returns
409(“still in progress, retry shortly”). - Window. Keys are remembered for 24 hours. Scope is per user.
addProject is the one action that does not honour the key. Pass an explicit project.id instead, so a repeat fails on the existing project rather than creating a second one.
Without an Idempotency-Key, retries are not deduplicated. Re-read with GET /nodes or search and re-submit only what is missing.
Validation
The API validates every mutation before applying it:- Label is required on all nodes.
- Parent must exist and be the correct type (need under goal, bet (
approach) under need, job under bet). - Fields must match the node type. Setting
iterationon a goal ormaturityon a task is rejected with'iteration' can only be set on job or task nodes, not on goal. - Kano values:
must-have,incremental,delighter.performanceis accepted as an alias forincrementaland stored as sent. - Size values:
skateboard,vespa,car,truck,antonov - Maturity values:
mvp,releasable - Bet status (
approachStatus) values:development,validation,resolved,retired - Status values:
done, oropento reopen (deliveredis accepted but deprecated) - Task kind:
memoordo-it, withurgentonly on ado-ittask - External references (
externalRefs): any node type, at most 50 entries, nosystem+keypair twice, with onlysystem,keyand anhttps://url. A rejection’s_meta._tiprestates the shape. See External references. - Demand (
demand): needs only, at most 25 entries, each withwhoand anatdate, plus optionalrevenue,size,sourceandask. A rejection’s_meta._tiprestates the shape. See Demand. - Served needs (
serves): bets only, at most 5 existing need ids, no repeats, never the bet’s own parent. See A bet that serves more than one need.
400 { "success": false, "errors": ["'kano' must be one of: must-have, incremental, performance, delighter"] } plus _meta.guide. Access and tier refusals use { "error": "<code>", "message": "..." } instead. See Authentication.
Free-tier node cap
The free Builder tier caps nodes per project. Read the live number fromGET /tier-status (limits.node_cap). It is a soft ceiling:
- An
addorbatchfrom under the cap is allowed to overshoot, so a multi-node branch is never stranded half-written. - At or over the cap, a new
addorbatchreturns403 { "error": "node_cap_reached", "current", "node_cap", "delete_to_resume" }.updateanddeleteare never gated. - The
addthat crosses the cap succeeds, and its response carries_meta.advisory { "type": "node_cap_reached", ... }.

