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

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

flowUuidstringRequired

The 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

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

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

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

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

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

7 min read