POST/1.0/flows/:flowUuid/runs
Executes a Workbench flow synchronously and returns the full result. Required scope: workbench:flows:run (scopes reference).
Each POST creates a run, identified by executionId in the response.
Path parameters
flowUuidstringRequiredThe flow's public id — a UUID. Copy it from the flow's Devtools → 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.
A chat flow can also read values besides the conversation, such as {{customer_name}} in its prompt. Send them on the same run as values, keyed by each input-data node's key: { kind: "chat", messages: [...], values: { customer_name: "Ada" } }.
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.
timeoutMsintegerHow long the run may take, in milliseconds. Both the default and the maximum are 600,000 (10 minutes), so this can only bring the ceiling down. Hitting it returns 504 timeout. A run stops when it stops, so a lower ceiling also caps what one run can spend.
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. Quote it when you contact support, and use it to line a run up with your own logs.
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 |
|---|---|---|
400 | validation | :flowUuid isn't a UUID. Usually the id copied out of the editor's URL, which is a different id — take flowUuid from the flow's API panel. |
403 | forbidden | The flow wants a model this workspace doesn't allow. The response names the models in question. |
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 Devtools → 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 Devtools → 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. Devtools → 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.