Flows API
Enterprise
Flows is an Enterprise module. These routes need an Enterprise license and answer 402 with requiresEnterprise: true without one. Concepts and the editor are in the Flows guide.
Read flows, start runs and follow them. Authoring (create, edit, publish, import) is done in the dashboard; the client API has the read and run surface only.
All routes live under /api/client/v1, take Authorization: Bearer cpeer_… and need the flows service (GET read, everything else write, except where noted). A token sees only its own project and acts for its owner. Field names are camelCase and timestamps are ISO strings. Errors use { "error": "message" }.
Endpoints
| Endpoint | |
|---|---|
GET /flows | List flows |
GET /flows/{idOrKey} | Get a flow |
POST /flows/{idOrKey}/runs | Start a run |
GET /flows/runs | List runs |
GET /flows/runs/{runId} | Get a run |
GET /flows/runs/{runId}/events | Run events |
POST /flows/runs/{runId}/cancel | Cancel a run |
POST /flows/runs/{runId}/resume | Resume a run |
POST /flows/runs/{runId}/approval | Decide an approval |
{idOrKey} is the flow id or its key.
Permission levels
| Level | Routes |
|---|---|
| Read | GET /flows, GET /flows/{idOrKey}, GET /flows/runs, GET /flows/runs/{runId}, and deciding an approval (the run's own rule decides who may) |
| Write | Start, cancel and resume, GET /flows/runs/{runId}/events, and ?steps=1 on a run. A token with write also sees a run's input and output. |
A read-only token gets run summaries without input and output.
List flows
GET /flows?agentKey=&status=&search=&limit=&offset=| Query | Meaning |
|---|---|
agentKey | Only the flows of this agent. |
status | active or archived. |
search | Text search, up to 200 characters. |
limit | 1 to 200. |
offset | Skip this many. |
{
"items": [
{
"id": "6720b4c3d2e1f0a9b8c7d6e5",
"key": "refund-request",
"name": "Refund request",
"status": "active",
"surface": "headless",
"agentKey": "support-bot",
"revision": 12,
"publishedVersion": 3,
"publishedAt": "2026-11-10T09:14:02.000Z",
"lastRun": { "runId": "…", "status": "succeeded", "startedAt": "…", "durationMs": 1840 },
"tags": [],
"inputs": [{ "name": "orderId", "type": "string", "required": true }],
"createdBy": "…"
}
],
"total": 1
}surface is headless or conversation. inputs are the declared inputs of the draft and are the flow's input schema.
Get a flow
GET /flows/{idOrKey}Returns { "flow": { … } }: the fields above plus draft, the flow definition (inputs, outputs, variables, nodes, edges, settings). 404 when the flow does not exist in the token's project.
Start a run
POST /flows/{idOrKey}/runs{
"input": { "orderId": "A-1042" },
"version": "published",
"waitMs": 30000,
"idempotencyKey": "refund-A-1042",
"unattended": false,
"chatId": "support-chat-7"
}All fields are optional; unknown fields are refused with 400.
| Field | Description |
|---|---|
input | An object with the flow's declared inputs. |
version | "published" (default), "draft" or a version number. A flow that was never published cannot be run without naming "draft" or a number (409). An archived flow cannot run (409). |
waitMs | Wait up to this long for the run to settle, 0 to 120000. Default: do not wait. |
idempotencyKey | 1 to 255 characters; also read from the Idempotency-Key header. A start repeated with the key of an existing run answers that run and starts nothing. |
unattended | true when nobody is there to answer; recorded on the run. |
chatId | Letters, digits, _ and -, up to 100. Agent steps keep one conversation across the runs with the same chatId. It is scoped to the caller, so two callers never share a conversation. |
Response: 200 when the run settled inside waitMs, 202 when it is still going.
{ "run": { "id": "…", "status": "running", "…": "…" }, "settled": false }A failed run is still a successful request: the failure is in run.status and run.failureReason. Poll GET /flows/runs/{runId} to follow it.
The run object
{
"id": "6720b5a1d2e1f0a9b8c7d700",
"flowId": "6720b4c3d2e1f0a9b8c7d6e5",
"flowKey": "refund-request",
"flowName": "Refund request",
"flowVersion": 3,
"status": "waiting",
"trigger": { "type": "api" },
"input": { "orderId": "A-1042" },
"output": null,
"runAs": { "kind": "token", "tokenId": "…", "label": "token:billing" },
"createdBy": "…",
"depth": 0,
"unattended": false,
"wait": { "kind": "approval", "nodeId": "…", "since": "…", "approval": { "title": "Approve refund", "approverUserIds": [], "requireDistinctApprover": true, "onTimeout": "reject" } },
"stepCount": 4,
"activeMs": 1210,
"usage": { "inputTokens": 812, "outputTokens": 96 },
"queuedAt": "…",
"startedAt": "…",
"lastEventSeq": 9
}| Field | Notes |
|---|---|
status | queued, running, waiting, succeeded, failed, canceled. |
failureReason | On a failed run: step_failed, interrupted_at_step, timeout, permission_lost, validation, limit_exceeded, context_too_large or internal. error has message and code. |
flowVersion | The version number, or null for a draft run. |
trigger.type | manual, test, api, agent_tool, agent_conversation, webhook, schedule, subflow or import. Runs started through this API are api. |
wait | Set while waiting: kind is delay, approval, input, form, subflow or agent_action, with resumeAt, prompt or approval details. |
input, output | Left out for a read-only token. |
parentRunId, rootRunId, depth | Set for runs started by a Run flow step. |
finishedAt, durationMs, usage.costUsd | When the run ended and what it cost. |
List runs
GET /flows/runs?flowId=&agentKey=&status=&trigger=&since=&until=&limit=&offset=since and until are dates. status and trigger take the values above. limit is 1 to 200. Response: { "items": [run, …], "total": 42 }.
Get a run
GET /flows/runs/{runId}?steps=1Returns { "run": { … } }. With steps=1 (or true) it adds steps, one entry per executed step with nodeKey, nodeType, iterationPath, attempt, status, input, output, error, logs, handle, truncated, usage and timings. A token without write access that asks for steps gets 403. 404 for an unknown run.
Run events
GET /flows/runs/{runId}/events?after=&limit=after is a sequence number (events with a higher seq), limit is 1 to 1000. Needs write.
{
"events": [
{ "seq": 1, "type": "run.queued", "at": "…" },
{ "seq": 2, "type": "run.started", "at": "…" },
{ "seq": 3, "type": "step.started", "at": "…", "data": { "nodeKey": "llm_1" } }
]
}Event types: run.queued, run.started, run.waiting, run.resumed, run.succeeded, run.failed, run.canceled, step.started, step.succeeded, step.failed, step.skipped, step.retrying, approval.requested, approval.decided, message, log.
Cancel a run
POST /flows/runs/{runId}/cancelNo body. Returns { "run": { … } }. 409 when the run has already finished.
Resume a run
POST /flows/runs/{runId}/resume{ "payload": { "message": "Yes, go ahead" }, "waitMs": 10000 }Continues a run that waits for a delay, an input or a form. payload is what the wait receives (for a form, { "values": { … } }); waitMs is as for starting. Response: { "run": …, "settled": … } with 200 or 202. 409 when the run is not waiting for something a caller can answer. Approvals are decided with the next route.
Decide an approval
POST /flows/runs/{runId}/approval{ "decision": "approved", "comment": "Checked with finance" }decision is approved or rejected; comment is optional, up to 2000 characters. Who may decide is the run's own rule: the users named on the approval step, otherwise a flows admin. A step that requires a distinct approver refuses the person who started the run (403). The decision is recorded in the audit log as flows.approval.decide. Response: { "run": …, "settled": … }.
Errors
| Status | When |
|---|---|
| 400 | Invalid body or query (issues lists the fields), invalid JSON, or a version that is not a positive integer. |
| 402 | No Enterprise license (requiresEnterprise: true). |
| 403 | The token's permission or the run's approval rule does not allow it. |
| 404 | Flow or run not found in the token's project. |
| 409 | The flow is archived or not published, or the run is in the wrong state. |
| 422 | The flow definition does not validate. |
| 429 | A quota or plan allowance is used up (code: "quota_exceeded" or a plan-limit refusal). |

