Skip to main content
All writes go through one endpoint. The action field selects the operation.
Node actions: 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.
Response:
_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 as null and completedAt is 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.
A closed phase cannot hold open work. Reopening a job whose phase is 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.
Two things to know before deleting a node that has children:
  • 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.
The LLM guide currently says that delete does not cascade and that a restored child comes back orphaned. The behaviour above is what the API does today (the parent link is a foreign key with 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 original id, type, parentId, and data. Identify it by nodeId (restores the most recent deletion of that node) or by a specific changelogId.
  • 409 if a node with that id already exists (nothing to restore). 404 if 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 mix add, update, delete, and addIteration.
How a batch behaves:
  • 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. addIteration entries 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 id starting with draft- and use that as the parentId of a later mutation. It is remapped to the real ID on apply. Do not send other client IDs. Any id that does not start with draft- 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 500 reports what it undid.
Read partial before you retry:
  • Absent: nothing from the batch survives. rolledBack says how much was undone. Fix the cause and retry the whole batch.
  • Present: nothing was rolled back (rolledBack is { "nodes": 0, "iterations": 0 }) and the batch is partially applied. Reconcile from GET /nodes and do not retry.
Compensation is only safe while the batch has applied nothing but creates. Once an 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 any mutate 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: true response 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.
The key is a header. An idempotencyKey field in the request body is ignored: two requests with the same body key both create a node. The MCP server’s idempotencyKey tool argument is fine because the shim turns it into the header.
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 iteration on a goal or maturity on a task is rejected with 'iteration' can only be set on job or task nodes, not on goal.
  • Kano values: must-have, incremental, delighter. performance is accepted as an alias for incremental and 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, or open to reopen (delivered is accepted but deprecated)
  • Task kind: memo or do-it, with urgent only on a do-it task
  • External references (externalRefs): any node type, at most 50 entries, no system + key pair twice, with only system, key and an https:// url. A rejection’s _meta._tip restates the shape. See External references.
  • Demand (demand): needs only, at most 25 entries, each with who and an at date, plus optional revenue, size, source and ask. A rejection’s _meta._tip restates 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.
Validation errors return 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 from GET /tier-status (limits.node_cap). It is a soft ceiling:
  • An add or batch from under the cap is allowed to overshoot, so a multi-node branch is never stranded half-written.
  • At or over the cap, a new add or batch returns 403 { "error": "node_cap_reached", "current", "node_cap", "delete_to_resume" }. update and delete are never gated.
  • The add that crosses the cap succeeds, and its response carries _meta.advisory { "type": "node_cap_reached", ... }.