| Parameter | Type | Required | Description |
|---|---|---|---|
step_requirements |
string (JSON array) |
Yes | Resolved StepRequirement objects from the paused response |
session_id |
string |
Yes | Session ID from the paused run |
user_id |
string |
No | User identifier |
stream |
bool |
No | Enable SSE streaming (default: true) |
| Response Code | Meaning |
|---|---|
| 200 | Resumed (SSE stream or JSON) |
| 400 | Invalid JSON in step_requirements |
| 404 | Workflow or run not found |
| 409 | Run is not paused |
Where to get step_requirements:
- Non-streaming (
stream=false): The paused JSON response containsstep_requirementsdirectly. - Streaming (
stream=true): The server emits a finalevent: WorkflowRunOutputSSE event when the workflow pauses. Parse thedataJSON of that event to getstep_requirements.
Best practice: Echo back the full step_requirements array from the paused response with your decisions filled in.
All examples use:
${wf_id}= workflow ID${run_id}= run ID from initial run response${sess_id}= session ID from initial run response${step_id}=step_requirements[0].step_idfrom the paused response
Step has requires_confirmation=True. User approves before it executes.
curl -X POST "http://localhost:7777/workflows/${wf_id}/runs/${run_id}/continue" \
-F "session_id=${sess_id}" \
-F "stream=false" \
-F 'step_requirements=[{
"step_id": "'${step_id}'",
"step_name": "process_data",
"step_type": "Step",
"requires_confirmation": true,
"confirmed": true
}]'Result: Step executes, workflow continues.
curl -X POST "http://localhost:7777/workflows/${wf_id}/runs/${run_id}/continue" \
-F "session_id=${sess_id}" \
-F "stream=false" \
-F 'step_requirements=[{
"step_id": "'${step_id}'",
"step_name": "process_data",
"step_type": "Step",
"requires_confirmation": true,
"confirmed": false,
"on_reject": "skip"
}]'Result: Step is skipped, workflow continues to the next step.
curl -X POST "http://localhost:7777/workflows/${wf_id}/runs/${run_id}/continue" \
-F "session_id=${sess_id}" \
-F "stream=false" \
-F 'step_requirements=[{
"step_id": "'${step_id}'",
"step_name": "process_data",
"step_type": "Step",
"requires_confirmation": true,
"confirmed": false,
"on_reject": "cancel"
}]'Result: Entire workflow is cancelled. Status: CANCELLED.
Step has requires_user_input=True with a user_input_schema. User fills in the fields.
curl -X POST "http://localhost:7777/workflows/${wf_id}/runs/${run_id}/continue" \
-F "session_id=${sess_id}" \
-F "stream=false" \
-F 'step_requirements=[{
"step_id": "'${step_id}'",
"step_name": "collect_preferences",
"step_type": "Step",
"requires_user_input": true,
"user_input": {
"tone": "formal",
"length": "short",
"language": "en"
}
}]'Result: Step executes with user input available in step_input.additional_data["user_input"].
Same variables as Section 2.
Selector picks route automatically, user confirms before execution.
curl -X POST "http://localhost:7777/workflows/${wf_id}/runs/${run_id}/continue" \
-F "session_id=${sess_id}" \
-F "stream=false" \
-F 'step_requirements=[{
"step_id": "'${step_id}'",
"step_name": "ops_router",
"step_type": "Router",
"requires_confirmation": true,
"confirmed": true
}]'Result: Router executes the selector-chosen route, workflow continues.
curl -X POST "http://localhost:7777/workflows/${wf_id}/runs/${run_id}/continue" \
-F "session_id=${sess_id}" \
-F "stream=false" \
-F 'step_requirements=[{
"step_id": "'${step_id}'",
"step_name": "ops_router",
"step_type": "Router",
"requires_confirmation": true,
"confirmed": false,
"on_reject": "skip"
}]'Result: Router is skipped entirely, workflow continues to the next step.
curl -X POST "http://localhost:7777/workflows/${wf_id}/runs/${run_id}/continue" \
-F "session_id=${sess_id}" \
-F "stream=false" \
-F 'step_requirements=[{
"step_id": "'${step_id}'",
"step_name": "ops_router",
"step_type": "Router",
"requires_confirmation": true,
"confirmed": false,
"on_reject": "cancel"
}]'Result: Entire workflow is cancelled. Status: CANCELLED.
User selects which route to execute from available_choices in the paused response.
curl -X POST "http://localhost:7777/workflows/${wf_id}/runs/${run_id}/continue" \
-F "session_id=${sess_id}" \
-F "stream=false" \
-F 'step_requirements=[{
"step_id": "'${step_id}'",
"step_name": "analysis_router",
"step_type": "Router",
"requires_route_selection": true,
"selected_choices": ["billing"]
}]'Result: Only the selected route ("billing") executes, workflow continues.
Step-level HITL:
| Scenario | Cookbook |
|---|---|
| Step confirmation (basic) | 01_basic_step_confirmation.py |
| Step confirmation (streaming) | 03_step_confirmation_streaming.py |
| Step user input | 02_step_user_input.py |
| Step user input (streaming) | 03_step_user_input_streaming.py |
Router-level HITL:
| Scenario | Cookbook |
|---|---|
| Router confirmation (approve / reject-skip) | 04_router_confirmation.py |
| Router user selection | 01_router_user_selection.py |
| Router multi-selection | 02_router_multi_selection.py |
from agno.workflow import OnReject
from agno.workflow.step import Step
Step(
name="process_data",
agent=processing_agent,
requires_confirmation=True,
confirmation_message="Process the data with these parameters?",
on_reject=OnReject.skip, # or OnReject.cancel
)Supported on_reject: skip (default), cancel
from agno.workflow.step import Step
from agno.workflow.types import UserInputField
Step(
name="collect_preferences",
agent=content_agent,
requires_user_input=True,
user_input_message="Provide your content preferences:",
user_input_schema=[
UserInputField(name="tone", field_type="str", description="formal / casual / technical", required=True),
UserInputField(name="length", field_type="str", description="short / medium / long", required=True),
UserInputField(name="language", field_type="str", description="Language code", required=False),
],
)from agno.workflow import OnReject
from agno.workflow.router import Router
from agno.workflow.step import Step
Router(
name="ops_router",
choices=[
Step(name="restart", description="Restart service", executor=restart_handler),
Step(name="scale", description="Scale service", executor=scale_handler),
],
selector=route_by_intent,
requires_confirmation=True,
confirmation_message="Execute this operation?",
on_reject=OnReject.cancel, # or OnReject.skip (default)
)Supported on_reject: skip (default), cancel
Router(
name="analysis_router",
choices=[
Step(name="quick", description="Fast analysis (2 min)", executor=quick_fn),
Step(name="deep", description="Deep analysis (10 min)", executor=deep_fn),
Step(name="custom", description="Custom analysis", executor=custom_fn),
],
requires_user_input=True,
user_input_message="Select analysis type:",
allow_multiple_selections=False,
)1. POST /workflows/{wf_id}/runs
-F 'message=...' -F 'stream=false'
-> Response: status=PAUSED, step_requirements=[...]
(If streaming: parse the final `event: WorkflowRunOutput` SSE event to get step_requirements)
2. Inspect step_requirements:
- requires_confirmation=true -> user approves/rejects (Step or Router)
- requires_user_input=true -> user fills in fields from user_input_schema (Step)
- requires_route_selection=true -> user picks from available_choices (Router)
3. POST /workflows/{wf_id}/runs/{run_id}/continue
-F 'session_id=...' -F 'step_requirements=[{...resolved...}]' -F 'stream=false'
-> Response: status=COMPLETED | PAUSED (if next step also needs HITL) | CANCELLED
4. Repeat step 3 if still PAUSED.