Running workflows
Introduction
Section titled “Introduction”A run is one execution of a workflow with an input. It ends finished (with an output) or failed, or it pauses until someone answers a question. This page shows how to start a run, follow it and answer it, first through the server’s REST API and then with the runtime directly.
Starting a run
Section titled “Starting a run”curl -X POST $API/workflows/$ID/runs -H 'content-type: application/json' \ -d '{"input":{"orderId":"A-1001"}}'The answer is 201 with the run as running. Read it back until it settles:
curl $API/workflows/$ID/runs/$RUN_ID{ "id": "run_...", "workflowId": "wf_...", "status": "finished", "output": { "result": "Paged on-call" }, "error": null, "interrupts": [] }From a React app, createWorkflowsClient from @pilotoo/chat-core wraps the same routes:
import { createWorkflowsClient } from '@pilotoo/chat-core';
const workflows = createWorkflowsClient(apiUrl, fetch, { credentials });const run = await workflows.start(workflowId, { orderId: 'A-1001' });const settled = await workflows.run(workflowId, run.id);const events = await workflows.events(workflowId, run.id);Failures throw a WorkflowError with a kind: invalid, forbidden, not-found, conflict, unavailable or http.
Run states
Section titled “Run states”| Status | Meaning |
|---|---|
running |
In progress |
paused |
Waiting on one or more interrupts; interrupts is non-empty |
finished |
Done; output holds the named values from output nodes |
failed |
A node failed after its retries, or the run hit maxSteps; error holds the message |
cancelled |
The server stopped it (a workflow delete, a shutdown) |
Following a run
Section titled “Following a run”Every run keeps its events. Read them in order:
curl $API/workflows/$ID/runs/$RUN_ID/events| Event | Emitted |
|---|---|
RUN_STARTED |
When a run starts, and again on each resume |
NODE_STARTED |
Before a node executes |
NODE_FINISHED |
After it succeeds |
NODE_FAILED |
After each failed attempt |
RUN_PAUSED |
When the run waits on interrupts |
RUN_FINISHED |
On success |
RUN_FAILED |
On failure |
The server stores each as a run event with the WORKFLOW_ prefix (WORKFLOW_NODE_FINISHED, and so on), stamped with at.
To follow a run live, join its socket room and listen for workflow:event. The demo web app does this in a small event source:
socket.on('connect', () => socket.emit('workflow:watch', { workflowId, runId }, () => undefined));socket.on('workflow:event', ({ run, event }) => render(run, event));run is the run as it is after that event. Details are in Server integration.
Pausing and resuming
Section titled “Pausing and resuming”Two kinds of node pause a run: approve, which asks a person, and agent, when the agent asks something.
An approval is a node with a message:
{ "id": "ok", "type": "approve", "config": { "message": "Refund {{ $input.amount }} to the customer?", "data": { "orderId": "{{ $input.orderId }}" } } }The run stops at it. Its status is paused and interrupts lists the pending question:
{ "status": "paused", "interrupts": [{ "interruptId": "int_...", "nodeId": "ok", "payload": { "message": "Refund 40 to the customer?", "data": { "orderId": "A-1001" } } }] }Answer it by id:
curl -X POST $API/workflows/$ID/runs/$RUN_ID/resume -H 'content-type: application/json' \ -d '{"interruptId":"int_...","answer":{"approved":true}}'The answer is 200 with the run as running. The run leaves the approve node through approved for true or { approved: true, ... }, and through rejected for anything else. The whole answer becomes the node’s output.
| Node | Pause payload | Resume answer |
|---|---|---|
approve |
{ message, data } from its config |
true or { approved: true, ... } leaves through approved; anything else leaves through rejected |
agent |
{ kind: 'agent', agentId, interrupts }, the agent’s own pending decisions |
{ interruptId, answer } naming one of the agent’s pending interrupts |
When several branches pause at once, each has its own interruptId. A resume answers one interrupt by id. The others stay pending, and the run stays paused until none are left.
Who a run acts as
Section titled “Who a run acts as”A run acts as whoever started or resumed it. The request’s credentials are lent to that run’s nodes, so an agent or tool node sees the same identity it would in a chat turn. Runs started by a webhook carry no credentials.
Stopping a runaway
Section titled “Stopping a runaway”A run has a step budget, maxSteps, which defaults to 1000. A run that exceeds it fails.
Using the runtime directly
Section titled “Using the runtime directly”Hosts using @pilotoo/server do not do this; the server does. Use the runtime when you embed workflows without the server, for example in a script or a test.
import { MemorySaver } from '@langchain/langgraph';import { coreNodeKinds, createWorkflowRuntime } from '@pilotoo/workflow';
const runtime = createWorkflowRuntime({ kinds: [...coreNodeKinds, ...hostKinds], checkpointer: new MemorySaver(), ports: { fetch, sandbox }, onEvent: (event) => console.log(event), maxSteps: 1000,});
const outcome = await runtime.start({ workflow, input, threadId, runId, signal });if (outcome.status === 'paused') { await runtime.resume({ workflow, threadId, runId: nextRunId, interruptId: outcome.interrupts[0].interruptId, answer: { approved: true }, });}await runtime.pendingInterrupts(threadId);start returns { status: 'finished', output } or { status: 'paused', interrupts: [{ interruptId, nodeId, payload }] }. A failed run throws WorkflowRunError (runId, nodeId, the node’s message, cause).
| Option | Meaning |
|---|---|
kinds |
Node kinds. coreNodeKinds ships the built-ins |
checkpointer |
A LangGraph BaseCheckpointSaver, the same one the agents use. MemorySaver in tests |
ports |
fetch, sandbox, and per-run ports passed to start |
onEvent |
Receives every engine event |
maxSteps |
Supersteps a run may take before it fails as runaway. Default 1000 |
Edge cases
Section titled “Edge cases”- A node with a missing port (
fetchforhttp,sandboxforcode) fails the run with a message naming the port. - A run has a thread of its own. It is not a conversation and never appears in the conversation list.
- Deleting a workflow stops its running runs and removes its runs, their events and their checkpoints.
Next steps
Section titled “Next steps”- Triggers: start runs from a webhook or a schedule.
- Server integration: the routes and the socket events in full.
- Building workflows: the
approveandagentnodes.