TL;DR
-
Human-in-the-loop (HITL) pauses agent execution at defined interrupt points so a person can approve, edit, or reject the next action before it runs.
-
Interrupt/resume requires durable state - the agent must checkpoint mid-run, survive process restarts, and continue with the human's decision attached.
-
Gate write actions, not every token - read-only research and drafting can stay autonomous; refunds, emails, deploys, and DB writes should interrupt.
-
Approval UX is part of the system - timeouts, escalation, partial edits, and audit logs matter as much as the graph node that pauses.
-
Progressive autonomy is the production path - start with broad gates, measure false positives and latency, then automate only what evals and risk tolerate.
Why This Matters
Agents that only generate text are low risk. Agents that call tools with side effects are not. A mistaken refund, a wrong customer email, or a bad production config change is a support incident or a security event - not a mildly wrong chatbot reply.
HITL is how teams ship useful agents before the model is perfect. You keep the speed of autonomous planning and tool selection, then insert a human gate where mistakes are expensive. That pattern shows up in support desks (approve refund), finance ops (approve wire), SRE (approve runbook step), and coding agents (approve PR merge).
Without interrupt/resume, "ask a human" usually means restarting the whole run after the person replies. That wastes tokens, loses tool results, and breaks multi-step context. Production HITL is a control-flow feature of the orchestrator, not a prompt that says "please wait for approval."
The Problem Human-in-the-Loop Solves
Fully autonomous loops fail in production when:
-
Actions are irreversible - money moves, emails send, tickets close, credentials rotate.
-
Policy is nuanced - "refund under $50" may be auto; edge cases need judgment the model lacks.
-
Trust is not yet earned - eval success is 80%, not 99%. You still want value from the agent without unbounded risk.
-
Compliance requires a named approver - regulated workflows need a human signature in the audit trail.
-
Context is ambiguous - the agent proposes a plan, but only a domain expert can choose among valid options.
Prompting the model to "be careful" does not create a hard stop. Guardrails can block invalid outputs, but they cannot apply organizational judgment. HITL is the missing control: execution waits until a human decision arrives.
How We Got Here
HITL for agents is not a new invention - it is the agent-era form of maker-checker and approval workflows that ops and finance already use:
Diagram: Evolution of human-in-the-loop
flowchart LR
A[Manual tickets] --> B[BPMN / saga approvals]
B --> C[Chatbots ask clarifying Qs]
C --> D[Tool-calling agents]
D --> E[Interrupt + durable resume]
E --> F[Progressive autonomy]
HITL for agents reuses maker-checker and workflow approvals, then adds interrupt/resume semantics.
| Era | Pattern | Gap for agents |
|---|---|---|
| Manual / ticket queues | Human does the whole task | No LLM leverage |
| BPMN / workflow approvals | Durable pause before writes | Steps were hand-authored, not model-proposed |
| Chatbot "are you sure?" | Conversational confirm | Not a hard stop; no checkpoint |
| Tool-calling agents | Model proposes side effects | Needs runtime interrupt before execute |
| Production HITL | LangGraph interrupt + checkpointer | Paired with durable execution and evals |
Public guidance that shaped the pattern: LangGraph Human-in-the-Loop, OpenAI Agents SDK HITL, and Anthropic — Building effective agents (prefer workflows and human gates over unbounded autonomy).
What Is Human-in-the-Loop?
Human-in-the-loop for agents is an orchestration pattern where the runtime:
- Runs the agent until a configured interrupt condition (tool, risk score, amount threshold, or explicit node).
- Persists checkpointed state (messages, tool args, plan).
- Surfaces a review payload to a human (UI, Slack, ticket, API).
- Accepts a decision: approve, reject, or edit.
- Resumes the graph from the same point with that decision as input.
This is distinct from:
| Pattern | Who decides | When |
|---|---|---|
| Human-on-the-loop | Human monitors; intervenes optionally | After or during autonomous run |
| Human-in-the-loop | Human must decide at a gate | Before the gated action executes |
| Human-out-of-the-loop | No human in the path | Full automation |
Most production agent systems use a mix: autonomous for reads and drafts, HITL for writes above a threshold, and human-on-the-loop dashboards for monitoring.
Engineering Insight
💡 Key Idea - HITL is not "chat with the user mid-task." It is a suspendable workflow with typed decisions, timeouts, and durable checkpoints - closer to a saga or approval BPMN step than to another LLM turn.
How Human-in-the-Loop Works
Frameworks implement HITL as interrupt points in a state machine. LangGraph exposes interrupt() / interrupt_before so a node pauses before (or after) tools run. OpenAI Agents SDK and similar runtimes support approval callbacks for sensitive tools. The conceptual flow is the same across stacks.
Diagram: HITL interrupt control flow
flowchart TD
A[User goal] --> B[Agent reasons / selects tool]
B --> C{Action gated?}
C -->|No| D[Execute tool]
D --> E[Observe result]
E --> B
C -->|Yes| F[Checkpoint state]
F --> G[Notify human reviewer]
G --> H{Decision}
H -->|Approve| I[Execute tool]
H -->|Edit args| J[Apply edits then execute]
H -->|Reject| K[Record rejection / replan]
I --> E
J --> E
K --> B
Safe steps run autonomously; gated writes checkpoint, wait for a human decision, then resume.
What the diagram shows: the agent loop is unchanged for safe steps. On a gated action, state is frozen, a human decides, and the loop resumes with that decision - without replaying prior tool calls.
Interrupt lifecycle
Diagram: HITL interrupt lifecycle
stateDiagram-v2
[*] --> Running
Running --> Running: safe tool / observe
Running --> Interrupted: gated write
Interrupted --> Running: approve / edit
Interrupted --> Replan: reject
Interrupted --> TimedOut: deadline
Replan --> Running
TimedOut --> Escalated
TimedOut --> Rejected
Running --> Done: final answer
Done --> [*]
Interrupted must be a durable status so approvals can arrive after deploys or hours of delay.
Lifecycle notes: Interrupted must be a durable status (not an in-memory flag). Resume injects the decision and continues from the same edge - see durable execution. Timeouts need an explicit policy (reject, escalate, or rare auto-approve).
Interrupt payloads
A useful interrupt package includes:
- Proposed action - tool name + validated arguments
- Why gated - policy rule, risk score, amount, destination
- Context summary - last N messages, relevant tool results, customer ID
- Alternatives - reject, edit fields, escalate to another role
- Deadline - timeout behavior if no response
Resume semantics
On resume, the orchestrator loads the checkpoint, injects the human decision into state, and continues from the interrupt edge. Idempotency matters: if the process crashed after approval but before the tool finished, resume must not double-execute the side effect. Pair HITL with durable execution patterns (idempotency keys, exactly-once side-effect wrappers).
Architecture
Diagram: HITL system architecture
flowchart TB
User[User / API] --> Orch[Orchestrator / graph]
Orch --> LLM[LLM policy]
Orch --> Policy[Policy engine]
Orch --> Tools[Tool registry]
Orch --> CP[Checkpoint store]
Policy -->|gate| Interrupt[Interrupt node]
Interrupt --> Review[Review UI / Slack / ticket]
Review --> Decision[Decision API + authz]
Decision --> Orch
Orch --> Audit[Audit log]
Tools --> Ext[Payments, email, deploys]
Policy and humans dispose of model-proposed writes; the checkpoint store makes resume reliable.
| Component | Responsibility |
|---|---|
| Orchestrator | Graph/loop with interrupt nodes and resume API |
| Checkpoint store | Persist state across process restarts (Postgres, Redis) |
| Policy engine | Decide which tools/amounts require approval |
| Review UI / channel | Slack, web console, ticketing system |
| Decision API | Accept approve / reject / edit with authz |
| Audit log | Who approved what, when, with which payload |
| Timeout & escalation | Auto-reject, auto-approve (rare), or escalate |
Policy should live outside the LLM when possible: amount thresholds, allowlists of tools, environment flags (prod vs sandbox). The model proposes; policy and humans dispose. Frameworks that expose this cleanly include LangGraph and OpenAI Agents SDK - compare stacks in Best AI Agent Frameworks.
Step-by-Step Flow
Consider a support agent: "Customer C-8842 wants a refund for order O-991 ($240)."
-
Ingest goal - Orchestrator starts a thread with customer context and tools:
search_orders,issue_refund,send_email. -
Read phase (autonomous) - Agent calls
search_orders, confirms charge and eligibility. No interrupt. -
Propose write - Agent prepares
issue_refund(order_id="O-991", amount=240, reason="duplicate charge"). -
Policy check - Amount > $50 → interrupt. State checkpointed under
thread_id. -
Notify reviewer - Slack message to
#refundswith order summary, agent rationale, Approve / Edit / Reject buttons. -
Human decides - Agent edits amount to $120 (partial refund) and approves.
-
Resume - Orchestrator loads checkpoint, applies edited args, executes refund once with idempotency key.
-
Continue - Agent may draft a confirmation email; email send may be a second gate or auto if policy allows.
-
Audit - Store: proposed args, edited args, approver identity, timestamps, tool result.
-
Metrics - Increment approval latency, edit rate, rejection rate for later policy tuning.
Real Production Example
A LangGraph-style interrupt/resume pattern for gated refunds:
from typing import Annotated, Literal, TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, StateGraph
from langgraph.types import Command, interrupt
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
class AgentState(TypedDict):
messages: list
refund_result: str | None
@tool
def search_orders(customer_id: str) -> str:
"""Look up recent orders for a customer."""
return '[{"id": "O-991", "total": 240, "status": "paid"}]'
def should_gate_refund(amount: float) -> bool:
return amount >= 50.0
def agent_node(state: AgentState):
"""LLM decides next action; omitted for brevity — returns a structured proposal."""
# In production: bind_tools + parse tool_calls from the model.
return {
"messages": state["messages"]
+ [
{
"role": "assistant",
"content": "Proposing refund",
"tool_calls": [
{
"name": "issue_refund",
"args": {
"order_id": "O-991",
"amount": 240.0,
"reason": "duplicate charge",
},
}
],
}
]
}
def gated_refund_node(state: AgentState):
last = state["messages"][-1]
args = last["tool_calls"][0]["args"]
if not should_gate_refund(args["amount"]):
result = payments.refund(**args, idempotency_key=f"refund:{args['order_id']}")
return {"refund_result": result}
# Pause for human. Payload is what the UI renders.
decision = interrupt(
{
"type": "approve_refund",
"proposed": args,
"policy": "amount >= 50 requires human approval",
}
)
# decision shape: {"action": "approve"|"reject"|"edit", "args": {...}}
if decision["action"] == "reject":
return {"refund_result": "REJECTED_BY_HUMAN"}
final_args = decision.get("args", args)
result = payments.refund(
**final_args,
idempotency_key=f"refund:{final_args['order_id']}",
)
return {"refund_result": result}
def route_after_agent(state: AgentState) -> Literal["gated_refund", "end"]:
last = state["messages"][-1]
if last.get("tool_calls"):
return "gated_refund"
return "end"
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("gated_refund", gated_refund_node)
builder.set_entry_point("agent")
builder.add_conditional_edges(
"agent",
route_after_agent,
{"gated_refund": "gated_refund", "end": END},
)
builder.add_edge("gated_refund", END)
# Use PostgresSaver / SqliteSaver in production — MemorySaver is for demos.
graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "support-C-8842"}}
# First invoke hits interrupt and returns an interrupt payload.
paused = graph.invoke(
{"messages": [{"role": "user", "content": "Refund order O-991"}], "refund_result": None},
config=config,
)
# Later, after UI collects a decision:
resumed = graph.invoke(
Command(resume={"action": "edit", "args": {"order_id": "O-991", "amount": 120.0, "reason": "partial"}}),
config=config,
)
Key production details: policy outside the model, interrupt payload for the UI, idempotency on the side effect, and a durable checkpointer keyed by thread_id.
Design Decisions
| Decision | Option A | Option B | When to choose |
|---|---|---|---|
| Gate placement | Before every write tool | Threshold / risk-based | Thresholds for scale; every write early in rollout |
| Decision types | Binary approve/reject | Approve / reject / edit | Prefer edit when args are often almost right |
| Reviewer channel | In-app console | Slack / email / ticket | Match where ops already works |
| Timeout | Auto-reject | Escalate / hold forever | Auto-reject for money moves; escalate for customer SLAs |
| Who can approve | Same user | Separate role | Separate role for compliance and fraud control |
| Autonomy growth | Static policy | Policy tuned from metrics | Raise thresholds when edit/reject rates drop |
Comparisons
HITL vs Guardrails
| Dimension | HITL | Guardrails |
|---|---|---|
| Who decides | Named human (or role) | Programmatic rules / models |
| Latency | Minutes–hours typical | Milliseconds–seconds |
| Strength | Judgment, fraud edge cases, compliance sign-off | Schema, PII, allowlists, rate limits |
| Weakness | Reviewer fatigue; not scalable alone | Cannot apply org nuance |
| Compose | Gate after guardrails pass | Block invalid proposals before humans see them |
Use guardrails to shrink the interrupt queue; use HITL for irreversible or gray-area actions. They are complementary, not substitutes.
HITL vs Human-on-the-Loop
| Dimension | Human-in-the-Loop | Human-on-the-Loop |
|---|---|---|
| Default path | Blocks until decision | Runs autonomously |
| Intervention | Required at the gate | Optional / exception-driven |
| Best for | Writes, money, external messages | Monitoring dashboards, low-risk drafts |
| UX | Explicit approve/edit/reject | Alerts, kill switches, sampled review |
Most production systems use both: HITL on privileged tools, human-on-the-loop observability for the rest of the agent loop.
HITL vs Agent Memory
| Dimension | HITL | Agent Memory |
|---|---|---|
| Purpose | Pause for a decision | Persist knowledge across turns/sessions |
| Stores | Checkpoint + decision record | Facts, preferences, embeddings |
| Resumes | Continues the same run with a decision | Recalls context in future runs |
| Does not | Replace long-term knowledge | Create a hard stop before side effects |
HITL needs durable checkpoints (durable execution); agent memory is a different layer. You often want both: remember customer prefs, still require approval on refunds.
Decision tree: when to gate
Decision tree: When to require human approval
flowchart TD
A[Proposed tool call] --> B{Side effect irreversible or external?}
B -->|No| C[Auto-execute]
B -->|Yes| D{Covered by hard guardrails alone?}
D -->|Yes| C
D -->|No| E{Volume too high for review?}
E -->|Yes| F[Raise auto-approve band or redesign]
E -->|No| G[HITL interrupt + durable resume]
Gate irreversible writes; keep reads autonomous; fix policy before drowning reviewers.
⚠ Common Mistakes
-
HITL as a system-prompt instruction - "Ask the user before refunding" is not a hard stop. The runtime must block tool execution.
-
No durable checkpoint - Approvals arrive minutes later; the process is gone. Pair with durable execution.
-
Interrupting every step - Reviewers drown; they rubber-stamp. Gate high-risk actions only.
-
Missing edit path - Force reject/re-run for a typo in amount. Edits save time and tokens.
-
No timeout policy - Threads hang forever. Define reject, escalate, or notify on expiry.
-
Ignoring double-execution - Resume after a network blip can fire the refund twice without idempotency keys.
-
Approver = requester - Self-approval defeats the control for fraud and mistakes.
Where It Breaks Down
-
Latency-sensitive UX - Multi-minute approval kills chat-like product feel. Use async status ("waiting for review") and notify when done.
-
High volume - Thousands of gates per hour need auto-approve bands, batch review UIs, or stronger guardrails before human time.
-
Ambiguous proposals - Humans cannot decide from a bare tool JSON blob. Poor interrupt payloads cause wrong approvals.
-
Model gaming the gate - Agents may split one large refund into many small ones under the threshold. Cap cumulative risk per session.
-
Cross-agent handoffs - Ownership of who must approve gets unclear in multi-agent handoffs. Define approver role per action type, not per agent.
When NOT to Use Human-in-the-Loop
Skip or shrink HITL when:
- The action is read-only or trivially reversible - search, draft, internal summary with no external send.
- You already have hard programmatic blocks - schema validation and allowlists reject the bad cases; humans would only rubber-stamp.
- Volume makes review impossible - thousands of gates/hour without auto-approve bands; fix policy or tooling first.
- Latency UX cannot tolerate a pause - real-time voice or sub-second products; redesign the side effect out of the critical path.
- There is no durable resume path - without checkpoints, "ask a human" forces full reruns; fix durable execution before adding gates.
- Maker-checker is already satisfied elsewhere - a separate BPMN/Temporal approval owns the write; do not double-gate the same action.
Prefer progressive autonomy: broad gates early, then raise thresholds using edit/reject metrics - the path Anthropic and production LangGraph teams describe for earning trust.
Running in Production
Best Practice
✅ Best Practices - Gate irreversible tools by policy, persist checkpoints, require role-separated approvers, log every decision, and measure approval latency and edit rate before widening autonomy.
| Dimension | Consideration |
|---|---|
| Scaling | Checkpoint store and review queue are the bottlenecks. Horizontal workers + Postgres/Redis checkpointers. |
| Latency | Agent pause time is dominated by humans (minutes–hours). Design async UX; do not hold HTTP requests open. |
| Cost | Checkpoints are cheap vs. replaying full runs. Edits beat reject-and-retry for token cost. |
| Monitoring | Track interrupt rate, time-to-decision, approve/edit/reject mix, timeout rate, post-approval tool errors. |
| Evaluation | Replay traces with simulated approvals. Assert gated tools never execute without a decision record. |
| Security | AuthZ on resume API, signed decision tokens, PII-minimized review payloads, full audit trail. |
Important
Never treat a webhook or Slack button as authenticated by obscurity. Bind resume tokens to
thread_id, reviewer identity, and expiry - and verify the proposed action hash so a forged resume cannot swap tool arguments.
Common Mistake
Building HITL without idempotent side effects. An approved refund that retries on resume will create duplicate payments. Wrap every gated write with an idempotency key derived from thread + action + args.
Related Guides
Frameworks & rankings
- LangGraph - First-class
interrupt/ resume with checkpointers. - OpenAI Agents SDK - Tool approval hooks for sensitive function calls.
- CrewAI · AutoGen · Semantic Kernel
- Best AI Agent Frameworks · LangGraph vs CrewAI
Concept guides
- AI Agents - The control loop HITL interrupts.
- Durable Execution - Checkpointing and resume that make HITL reliable.
- Multi-Agent Handoffs - Delegation across specialists; often combined with approval gates.
- Multi-Agent Systems - Broader collaboration patterns.
- Agent Memory - What state to persist across interrupt gaps.
- Guardrails - Programmatic blocks that reduce how often humans must intervene.
- Tool Calling - The actions you choose to gate.
Learning path
If you understood this topic, read next:
Diagram: Learning path for HITL controls
flowchart LR
A[Durable] --> B[HITL]
B --> C[Handoffs]
C --> D[Memory]
D --> E[Guardrails]
Prerequisites: AI Agents · Tool Calling
Next topics: Durable Execution · Multi-Agent Handoffs · Guardrails
Key Takeaways
- HITL is a suspendable control-plane gate, not a polite prompt asking the model to be careful.
- Gate irreversible and externally visible writes; keep reads and drafts autonomous to avoid reviewer fatigue.
- Interrupt/resume only works with durable checkpoints and idempotent side effects.
- Pair with guardrails: programs shrink the queue; humans handle judgment and compliance.
- Progressive autonomy: raise auto-approve thresholds only after edit/reject/timeout metrics justify it.
- Clarify approver roles across multi-agent handoffs so specialists do not invent conflicting policies.
- Compare orchestration stacks via Best AI Agent Frameworks and LangGraph vs CrewAI.
FAQs
Is human-in-the-loop the same as asking the user clarifying questions?
No. Clarifying questions are conversational turns. HITL is a hard interrupt before a side effect, with durable state and an explicit approve/reject/edit decision.
Do I need HITL if I have guardrails?
Yes for judgment and irreversible actions. Guardrails enforce rules; humans handle exceptions, fraud edge cases, and policy gray areas.
Which tools should require approval?
Start with anything that moves money, sends external messages, deletes data, changes IAM, or deploys. Expand or shrink based on eval risk and volume.
How do I avoid reviewer fatigue?
Raise auto-approve thresholds for low-risk bands, batch similar approvals, improve interrupt summaries, and fix root causes of frequent edits.
What happens if the reviewer never responds?
Define timeout: reject, escalate to a backup role, or (rarely) auto-approve within a tight policy. Never leave unbounded open threads without alerting.
Can the same user who triggered the agent approve the action?
Technically yes; for compliance-sensitive actions, require a different role (maker-checker).
How does HITL interact with streaming UIs?
Stream progress until the interrupt, then show a waiting state with the proposal. On resume, stream continuation. Do not block the original HTTP request for hours.
What should be in the interrupt payload?
Tool name, args, policy reason, short context, customer/account IDs, risk score, and links to source records - enough to decide without opening five other tools.
How do I test HITL in CI?
Simulate interrupt + resume with a fake decision API. Assert gated tools are not invoked before resume, and that edited args are what execute.
Does HITL work with multi-agent systems?
Yes. Put gates on tools or on handoff boundaries. Clarify which role owns approval so specialists do not each invent their own policy.
Is Slack a good approval channel?
It works for ops teams already in Slack. Harden with signed callbacks, role checks, and an audit mirror in your system of record.
How is this different from Temporal signals or BPMN approvals?
Same idea at the workflow layer. Agent HITL applies that pattern to LLM tool loops. Many teams combine Temporal/durable workflows with an LLM planner.
Can I auto-approve after N successful human approvals?
Yes as progressive autonomy - but reset or tighten policy when prompts, tools, or models change. Past approvals are not a permanent license.
What metrics prove HITL is working?
Low timeout rate, falling edit/reject rates over time, zero ungated writes in audit samples, and acceptable p50 time-to-decision for your SLA.
References
- LangGraph Human-in-the-Loop
- OpenAI Agents SDK - Human-in-the-loop
- Anthropic - Building effective agents