Connect to the workflow WebSocket at:
ws://<host>:<port>/workflows/ws
All actions (start, continue, reconnect) go through this single connection.
Send an authenticate action immediately after connecting:
{"action": "authenticate", "token": "<bearer_token_or_jwt>"}Wait for {"event": "authenticated"} before sending other actions.
{
"action": "start-workflow",
"workflow_id": "<workflow_id>",
"message": "<user_message>",
"session_id": "<optional_session_id>",
"user_id": "<optional_user_id>"
}The server streams events over the WebSocket. If a step requires HITL, the stream will include a StepPaused event and then a final WorkflowRunOutput event with the full paused state.
When the workflow pauses, you receive a StepPaused event:
{
"event": "StepPaused",
"run_id": "abc-123",
"session_id": "sess-456",
"step_name": "collect_preferences",
"step_index": 0,
"step_id": "step-789",
"requires_confirmation": false,
"requires_user_input": true,
"user_input_message": "Provide your preferences:",
"user_input_schema": [
{"name": "tone", "field_type": "str", "description": "formal / casual", "required": true, "value": null},
{"name": "language", "field_type": "str", "description": "Language code", "required": false, "value": null}
]
}Followed by a WorkflowRunOutput event containing the full step_requirements array. Save this — you'll echo it back with your decisions filled in.
Field in StepPaused |
HITL Type | What to Collect |
|---|---|---|
requires_confirmation=true |
Step/Router Confirmation | confirmed: true/false |
requires_user_input=true |
Step User Input | user_input: {field: value, ...} |
requires_route_selection=true |
Router Selection | selected_choices: ["route_name"] |
requires_executor_input=true |
Agent/Team Tool HITL | Resolve executor_requirements |
Send a continue-workflow action with the resolved step_requirements:
{
"action": "continue-workflow",
"workflow_id": "<workflow_id>",
"run_id": "<run_id from StepPaused>",
"session_id": "<session_id from StepPaused>",
"step_requirements": [ <resolved requirements> ]
}The server resumes execution and streams events over the same WebSocket connection. If another step pauses, you'll receive another StepPaused + WorkflowRunOutput — repeat the continue flow.
{
"action": "continue-workflow",
"workflow_id": "my-workflow",
"run_id": "abc-123",
"session_id": "sess-456",
"step_requirements": [
{
"step_id": "step-789",
"step_name": "process_data",
"step_index": 0,
"step_type": "Step",
"requires_confirmation": true,
"confirmed": true
}
]
}{
"action": "continue-workflow",
"workflow_id": "my-workflow",
"run_id": "abc-123",
"session_id": "sess-456",
"step_requirements": [
{
"step_id": "step-789",
"step_name": "process_data",
"step_index": 0,
"step_type": "Step",
"requires_confirmation": true,
"confirmed": false,
"on_reject": "skip"
}
]
}{
"action": "continue-workflow",
"workflow_id": "my-workflow",
"run_id": "abc-123",
"session_id": "sess-456",
"step_requirements": [
{
"step_id": "step-789",
"step_name": "process_data",
"step_index": 0,
"step_type": "Step",
"requires_confirmation": true,
"confirmed": false,
"on_reject": "cancel"
}
]
}When requires_user_input=true, fill in user_input with the field values AND update user_input_schema field values to match:
{
"action": "continue-workflow",
"workflow_id": "my-workflow",
"run_id": "abc-123",
"session_id": "sess-456",
"step_requirements": [
{
"step_id": "step-789",
"step_name": "collect_preferences",
"step_index": 0,
"step_type": "Step",
"requires_user_input": true,
"user_input": {
"tone": "formal",
"language": "en"
},
"user_input_schema": [
{"name": "tone", "field_type": "str", "description": "formal / casual", "required": true, "value": "formal"},
{"name": "language", "field_type": "str", "description": "Language code", "required": false, "value": "en"}
]
}
]
}Important: The user_input_schema fields must have their value set to the corresponding values from user_input. The backend checks user_input_schema[].value to determine if the requirement is resolved.
{
"action": "continue-workflow",
"workflow_id": "my-workflow",
"run_id": "abc-123",
"session_id": "sess-456",
"step_requirements": [
{
"step_id": "step-789",
"step_name": "analysis_router",
"step_index": 0,
"step_type": "Router",
"requires_route_selection": true,
"selected_choices": ["quick"]
}
]
}Same as step confirmation (5a-5c) but with "step_type": "Router".
When an agent/team inside a step pauses for tool confirmation, the StepPaused event has requires_executor_input=true with executor_requirements containing the tool details:
{
"action": "continue-workflow",
"workflow_id": "my-workflow",
"run_id": "abc-123",
"session_id": "sess-456",
"step_requirements": [
{
"step_id": "step-789",
"step_name": "execute_action",
"step_index": 1,
"requires_executor_input": true,
"executor_id": "agent-001",
"executor_name": "ActionAgent",
"executor_run_id": "run-xyz",
"executor_type": "agent",
"executor_session_id": "agent-sess-123",
"executor_requirements": [
{
"id": "req-001",
"tool_execution": {
"tool_name": "delete_record",
"tool_args": {"record_id": "42"},
"requires_confirmation": true
},
"confirmation": true
}
]
}
]
}| Event | Description |
|---|---|
WorkflowStarted |
Workflow execution has begun |
StepStarted |
A step has started executing |
RunResponseContentEvent |
Streaming content from agent/team executor |
StepCompleted |
A step has finished |
WorkflowCompleted |
Workflow finished successfully |
| Event | Description |
|---|---|
StepPaused |
Step requires user action (confirmation, input, or route selection) |
StepExecutorPaused |
Agent/team inside a step requires tool confirmation |
RouterPaused |
Router requires route selection |
StepOutputReview |
Step output requires user review |
WorkflowRunOutput |
Full workflow state (sent when paused — contains step_requirements) |
| Event | Description |
|---|---|
StepContinued |
Step-level HITL resolved, step resuming |
StepExecutorContinued |
Executor-level HITL resolved, agent/team resuming |
WorkflowCancelled |
Workflow was cancelled (e.g. user rejected with on_reject=cancel) |
If the WebSocket disconnects, reconnect and send:
{
"action": "reconnect",
"run_id": "<run_id>",
"workflow_id": "<workflow_id>",
"session_id": "<session_id>",
"last_event_index": <last_received_event_index>
}The server replays missed events and subscribes you to new ones. This works for both running and paused workflows.
Track event_index on each received event to know your position.
-
Echo back full requirements. Take the
step_requirementsfrom theWorkflowRunOutputevent, fill in your decisions, and send the full objects back. Don't send partial patches. -
Sync
user_inputintouser_input_schema. When providing user input, set bothuser_input: {field: value}AND update eachuser_input_schemafield'svalue. The backend checks schema field values to determine resolution. -
Handle chained pauses. After a continue, the workflow may pause again at the next step. Always check for new
StepPausedevents. -
Optimistic UI. Mark the run as
RUNNINGlocally when sendingcontinue-workflow. Revert toPAUSEDif the send fails. -
Track
event_index. Each event includes anevent_index. Store it so you can reconnect without missing events.
1. Connect to ws://<host>/workflows/ws
2. Authenticate: {"action": "authenticate", "token": "..."}
3. Start: {"action": "start-workflow", "workflow_id": "...", "message": "..."}
4. Receive events... until StepPaused arrives
5. Parse StepPaused to determine HITL type
6. Receive WorkflowRunOutput with full step_requirements
7. User makes decision (confirm, fill input, select route)
8. Send: {"action": "continue-workflow", ...resolved step_requirements...}
9. Receive StepContinued, then more events...
10. If another StepPaused arrives, repeat from step 5
11. Receive WorkflowCompleted — done
| Aspect | WebSocket (/workflows/ws) |
SSE (POST /workflows/.../continue) |
|---|---|---|
| Transport | Persistent bidirectional connection | HTTP streaming response |
| Start + Continue | Same connection | Separate HTTP requests |
| Reconnection | reconnect action with last_event_index |
GET /resume endpoint |
| Event Buffering | Yes (via event buffer) | Yes (via event buffer) |
| Authentication | Message-based (authenticate action) |
HTTP header / form param |
| Best For | Real-time apps, chat UIs | REST-based integrations |