Skip to main content

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

flowUuidstringRequired

The 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

inputobjectRequired

The payload to feed the flow's input nodes. Discriminated by kind:

  • { kind: "form", values: Record<string, unknown> } — for flows with input-data / input-prompt nodes. The values keys must match each input node's key setting (or its type default: "data", "prompt").
  • { kind: "chat", messages: { role, content }[] } — for flows with input-chat nodes. Roles are "system" / "user" / "assistant" / "tool". When a flow uses tools, resend the conversation it returned verbatim on the next turn — assistant messages may carry tool_calls (with content: null for pure tool-call turns) and tool responses use role: "tool" with tool_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" } }.

sourceobject

Which 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.

timeoutMsinteger

How 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.

verboseboolean

Default 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).

streamboolean

Default 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.

executionIdstring

Stable 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: ... }.

costSummaryobject

Roll-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.

durationMsinteger

Total wall-clock duration of the run.

messagestring

Optional 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" }
  }'

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." }
      ]
    }
  }'

Errors

Endpoint-specific error codes on top of the shared error envelope:

StatuscodeWhen
400validation: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.
403forbiddenThe flow wants a model this workspace doesn't allow. The response names the models in question.
404not_foundNo 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.
404flow_not_publishedThe pinned revision or published version doesn't exist for this flow.
504timeoutRun 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:

eventWhenKey fields
node_startA node is about to executenodeId, nodeType, inputs?, settings?
node_completeA node finishednodeId, outputs?, startTime?, endTime?, durationMs
node_updateProgress mid-run (typically token deltas from an LLM node)nodeId, data
node_errorA node failednodeId, message, details?
run_completeTerminal success / partialexecutionId, status, outputs, costSummary, timeline, durationMs, message?
run_errorTerminal failureexecutionId, 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

  1. 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)

  2. 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.

  3. 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"}}}'
  4. 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.

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.

8 min read