Nested HITL
Combine step-level and executor-level HITL on the same step. The workflow pauses twice: once before the step runs, once when the agent calls a HITL tool.
A single step can use step-level HITL (gate the step itself) and executor-level HITL (gate a tool call inside the agent) together. The workflow pauses twice in sequence: once before the step runs, then again during the step when the agent's HITL tool is called.
1from kern.agent import Agent2from kern.db.sqlite import SqliteDb3from kern.models.openai import OpenAIResponses4from kern.tools import tool5from kern.workflow.step import Step6from kern.workflow.workflow import Workflow789@tool(requires_confirmation=True)10def send_alert(city: str, message: str) -> str:11 return f"Alert sent for {city}: {message}"121314alert_agent = Agent(15 name="AlertAgent",16 model=OpenAIResponses(id="gpt-5.4"),17 tools=[send_alert],18 db=SqliteDb(db_file="workflow.db"),19)2021workflow = Workflow(22 name="DualConfirmation",23 db=SqliteDb(db_file="workflow.db"),24 steps=[25 Step(26 name="send_alert",27 agent=alert_agent,28 requires_confirmation=True, # step-level gate29 confirmation_message="Proceed with sending the alert?",30 ),31 ],32)When this workflow runs:
- Pause 1 (step-level): user sees
confirmation_message, callsreq.confirm(). - Pause 2 (executor-level): agent calls
send_alert, user approves the specific tool call. - Step finishes.
The Active-Requirement Pattern
step_requirements accumulates across pauses within a single run. The first pause adds the step-level requirement. After resolution and continue, a second pause adds the executor-level requirement on top of it. To detect the current pause type, always look at the last entry.
1# Only the LAST requirement reflects the current pause state.2_active = (run_output.step_requirements or [])[-1:]3has_executor = any(r.requires_executor_input for r in _active)45if has_executor:6 resolve_executor_pause(run_output)7else:8 resolve_step_pause(run_output)Iterating over the full step_requirements list re-reads requirements that were already resolved in earlier pauses of the same run. Two concrete failures:
- Wrong pause type detected. If an earlier entry was an executor requirement and the current pause is step-level,
any(r.requires_executor_input for r in step_requirements)is stillTrue, so you run the executor branch and skip the step the workflow is actually waiting on.continue_runthen raises because the active requirement is unresolved. - Stale decisions reapplied. A route selection or confirmation from a prior pause gets re-applied over the current one. For example, re-confirming an old router selection overrides the user's new choice, sending the workflow down the previous branch.
Scope to the active pause with (run_output.step_requirements or [])[-1:], or use the filter properties (steps_requiring_confirmation, steps_requiring_executor_resolution, ...) which already exclude resolved entries. See Pause Anatomy.
Resolution Loop
Wrap continue calls in a while is_paused: loop. Each pause resolves one gate; the workflow either pauses again or completes.
1def resolve_step_pause(run_output):2 for req in (run_output.step_requirements or [])[-1:]:3 if req.requires_confirmation and not req.requires_executor_input:4 req.confirm() # or req.reject()567def resolve_executor_pause(run_output):8 for req in (run_output.step_requirements or [])[-1:]:9 if req.requires_executor_input:10 for executor_req in req.executor_requirements or []:11 executor_req.confirm() # or .reject(note=...) / .provide_user_input(...)121314run_output = workflow.run("Send a weather alert for Tokyo about heavy rain")1516while run_output.is_paused:17 _active = (run_output.step_requirements or [])[-1:]18 if any(r.requires_executor_input for r in _active):19 resolve_executor_pause(run_output)20 else:21 resolve_step_pause(run_output)2223 run_output = workflow.continue_run(run_output)2425print(run_output.content)Cookbooks
Runnable examples in cookbook/04_workflows/08_human_in_the_loop/dual_level_hitl/:
| File | Step-Level Gate | Executor-Level Gate |
|---|---|---|
01_step_confirmation_and_tool_confirmation.py | Step confirmation | Tool confirmation |
02_step_user_input_and_tool_confirmation.py | Step user input | Tool confirmation |
03_condition_and_tool_confirmation.py | Condition confirmation | Tool confirmation |
04_router_selection_and_tool_confirmation.py | Router route selection | Tool confirmation |
05_output_review_and_tool_confirmation.py | Step output review | Tool confirmation |
06_loop_confirmation_and_tool_confirmation.py | Loop start confirmation | Tool confirmation |
07_router_confirmation_and_tool_confirmation.py | Router confirmation | Tool confirmation |
09_multi_step_mixed_hitl.py | Multiple steps with mixed gates | Tool confirmation |