--- title: 'Durable workflows' description: 'Starting, streaming and inspecting a durable workflow run over HTTP, and the endpoints a parked run waits on.' position: 5 --- # Durable workflows > A workflow run outlives the request that started it. These routes start > one, follow it, read the schema it declares, and clear the human decisions > it parks on. > A workflow is a `'use workflow'` function under `workflows/`. Durability > comes from the upstream > [Workflow SDK](https://workflow-sdk.dev/docs): each `'use step'` runs once, > the runtime persists its result to an append-only event log and > deterministically replays the run on restart, so a workflow can pause (`sleep`, > hooks) for hours or days and resume where it left off. See > [Workflows](/docs/sdk/workflows/overview) for the authoring model. ### Start a run ```http POST /api/workflows/:name/start Content-Type: application/json { "orderId": "ord_42", "amount": 19.99 } ``` The runtime validates the input against the workflow's input schema **at the frontier**. A bad input returns `400` with `code: "workflow_input_invalid"` and the offending `{ path, message }` issues, and **no run starts**. A valid input returns the run handle: ```jsonc { "workflowName": "refund", "status": "started", "runId": "…", // correlate with `stackbone runs get ` "worldRunId": "…", "trigger": "POST /api/workflows/refund/start", } ``` Read `status` before the ids. A workflow declared `serial` runs one at a time, so a trigger that arrives while an earlier run is active is enqueued instead: the receipt comes back `"status": "queued"` with **no** `runId` and no `worldRunId`, because no run exists yet. It starts on its own when the lock frees. These are the other answers this route gives: | Status | `code` | When | | ------ | -------------------------------------------------------- | --------------------------------------------------------------------- | | `400` | `workflow_input_invalid` | the declared input schema rejected the body, and no run started | | `404` | `workflow_not_found` | no workflow by that name (`details.known` lists the ones there are) | | `422` | `workflow_guardrail_blocked` | a guardrail refused this payload (`details.guardrail` names the rule) | | `503` | `workflow_compile_failed` / `workflow_runtime_not_armed` | the name is registered, but the workflow could not be made runnable | ### Stream a run ```http POST /api/workflows/:name/chat Content-Type: application/json { "…": "…" } ``` A server-sent-event stream: a leading `run` frame carrying the `worldRunId` for correlation, followed by the run's own event frames as its steps execute (including any agent turn a step delegates to). It runs the same 404 and 400 checks as `/start` before it opens the stream. A `serial` workflow is refused here with `409`: this route has to hold the stream open, so it cannot queue behind an active run. Start those through `/start` instead. ### Catalog and schema | Route | Returns | | --------------------------------- | --------------------------------------------------------------------------------- | | `GET /api/workflows` | `{ items }`, the workflow catalog (name, trigger, whether it has a schema) | | `GET /api/workflows/:name` | one workflow's catalog row on its own | | `GET /api/workflows/:name/schema` | the workflow's input/output JSON Schema | | `GET /api/discovery` | the combined `{ agents, workflows }` view | | `GET /api/recurring-jobs` | the timers armed right now, when each fires next, and how its last execution went | From the CLI, the same data is `stackbone workflows list` and `stackbone workflows schema `, and you start a run by name with `stackbone workflows start `. ## Human-in-the-loop for workflows A workflow pauses for a human decision by calling `requestApproval()` from `@stackbone/sdk/workflow` (the raw `defineHook` + `sleep` are the escape hatch underneath it). The run parks durably until the decision arrives: ```ts // workflows/refund.workflow.ts import { requestApproval } from '@stackbone/sdk/workflow'; export async function refundWorkflow(input: { orderId: string; amount: number }) { 'use workflow'; const decision = await requestApproval({ topic: 'refund', payload: { orderId: input.orderId, amount: input.amount }, title: 'Approve refund', timeout: '24h', fallback: 'reject', }); if (decision.status !== 'approved') { return { orderId: input.orderId, refunded: false }; } // …perform the refund in a 'use step'… return { orderId: input.orderId, refunded: true }; } ``` The decision comes back as `{ status, payload?, timedOut }`. `status` is `'approved'` or `'rejected'`, and it is the only thing to branch the side-effect on. `timedOut` is `true` when nobody decided and the `fallback` was applied instead, so a human rejection and a lapsed timeout stay apart. Leave `token` unset. The runtime mints a resume token that is unique per run, so a retry or a second concurrent run never collides with a pause another run still holds. You read the token back from the pending-hooks list below, or from `stackbone hitl list`. Pass your own only when an outside system has to reconstruct the token without reading it back, and then you keep it unique across active runs. Posting a decision to the run's hook resumes it: | Route | Purpose | | ----------------------------------------- | ------------------------------------ | | `POST /api/workflows/hooks/:token/resume` | record a decision and resume the run | | `GET /api/workflows/runs/:traceId/hooks` | list a run's pending hooks | The runtime owns this round-trip: you call `requestApproval()` in the workflow body, never a webhook receiver. From the CLI it is `stackbone hitl list|get|approve|reject`. ## Calling an agent from a workflow A workflow step reaches an agent **in-process**, not over this HTTP surface: `callDeepAgent(name, input)` from `@stackbone/sdk/workflow` runs one turn of the named agent in the same process and resolves with `{ text }`. ```ts // workflows/qualify-lead.workflow.ts import { callDeepAgent } from '@stackbone/sdk/workflow'; async function askAgent(question: string) { 'use step'; return callDeepAgent('lead-qualifier', question); } ``` See [Workflow agents](/docs/sdk/workflow-agents/overview) for the full pattern.