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

VerbPathMeaning
GET/healthLiveness: { ok: true, surface: "authoring" }
GET/catalogEvery 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/guideThe authoring guide (document format, envelope model, expressions, frames)
GET/workflowsStored 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:

StatusMeaning
401Missing/bad bearer (+ WWW-Authenticate)
404Unknown path, unknown workflow id, unknown node type
405Known path, wrong verb
409VERSION_CONFLICT — someone saved after your baseVersion; re-read or pass force: true
422INVALID_WORKFLOW — the graph failed validation; the body carries coded issues
423The 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.