Human-in-the-Loop in Workflows

Pause workflow execution to collect user confirmation, input, decisions, or tool approvals. State is persisted to the database so you can resume after the user responds.

Workflows pause for user action at three levels:

LevelWhat pausesConfigured onGuide
Step-levelThe workflow primitive, before or after it executesStep, Loop, Router, Condition, StepsStep HITL
Executor-levelA tool call inside the agent or team running the stepThe @tool decoratorExecutor HITL
NestedBoth gates on the same step, in sequenceBoth of the aboveNested HITL

Pause state is persisted to the workflow's database. When the user resolves the requirement, call workflow.continue_run(run_output) to resume from where it left off.

Setup

In an activated Python environment, install SQLite support for the examples:

uv pip install -U agno sqlalchemy

Examples using OpenAI agents also require the model client and key:

uv pip install -U openai
export OPENAI_API_KEY="your_openai_api_key"

On Windows, use $Env:OPENAI_API_KEY="your_openai_api_key" in PowerShell. For the optional PostgreSQL example, install "agno[postgres]" and configure an existing PostgreSQL server and its connection URL.

Configure a pause

from agno.workflow import Workflow, OnReject
from agno.workflow.step import Step
from agno.workflow.types import HumanReview
from agno.db.sqlite import SqliteDb

workflow = Workflow(
    name="data_pipeline",
    db=SqliteDb(db_file="workflow.db"),  # Required for HITL
    steps=[
        Step(name="fetch_data", agent=fetch_agent),
        Step(
            name="process_data",
            agent=process_agent,
            human_review=HumanReview(
                requires_confirmation=True,
                confirmation_message="Process sensitive data?",
                on_reject=OnReject.skip,
            ),
        ),
    ],
)

run_output = workflow.run("Process user data")

if run_output.is_paused:
    for req in run_output.steps_requiring_confirmation:
        req.confirm()  # or req.reject()
    run_output = workflow.continue_run(run_output)

Pick a Level

You want to...Use
Approve a whole step before it runsStep confirmation
Collect parameters before a step runsStep user input
Review a step's output after it runsOutput review
Let the user pick which Router branch executesRouter selection
Review each iteration of a LoopLoop iteration review
Pause a specific tool call inside an agentExecutor confirmation
Have an agent's tool ask the user for argument valuesExecutor user input
Defer a tool's execution to an external systemExternal execution
Combine a step gate and a tool gate on the same stepNested HITL
Pause when a step errorsError handling
Auto-resolve a pause if the user is slowTimeout

Step-Level vs Executor-Level

AspectStep-LevelExecutor-Level
Configured onThe workflow primitive (Step, Loop, etc.)The tool, via @tool(...)
When it pausesBefore or after the step executesDuring the step, when the agent calls the tool
pause_kind"step""executor"
Pause eventStepPausedEvent, RouterPausedEvent, StepOutputReviewEventStepExecutorPausedEvent
Continue eventStepContinuedEventStepExecutorContinuedEvent
Use caseGate the whole stepGate a specific tool call
Resolved byreq.confirm(), req.set_user_input(...), req.select(...)Mutating the requirement dicts (see Executor HITL)

Supported Primitives

PrimitiveConfirmationUser InputOutput ReviewIteration ReviewRoute SelectionExecutor HITL
Step-- (when agent / team is set)
Steps---- (via inner Step)
Condition---- (via inner Step)
Loop--- (via inner Step)
Router-- (via selected branch)

User input is currently supported on Step (to collect parameters) and Router (to select routes). Steps and Condition support confirmation only; Loop adds iteration review.

Requirements

HITL workflows need a database to persist state between pauses.

from agno.db.sqlite import SqliteDb       # SQLite for development
from agno.db.postgres import PostgresDb   # PostgreSQL for production

workflow = Workflow(db=SqliteDb(db_file="workflow.db"))
workflow = Workflow(db=PostgresDb(db_url="postgresql+psycopg://user:password@localhost:5432/dbname"))

Run Output Properties

When a workflow pauses, inspect WorkflowRunOutput:

PropertyDescription
is_pausedTrue if waiting for user action
pause_kind"step" or "executor"
paused_step_nameName of the step where the pause occurred
step_requirementsAll requirements for the run (last entry is the active one)
steps_requiring_confirmationFilter: steps needing confirm/reject
steps_requiring_user_inputFilter: steps needing user input values
steps_requiring_output_reviewFilter: steps needing output review
steps_requiring_routeFilter: routers needing route selection
steps_with_errorsSteps that failed with on_error="pause"

For nested HITL, always read the last entry in step_requirements to determine the current pause type. Earlier entries are history. See Nested HITL.

For the full structure of every pause object (WorkflowRunOutput, StepRequirement, and executor requirements) and the methods that resolve each, see Pause Anatomy.

OnReject Behavior

on_reject controls what happens when a user rejects a step.

ValueBehavior
OnReject.skipSkip the step and continue (default for most primitives)
OnReject.cancelCancel the entire workflow
OnReject.retryRe-execute the step. Pair with reject(feedback=...) to send feedback. See Output Review
OnReject.else_branchFor Condition only: execute else_steps. Set explicitly when supplying HumanReview; its default is skip

Timeout

Step confirmation and output-review pauses can have a deadline. If the user does not respond in time, on_timeout decides what happens. Other primitives ignore the timeout fields.

from agno.workflow import OnTimeout
from agno.workflow.types import HumanReview

Step(
    name="approve_deploy",
    agent=deploy_agent,
    human_review=HumanReview(
        requires_confirmation=True,
        confirmation_message="Approve deployment?",
        timeout=300,                       # 5 minutes
        on_timeout=OnTimeout.approve,      # approve | skip | cancel
    ),
)

See Timeout for the full set of behaviors.

Streaming

HITL works with streaming. Watch for pause events; read the paused run from the session.

from agno.run.workflow import StepPausedEvent, StepExecutorPausedEvent

for event in workflow.run("input", stream=True):
    if isinstance(event, (StepPausedEvent, StepExecutorPausedEvent)):
        pass

session = workflow.get_session()
run_output = session.runs[-1]

if run_output.is_paused:
    # resolve requirements...
    for event in workflow.continue_run(run_output, stream=True):
        pass

The full WorkflowRunOutput (including step_requirements) is persisted to the database, not yielded as a stream event.

The @pause Decorator

Mark custom function steps with HITL configuration.

from agno.workflow.decorators import pause
from agno.workflow.types import StepInput, StepOutput, UserInputField

@pause(
    requires_user_input=True,
    user_input_message="Enter parameters:",
    user_input_schema=[
        UserInputField(name="threshold", field_type="float", required=True),
    ],
)
def process_data(step_input: StepInput) -> StepOutput:
    threshold = step_input.additional_data["user_input"]["threshold"]
    return StepOutput(content=f"Processed with threshold {threshold}")

Step(name="process", executor=process_data)

Guides

Developer Resources