The Authoring REST API
Everything an AI client can do over MCP is also plain HTTP. The Authoring API lives on your Nucleus at:
http://localhost:48170/api/svc/authoring-api
Auth: every request carries Authorization: Bearer flowdrome-authoring — that token is the
API workflow’s own trigger authentication, so you change it by editing the trigger (like any
webhook). Requests without it answer 401 with a WWW-Authenticate header.
Routes
| Verb | Path | Meaning |
|---|---|---|
GET | /health | Liveness: { ok: true, surface: "authoring" } |
GET | /catalog | Every node type — type, label, category, description |
GET | /catalog/{type} | One node’s full contract; add ?config=<url-encoded JSON> to resolve config-dependent ports |
POST | /validate | { nodes, edges } → { ok, issues[] } — never saves |
GET | /guide | The authoring guide (document format, envelope model, expressions, frames) |
GET | /workflows | Stored workflows — id, name, version, updatedAt |
GET | /workflows/{id} | One workflow document |
POST | /workflows | { name, nodes, edges } → validate + auto-layout + checkpoint-save |
PUT | /workflows/{id} | Replace a graph; { baseVersion?, force? } for concurrency; optional name renames the workflow in the same save; lenient: true saves through draft-state validation issues (an unwired trigger mid-edit) and returns them as issues instead of a 422 — the platform’s surgical edit tools use this for single-step edits |
POST | /test-runs | { id } or { doc }, optional { input } → run it, verifiable trace back |
Error semantics are real API semantics, produced by the workflow’s own graph paths:
| Status | Meaning |
|---|---|
401 | Missing/bad bearer (+ WWW-Authenticate) |
404 | Unknown path, unknown workflow id, unknown node type |
405 | Known path, wrong verb |
409 | VERSION_CONFLICT — someone saved after your baseVersion; re-read or pass force: true |
422 | INVALID_WORKFLOW — the graph failed validation; the body carries coded issues |
423 | The workflow is locked — view-only until unlocked |
A worked flow: discover → validate → create → test
1. Find nodes and learn their contracts. Never guess a config key — describe returns the
node’s configSchema with the exact enums:
curl -s http://localhost:48170/api/svc/authoring-api/catalog \
-H "Authorization: Bearer flowdrome-authoring"
# A Switch derives its OUTPUT PORTS from its rules — pass your config to see the real handles:
curl -s "http://localhost:48170/api/svc/authoring-api/catalog/logic.switch?config=%7B%22rules%22%3A%5B%7B%22port%22%3A%22hot%22%2C%22path%22%3A%22n%22%2C%22operator%22%3A%22greaterThan%22%2C%22value%22%3A10%7D%5D%7D" \
-H "Authorization: Bearer flowdrome-authoring"
# → outputPorts: [{ id: "hot", … }, { id: "default", … }], each with a description of when it fires
2. Validate before saving (optional — create refuses invalid graphs anyway):
curl -s http://localhost:48170/api/svc/authoring-api/validate \
-H "Authorization: Bearer flowdrome-authoring" -H "content-type: application/json" \
-d '{"nodes":[{"id":"a","type":"not.a.real.type","label":"A","category":"x","config":{}}],"edges":[]}'
# → { "ok": false, "issues": [{ "nodeId": "a", "code": "NODE_UNKNOWN_TYPE", … }] }
3. Create. The server validates, auto-lays-out the canvas, and checkpoint-saves a version:
curl -s http://localhost:48170/api/svc/authoring-api/workflows \
-H "Authorization: Bearer flowdrome-authoring" -H "content-type: application/json" \
-d '{
"name": "Hello time",
"nodes": [
{ "id": "trg", "type": "input.manual", "label": "Start", "category": "trigger", "config": {} },
{ "id": "log", "type": "utility.console", "label": "Log", "category": "utility", "config": { "message": "now: {{ $now }}" } }
],
"edges": [ { "id": "e1", "sourceNodeId": "trg", "sourcePort": "output", "targetNodeId": "log", "targetPort": "input" } ]
}'
# → { "id": "wf_…", "name": "Hello time", "version": 2, "issues": [] }
4. Test-run and verify behavior, not just success:
curl -s http://localhost:48170/api/svc/authoring-api/test-runs \
-H "Authorization: Bearer flowdrome-authoring" -H "content-type: application/json" \
-d '{ "id": "wf_…" }'
The trace answers the questions that matter: every node’s status (an untaken branch reads
skipped), activePorts (which lanes a Switch or routed trigger actually fired), a capped
output preview per node, and the run’s console feed:
{
"state": "succeeded",
"durationMs": 12,
"nodes": [
{ "id": "trg", "type": "input.manual", "status": "succeeded" },
{ "id": "log", "type": "utility.console", "status": "succeeded", "output": { "message": "now: …" } }
],
"console": [ { "node": "Log", "message": "now: 2026-07-18T…" } ]
}
Building error handling and iteration
Frame membership and per-item execution are node-level fields beside id/type/config —
not config keys:
{ "id": "risky", "type": "output.http-request", "…": "…",
"parentTry": "try1" } // runs INSIDE the error-handling.try frame "try1"
{ "id": "each", "type": "processing.set", "…": "…",
"forEachItem": true } // runs once per item of an incoming list
parentLoop and parentThread work the same way for Loop and Thread-pool frames. Try wiring:
the inside node’s output wires to the frame’s result input; recovery wires from the frame’s
error port. Each port’s description in GET /catalog/{type} states its contract.