Skip to content

Runs and approvals

A run is one task for an agent: an input in, a model’s answer out, with any tool calls in between.

const run = await zupflow.runs.create(workspaceId, agentId, { input: "What's due this week?" });

A run’s status moves through:

stateDiagram-v2
direction LR
[*] --> queued
queued --> running
running --> waiting_for_approval: a call needs approval
waiting_for_approval --> running: decided
running --> completed
running --> failed
queued --> cancelled
running --> cancelled
waiting_for_approval --> cancelled
completed --> [*]
failed --> [*]
cancelled --> [*]

The run records its model and connection when it starts, and checks before every model call that both are still there. With neither an agent model nor an environment default, starting a run is refused (422 model_not_configured).

Every step is an event with a sequence number. Read them all, or only those after a sequence you already have:

const events = await zupflow.runs.events(workspaceId, run.id, lastSequence);
Event When
run.queued, run.started Admitted, then taken up with its model.
message.assistant The model said something.
tool_call.started, tool_call.completed, tool_call.failed A call was sent and answered.
approval.requested, approval.resolved A call waits for a decision, then gets one.
run.completed, run.failed, run.cancelled The run ended.
run_034HXHH2LZFzvoaSAhfGVDcompleted
  1. 1run.queuedAdmitted for agent Planner0.0s
  2. 2run.startedOn claude-sonnet-4-50.1s
  3. 3tool_call.startedlinear_k7m2qd_list_issues1.6s
  4. 4tool_call.completed14 issues2.2s
  5. 5approval.requestedupdate_issue · move ENG-212 to next sprint4.0s
  6. 6approval.resolvedApproved by the person, in your product3m 12s
  7. 7tool_call.startedlinear_k7m2qd_update_issue3m 12s
  8. 8tool_call.completedENG-212 moved3m 13s
  9. 9message.assistant“Three issues are due this week…”3m 15s
  10. 10run.completedOutput saved on the run3m 15s
A run's events, by sequence. The amber stretch is the pause: the run waits for a decision for as long as it takes, holding nothing open.

For anything you need to react to, prefer webhooks over polling events.

When a call needs approval, the run pauses with waiting_for_approval, records approval.requested with the call’s ID and arguments, and Zupflow sends the run.waiting_for_approval webhook. Your product shows the person what the agent wants to do; your backend sends the decision:

await zupflow.runs.resolveApproval(workspaceId, runId, {
call_id: callId,
decision: "denied",
reason: "Not this week.", // optional, goes back to the model
});
  • Only an API key can decide. A Console session gets 403 approval_requires_api_key: the Console observes approvals, the person decides in your product.
  • A denial’s reason goes back to the model as the call’s result, so it can carry on without the call.
  • Deleting or disconnecting the installation refuses its waiting calls, so the run goes on without anyone deciding. A decision sent after that answers 409 not_waiting.

POST /v1/workspaces/{ws}/runs/{run}/cancel stops a run and closes every call not yet sent.

Runs survive interruptions. Each step is saved before it’s taken, and a call whose outcome was lost mid-send is settled as unknown, never sent again. A run nothing can take forward any more fails with run_unavailable.