POST/1.0/flows/:flowUuid/runs
Executes a Workbench flow synchronously and returns the full result. Required scope: workbench:flows:run (scopes reference).
The collection-shaped path (/flows/:flowUuid/runs) is intentional — each POST creates a new "run" resource, identified by executionId in the response. Future webhook + status-polling endpoints will share the same shape.
Path parameters
flowUuidstringRequiredThe flow's public id — a UUID. Copy it from the flow's dev drawer → Quickstart → Flow ID in Workbench. The key's workspace must own the flow.
Request body
inputobjectRequiredThe payload to feed the flow's input nodes. Discriminated by kind:
{ kind: "form", values: Record<string, unknown> }— for flows withinput-data/input-promptnodes. Thevalueskeys must match each input node'skeysetting (or its type default:"data","prompt").{ kind: "chat", messages: { role, content }[] }— for flows withinput-chatnodes. Roles are"system"/"user"/"assistant"/"tool". When a flow uses tools, resend the conversation it returned verbatim on the next turn — assistant messages may carrytool_calls(withcontent: nullfor pure tool-call turns) and tool responses userole: "tool"withtool_call_id. These fields round-trip intact, so the model keeps its tool memory across turns.
sourceobjectWhich version of the flow to run. Defaults to the current draft.
{ kind: "draft" }— current in-editor version. Default.{ kind: "revision", revisionId: string }— a specific saved revision.{ kind: "published", version?: string }— the most recently published revision, or a specific version tag.
For production integrations, prefer { kind: "published" } with an explicit version so the integration doesn't drift if the flow is edited.
timeoutMsintegerHard runtime ceiling in milliseconds. A default ceiling applies; max is 600,000 (10 minutes). Hitting this returns 504 timeout. Since there's no mid-run cost cap, the timeout doubles as a spend ceiling.
verbosebooleanDefault false. When true, the sync response includes the per-node timeline and the itemized cost summary — useful for debugging. The default slim response carries only the essentials: executionId, status, outputs, costSummary.total, durationMs, and an optional message. Ignored when stream is true (streaming always emits every node event).
streambooleanDefault false. When true, the response is text/event-stream SSE — one event per node lifecycle (node_start / node_complete / node_update / node_error) plus a terminal run_complete or run_error, closed by an event: done sentinel. Use for live token streaming or progressive trace UIs. Accept: text/event-stream is honored as an alternative trigger.
Response (200)
Default (slim) shape. costSummary.itemized and timeline only appear when the request set verbose: true.
executionIdstringStable identifier for this run. Use for support, log correlation, and (when available) status-polling.
status"ok" | "partial" | "error"ok — every node executed successfully. partial — some inputs were missing or output nodes aren't wired (message explains). error — terminal failure; details in the error envelope.
outputsRecord<string, unknown>The flow's emitted outputs, keyed by each output node's key setting (or its type default — "data" / "chat" / "prompt"). For example, a flow with a single output-data node and key "reply" returns { reply: ... }; a default-keyed output-data returns { data: ... }.
costSummaryobjectRoll-up cost in USD. Always { total: number }; the per-node itemized array is included only when verbose: true. The roll-up is what the per-key cost cap sums against.
durationMsintegerTotal wall-clock duration of the run.
messagestringOptional human-readable summary message — e.g. "Completed with missing input values" on a partial run.
timelineobject[]Per-node trace. Only present when verbose: true. Each entry: { nodeId, nodeType, status, durationMs, inputs?, outputs?, startTime?, endTime?, errorMessage? }. Empty array on early-failure verbose runs.
Example: form-shape flow
curl -X POST https://api.zerowidth.ai/1.0/flows/b3c1a2d4-5e6f-7081-92a3-b4c5d6e7f809/runs \
-H "Authorization: Bearer $ZW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": {
"kind": "form",
"values": { "data": "What is the capital of France?" }
},
"source": { "kind": "published" }
}'const res = await fetch(
"https://api.zerowidth.ai/1.0/flows/b3c1a2d4-5e6f-7081-92a3-b4c5d6e7f809/runs",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ZW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: { kind: "form", values: { data: "What is the capital of France?" } },
source: { kind: "published" },
}),
},
)
const result = await res.json()import os, requests
res = requests.post(
"https://api.zerowidth.ai/1.0/flows/b3c1a2d4-5e6f-7081-92a3-b4c5d6e7f809/runs",
headers={"Authorization": f"Bearer {os.environ['ZW_API_KEY']}"},
json={
"input": {"kind": "form", "values": {"data": "What is the capital of France?"}},
"source": {"kind": "published"},
},
)
result = res.json()Response:
{
"executionId": "exec_8a2c91...",
"status": "ok",
"outputs": { "data": "Paris" },
"costSummary": { "total": 0.000312 },
"durationMs": 418
}
Example: chat-shape flow
curl -X POST https://api.zerowidth.ai/1.0/flows/b3c1a2d4-5e6f-7081-92a3-b4c5d6e7f809/runs \
-H "Authorization: Bearer zw_live_abc12345_..." \
-H "Content-Type: application/json" \
-d '{
"input": {
"kind": "chat",
"messages": [
{ "role": "user", "content": "Hi there." }
]
}
}'{
"executionId": "exec_b41e7d...",
"status": "ok",
"outputs": {
"chat": [
{ "role": "assistant", "content": "Hi! How can I help?" }
]
},
"costSummary": { "total": 0.000204 },
"durationMs": 387
}Errors
Endpoint-specific error codes on top of the shared error envelope:
| Status | code | When |
|---|---|---|
404 | not_found | No flow with that flowUuid in the key's workspace — or it's a private flow the key isn't permitted to see. The response is the same either way. |
404 | flow_not_published | The pinned revision or published version doesn't exist for this flow. |
504 | timeout | Run exceeded the timeoutMs ceiling. The partial timeline is in details.partial.timeline when available. |
Auth + scope + quota codes (401, 403, 402) come from the shared envelope.
Streaming
Set stream: true (or send Accept: text/event-stream) to receive a server-sent-events response instead of a single JSON blob. Events fire as the run progresses; the final run_complete (or run_error) carries the same payload the sync response would have returned, followed by an event: done sentinel.
Event types:
event | When | Key fields |
|---|---|---|
node_start | A node is about to execute | nodeId, nodeType, inputs?, settings? |
node_complete | A node finished | nodeId, outputs?, startTime?, endTime?, durationMs |
node_update | Progress mid-run (typically token deltas from an LLM node) | nodeId, data |
node_error | A node failed | nodeId, message, details? |
run_complete | Terminal success / partial | executionId, status, outputs, costSummary, timeline, durationMs, message? |
run_error | Terminal failure | executionId, error: { kind, message, details? } |
curl -N -X POST https://api.zerowidth.ai/1.0/flows/$ZW_FLOW/runs \
-H "Authorization: Bearer $ZW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":{"kind":"chat","messages":[{"role":"user","content":"hi"}]},"stream":true}'
The streaming response always carries the full per-node timeline and itemized cost summary in the terminal event — the verbose flag is ignored when streaming since the trace is implicit in the event stream itself.
Testing your integration
- Mint a key
From accounts.zerowidth.ai → Workspace → API keys → New API key. Pick Personal kind + Run flows preset + leave cost cap at $10. Copy the raw key when shown — it's the only time you'll see it. (API keys reference)
- Find the flow's id
Open the flow in Workbench and copy its Flow ID from the dev drawer → Quickstart — a UUID, distinct from the id in the editor URL. Use the published version for stability; pass
{ "source": { "kind": "draft" } }(or omit) for the in-editor version. - Curl it
export ZW_API_KEY='zw_live_…' export ZW_FLOW='b3c1a2d4-5e6f-7081-92a3-b4c5d6e7f809' curl https://api.zerowidth.ai/1.0/flows/$ZW_FLOW/runs \ -H "Authorization: Bearer $ZW_API_KEY" \ -H "Content-Type: application/json" \ -d '{"input":{"kind":"form","values":{"data":"hello"}}}' - Confirm failure paths
- Tamper with the secret half →
401 auth_invalid. - Drop the scope (mint a key without
workbench:flows:run) →403 scope_missing. - Use a flowUuid from another workspace →
404 not_found. - Revoke the key from the dashboard, retry →
401 auth_invalid.
- Tamper with the secret half →
Troubleshooting
404 not_found — but the flow exists
Almost always the wrong id. This endpoint takes the flow's Flow ID (a UUID), not the id in the editor URL — copy it from the flow's dev drawer → Quickstart. The key's workspace must also own the flow; a private flow another member owns returns the same 404.
It's running an old version of the flow
The default source is the live draft, which changes as the flow is edited. For production, pin the version explicitly: "source": { "kind": "published", "version": "1.1.0" }. See ship to production.
status is 'partial' or my inputs seem ignored
The values keys must match each input node's key. When they don't line up, those inputs are missing and the run returns partial with an explanatory message. The dev-drawer Quickstart shows the exact request body for this flow — copy its keys.
402 plan_limit
Either the key hit its lifetime cost cap, or the workspace's inference balance is exhausted. Raise the cap, add credit, or wait for the monthly reset.
504 timeout
The run exceeded timeoutMs (max 600,000). A partial timeline is in details when available. Raise the ceiling, or check whether a node is hanging.