Human-in-the-loop (HITL) review for agents¶
HITL Review adds an interactive feedback loop to agentic operators. After the LLM Agent produces an initial output, a human reviewer can approve, reject, or request changes through a chat UI. The operator blocks until a terminal action, or until a timeout is reached, max_iterations is reached, or polling the review XCom keeps failing.
This document describes the architecture, workflow, API, XCom schema, and usage.
Overview¶
See also
This page covers the iterative review loop on AgentOperator. For a one-shot
approve, edit or reject gate on LLMOperator and its subclasses, see
Approval gates for LLM operators.
Components
HITL Review Plugin: FastAPI app mounted at
/hitl-reviewon the Airflow API server. Provides REST endpoints and a chat UI for reviewers.
Storage: All state is stored in XCom on the running task instance. The worker writes session and agent outputs; the plugin writes human feedback and actions. Both sides read and write the same keys.
Compatibility: Requires Airflow 3.1+. Uses the Task SDK execution model where workers communicate via the Execution API and XCom; the plugin runs on the API server and accesses the metadata database.
Important
Worker slot usage: Each HITL task holds a worker slot for the entire
review duration (until approve, reject, timeout, max_iterations, or
repeated XCom polling failures). The operator polls
XCom with time.sleep; it does not defer. With a 10-second poll interval
and review times of 30+ minutes, the worker is occupied for the duration.
Why the operator does not defer: the agent state (message history, tool
results) lives in the worker process. Polling XCom with time.sleep keeps it
there for the whole review instead of serializing and restoring it across a
defer and resume. The standard provider’s HITLOperator defers because it
carries no in-process state.
Workflow¶
The operator and the plugin never talk to each other directly — every arrow below crosses the XCom store. The operator holds its worker slot for the whole loop; it polls instead of deferring because the agent’s message history and tool state live in-process.
Once the operator reads a human action, it resolves to one of five outcomes:
Using HITL review with AgentOperator¶
Enable the review loop with enable_hitl_review=True:
from airflow.providers.common.ai.operators.agent import AgentOperator
from airflow.providers.common.ai.toolsets.sql import SQLToolset
from datetime import timedelta
AgentOperator(
task_id="summarize",
prompt="Summarize the sales data",
llm_conn_id="openai",
toolsets=[SQLToolset(db_conn_id="postgres")],
enable_hitl_review=True,
hitl_timeout=timedelta(minutes=30),
hitl_poll_interval=10.0,
)
Parameters
enable_hitl_review: WhenTrue, the operator enters the review loop after the first generation. DefaultFalse.max_hitl_iterations: Maximum outputs the reviewer can see (1 = initial output plus subsequent regenerations). When the reviewer requests changes at iteration>= max_hitl_iterations, the task fails withHITLMaxIterationsErrorwithout running the LLM. For example,5allows changes at iterations 1 to 4; the fifth output must be either approved or rejected. Default5.hitl_timeout: Maximum wall-clock time to wait for all review rounds.None= no wall-clock timeout. The task still fails, re-raising the XCom error, if polling the human action XCom fails 10 times in a row.hitl_poll_interval: Seconds between XCom polls while waiting for a human response. Default10.
Accessing the chat UI: The chat loads as a React plugin on the task
instance page. Use the HITL Review extra link on the task instance, or
navigate to
/dags/{dag_id}/runs/{run_id}/tasks/{task_id}/plugin/hitl-review.
Example Dag
@dag(tags=["example"])
def example_agent_operator_hitl_review():
"""AgentOperator with HITL review — a human approves output via hitl-review plugin UI."""
AgentOperator(
task_id="summarize_with_review",
prompt="Summarize the Q4 sales report in 3 bullet points.",
llm_conn_id="pydantic_ai_default",
system_prompt="You are a concise business analyst.",
enable_hitl_review=True,
max_hitl_iterations=5,
hitl_timeout=timedelta(minutes=30),
hitl_poll_interval=10.0,
)
REST API¶
The plugin exposes a FastAPI app at /hitl-review. Base URL:
{AIRFLOW_BASE_URL}/hitl-review
Common query parameters (where applicable):
dag_id: Dag ID.run_id: Dag run ID.task_id: Task ID.map_index: Map index for mapped tasks. Use-1for non-mapped tasks or index for dynamic mapping.region_idandregion_index: Select one loop pass or one mapped slot. Use theregion_idandregion_indexof the task instance as returned by the public task instance endpoints. Send them together;region_indexwithoutregion_idis rejected. Only supported on Airflow 3.4 or later; earlier hosts return 400 when either is sent. A task inside a loop addressed without them returns 400, and a selection that matches more than one task instance returns 409.
Endpoints¶
Method |
Path |
Description |
|---|---|---|
GET |
|
Liveness check. Returns |
GET |
|
Find the feedback session for a task instance. Returns
|
POST |
|
Request changes. Body: |
POST |
|
Approve the current output. Session must be |
POST |
|
Reject the output. Session must be |
Response model: HITLReviewResponse¶
{
"dag_id": str,
"run_id": str,
"task_id": str,
"status": "pending_review"
| "changes_requested"
| "approved"
| "rejected"
| "max_iterations_exceeded"
| "timeout_exceeded",
"iteration": int,
"max_iterations": int,
"prompt": str,
"current_output": str,
"conversation": [{"role": "assistant" | "human", "content": str, "iteration": int}],
"task_completed": bool,
}
XCom keys and storage¶
All keys use the prefix airflow_hitl_review_.
Key |
Writer |
Value |
|---|---|---|
|
Worker |
|
|
Plugin |
|
|
Worker |
Per-iteration AI output (string or JSON) |
|
Plugin |
Per-iteration human feedback text |
Session lifecycle¶
pending_review: Awaiting human action. Plugin accepts approve, reject, or feedback.
changes_requested: Feedback submitted; worker is regenerating (or polling for the next action). Plugin does not accept new actions until the worker pushes a new output and status returns to
pending_review.approved / rejected: Terminal. Worker has exited the loop.
Chat UI¶
The plugin provides an interactive chat UI that loads in the task instance page. The UI:
Fetches session and conversation from the REST API
Displays the current output and feedback history
Submits approve, reject, or feedback via POST endpoints