Human-in-the-Loop (HITL) in YAML
This page explains how to declare Human-in-the-Loop steps in YAML, how pause/resume works, and patterns for using HITL with map and conditional steps. For a runnable end‑to‑end example that pauses inside an imported child pipeline and resumes in the parent session, see examples/imports_demo/main_with_hitl.yaml.
Overview
kind: hitlpauses the pipeline and waits for human input.- The engine records a pause message and optional schema for validation.
- Resuming the pipeline validates the human payload (if
input_schemais provided) and continues execution.
Basic Syntax
- kind: hitl
name: user_approval
message: "Please review and approve (yes/no)"
input_schema:
type: object
properties:
confirmation: { type: string, enum: ["yes", "no"] }
reason: { type: string }
required: [confirmation]
message: Short, actionable instruction shown to the human.input_schema: JSON Schema that Flujo compiles to a Pydantic model for validation on resume.
Automatic Context Storage (sink_to)
The sink_to field automatically stores the human response to a specified context path, eliminating the need for boilerplate passthrough steps:
- kind: hitl
name: get_user_name
message: "What is your name?"
sink_to: "import_artifacts.user_name"
- kind: hitl
name: get_preferences
message: "Select your preferences"
input_schema:
type: object
properties:
theme: { type: string, enum: ["light", "dark"] }
notifications: { type: boolean }
sink_to: "import_artifacts.user_preferences"
How it works:
- The human response is automatically stored to context.import_artifacts.user_name or context.import_artifacts.user_preferences
- Works with both simple text responses and structured input_schema responses
- Supports nested paths like "import_artifacts.settings.user.name"
- If the context path doesn't exist, a warning is logged and execution continues normally
Without sink_to (old pattern):
- kind: hitl
name: get_user_name
message: "What is your name?"
# Manual storage step (now unnecessary)
- kind: step
name: store_name
agent: { id: "flujo.builtins.passthrough" }
input: "{{ previous_step }}"
updates_context: true
With sink_to (new pattern):
- kind: hitl
name: get_user_name
message: "What is your name?"
sink_to: "import_artifacts.user_name"
# Done! No manual storage step needed
Conditional Routing (Approval Gate)
Use a synchronous helper for branch selection in YAML:
- kind: hitl
name: get_user_approval
message: "Approve the plan? (yes/no)"
input_schema:
type: object
properties:
confirmation: { type: string, enum: ["yes", "no"] }
required: [confirmation]
- kind: conditional
name: route_by_approval
condition: "flujo.builtins.check_user_confirmation_sync"
branches:
approved:
- kind: step
name: proceed
uses: agents.executor
denied:
- kind: step
name: abort
uses: agents.logger
HITL inside a Map
Pause once per item; resume repeatedly until done:
- kind: map
name: annotate_items
map:
iterable_input: "items"
body:
- kind: step
name: store_item
agent: { id: "flujo.builtins.passthrough" }
updates_context: true
- kind: hitl
name: annotate
message: "Provide note for current item"
- kind: step
name: combine
agent: { id: "flujo.builtins.stringify" }
Using resume_input Template Variable
After a HITL step, the resume_input variable is automatically available in templates and expressions, containing the most recent human response:
- kind: hitl
name: get_user_feedback
message: "What do you think of the analysis?"
- kind: step
name: process_feedback
uses: agents.feedback_processor
input: |
User said: {{ resume_input }}
Analysis was: {{ steps.analyze.output }}
Benefits:
- Clearer intent: resume_input explicitly indicates you're referencing HITL data
- Stable reference: Always points to the last human response, regardless of step structure
- Works in loops: Each iteration has the correct resume_input for that HITL interaction
Example with conditional logic:
- kind: hitl
name: ask_continue
message: "Continue processing? (yes/no)"
- kind: conditional
name: check_answer
condition_expression: "resume_input.lower() == 'yes'"
branches:
"true":
- kind: step
name: continue_processing
uses: agents.processor
"false":
- kind: step
name: abort
uses: agents.cleanup
Best Practices
- Use
sink_tofor automatic context storage — reduces boilerplate and keeps pipelines concise - Keep
input_schemaminimal and explicit - Use the sync helper
flujo.builtins.check_user_confirmation_syncfor YAML conditionals - Prefer short, directive messages
- For nested storage, ensure intermediate context fields exist (e.g.,
context.import_artifactsmust exist before usingsink_to: "import_artifacts.field")
Troubleshooting
- If a conditional branch with nested HITL fails instead of pausing, ensure you’re on a version where HITL pauses are propagated in conditional branches, and the condition callable is synchronous in YAML.
- If resume doesn’t validate, confirm the payload matches
input_schemaexactly (e.g., enum values like "yes"/"no").