Skip to content

Instantly share code, notes, and snippets.

@kausmeows
Created April 29, 2026 20:31
Show Gist options
  • Select an option

  • Save kausmeows/1a1a3c7fb8a0a06cdd19853480fa8ab2 to your computer and use it in GitHub Desktop.

Select an option

Save kausmeows/1a1a3c7fb8a0a06cdd19853480fa8ab2 to your computer and use it in GitHub Desktop.

HITL Workflow: WebSocket Integration Guide

1. WebSocket Endpoint

Connect to the workflow WebSocket at:

ws://<host>:<port>/workflows/ws

All actions (start, continue, reconnect) go through this single connection.

Authentication

Send an authenticate action immediately after connecting:

{"action": "authenticate", "token": "<bearer_token_or_jwt>"}

Wait for {"event": "authenticated"} before sending other actions.


2. Starting a Workflow

{
  "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.


3. Handling Paused Events

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.

Pause Types

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

4. Continuing a Paused Workflow

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.


5. Payload Examples for step_requirements

5a. Step Confirmation: Approve

{
  "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
    }
  ]
}

5b. Step Confirmation: 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": "skip"
    }
  ]
}

5c. Step Confirmation: Reject (Cancel)

{
  "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"
    }
  ]
}

5d. Step User Input

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.

5e. Router User Selection

{
  "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"]
    }
  ]
}

5f. Router Confirmation

Same as step confirmation (5a-5c) but with "step_type": "Router".

5g. Executor HITL (Agent/Team Tool Confirmation)

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
        }
      ]
    }
  ]
}

6. Event Flow

Events During Normal Execution

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

Events During HITL

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)

Events After Continue

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)

7. Reconnection

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.


8. Best Practices

  1. Echo back full requirements. Take the step_requirements from the WorkflowRunOutput event, fill in your decisions, and send the full objects back. Don't send partial patches.

  2. Sync user_input into user_input_schema. When providing user input, set both user_input: {field: value} AND update each user_input_schema field's value. The backend checks schema field values to determine resolution.

  3. Handle chained pauses. After a continue, the workflow may pause again at the next step. Always check for new StepPaused events.

  4. Optimistic UI. Mark the run as RUNNING locally when sending continue-workflow. Revert to PAUSED if the send fails.

  5. Track event_index. Each event includes an event_index. Store it so you can reconnect without missing events.


9. Typical E2E Flow

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

10. Comparison: WebSocket vs SSE

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment