LangGraph Human-in-the-Loop (HITL)
Human-in-the-loop (HITL) is the mechanism that allows a LangGraph agent to pause execution at a specific node, wait for a human decision, and then resume exactly where it left off—preserving the entire workflow state. It’s the critical bridge between fully autonomous agents and the real‑world need for oversight, approval, and correction. This article is a practical, implementation‑focused guide to building HITL workflows with LangGraph: how to pause, inject human input, and resume reliably in production.
What Is Human-in-the-Loop in LangGraph
In LangGraph, HITL is implemented through two core concepts:
- Interrupt – A function that, when called inside a node, halts the graph’s execution and saves the current state. It can optionally surface a prompt for the human.
- Resume – A subsequent graph invocation (with the same
thread_id) that provides the human’s answer and continues execution from the suspended node.
The entire state—including conversation history, intermediate results, and tool outputs—is safely checkpointed during the pause. When the human responds, the graph picks up inside the same node, with the interrupt call returning the human’s input as its value.
A simple mental model:
User Input → Node A → Pause (Interrupt) → [human waits] → Human Input → Resume → Node B → End
This isn’t an external polling loop or a work queue; it’s a first‑class language feature of the graph runtime.
Why Human-in-the-Loop Matters
Autonomous agents are powerful, but many workflows require a human in the decision loop for safety, quality, or compliance. HITL patterns directly enable:
- Safety‑critical workflows – Prevent an agent from executing a high‑risk action (e.g., a large financial transfer) without explicit approval.
- Decision validation – Let a human verify the agent’s reasoning before it takes effect.
- Compliance requirements – Financial services, healthcare, and legal applications often mandate human review.
- Quality control – A human editor can polish AI‑generated content, code, or translations.
- Enterprise approval processes – Integrate existing approval hierarchies (manager, director) into the automated workflow.
- Reducing agent errors – Human intervention catches hallucinations, incorrect tool usage, and policy violations before they propagate.
Consider an HR onboarding agent: it can fill out forms, but a manager must approve the final contract. HITL makes that approval a seamless step inside the same automated process.
How Human-in-the-Loop Works in LangGraph
The lifecycle of a HITL step is deterministic and fully integrated with LangGraph’s state machine.
- Execution reaches the pause node – The node calls
interrupt(value)with a value that describes what the human needs to decide. - Graph halts – The node does not return. The runtime creates a checkpoint that stores the entire state, the pending interrupt, and the next nodes.
- Control returns to the caller – The
invoke()orstream()call returns with anInterruptexception (or the state up to the pause, depending on the API). - External system presents the prompt – The developer’s UI or backend receives the interrupt value and requests human input.
- Human provides input – A new graph invocation is made with
Command(resume=human_input)and the samethread_id. - Graph resumes – The runtime loads the latest checkpoint, sees the pending interrupt, and re‑enters the same node. The
interrupt()call now returnshuman_input. - Node finishes – The node processes the human input, updates the state, and returns normally. The workflow continues to the next node.
Key property: the node code that called interrupt() does not need to know it was paused. It simply receives the human’s response and continues.
Core HITL Mechanisms
Interrupt / Pause Execution
The interrupt(value) function is imported from langgraph.types. It can be called inside any node.
from langgraph.types import interrupt
def approval_node(state: State):
# Pause and ask a question
decision = interrupt({"question": "Approve this transaction?", "details": state["tx"]})
# After resume, `decision` contains the human's answer
state["approved"] = decision.get("approved", False)
return state
- Execution stops immediately – The node does not proceed past the
interruptcall. - State is preserved – The checkpointer saves the state as it was before the interrupt, along with the interrupt payload.
- Multiple interrupts per graph – You can call
interruptin multiple nodes; the runtime handles them in order.
Resume Execution
To resume, you invoke the graph again with the same thread_id and provide a Command object containing a resume value.
from langgraph.types import Command
config = {"configurable": {"thread_id": "tx-123"}}
# First invocation (will pause)
graph.invoke({"tx": {...}}, config)
# Later, after human decision
graph.invoke(Command(resume={"approved": True}), config)
The resume value must be serialisable (a dict, string, etc.). It is returned directly by the interrupt call inside the node.
Human Input Injection
The resume value becomes the output of interrupt(). Your node code decides how to use it. Best practice:
- Define a clear structure for the interrupt prompt and the resume answer.
- Validate the human’s input inside the node after resumption.
- Update state fields accordingly (
approved,edited_text,feedback, etc.).
HITL Design Patterns
Four recurring patterns cover most real‑world HITL needs.
1. Approval Workflow Pattern
A human must explicitly approve an action before the agent executes it.
Node code:
def approval_node(state):
decision = interrupt({"action": "transfer", "amount": state["amount"]})
state["approved"] = decision.get("approved", False)
return state
2. Review & Edit Pattern
The agent generates a draft (an email, a document, a piece of code). The human reviews and optionally edits the content before it is finalised.
def review_node(state):
edited = interrupt({"draft": state["draft_email"], "instruction": "Edit or approve"})
# If the human returns an edited version, use it; otherwise keep original
state["final_email"] = edited.get("text", state["draft_email"])
return state
3. Escalation Pattern
The agent tries to handle a request, but when confidence is low or a situation is unfamiliar, it escalates to a human who provides instructions or takes over.
def escalation_node(state):
if state.get("confidence", 1.0) < 0.8:
human_guidance = interrupt({"issue": state["query"], "suggested_action": state["plan"]})
state["plan"] = human_guidance.get("override", state["plan"])
return state
4. Safety Gate Pattern
Certain operations are always blocked until a human explicitly confirms. This is a variant of the approval pattern, but the pause is unconditional—the agent cannot proceed without human consent.
- Use when the cost of an error is extremely high (e.g., deleting a production database, sending a bulk email).
- Implement by placing the
interruptbefore the sensitive tool call, with no automated bypass.
State Management in HITL
State behavior during HITL is critical for a consistent resumption.
- State preservation – When the graph pauses, the current state (all keys, all messages) is checkpointed. No data is lost.
- State mutation after human input – The node that called
interruptis responsible for updating the state with the human’s input. The resume value does not automatically modify the state; your code does. - State consistency – Because the node resumes mid‑function, local variables (before the interrupt) are gone unless you store them in the state dictionary. Do not rely on local variables across an interrupt.
Best practice: put everything the resuming node needs into the state before calling interrupt.
Checkpointing in HITL
HITL depends on persistent checkpointing. Without a checkpointer, the state will be lost when the process stops.
- Before pause – The runtime automatically creates a checkpoint that includes the interrupt payload. The checkpoint marks the graph as “interrupted.”
- After resume – A new checkpoint is created with the updated state after the node finishes. The previous interrupt state is cleared.
- Failure recovery – If the service restarts after the pause but before the human responds, the interrupt is still stored in the checkpoint. A new invocation with the same
thread_idwill immediately re‑raise theInterrupt(or, if you useCommand(resume=…), it will resume).
Always use a persistent checkpointer (SQLite or Postgres) for HITL workflows. In‑memory checkpointers are fine for development but disappear on restart.
Human Input Types
The resume value can be as simple or as structured as needed.
| Input Type | resume value | Node usage |
|---|---|---|
| Binary Approval | {"approved": True} or False | Simple yes/no gate |
| Structured Feedback | {"approved": True, "comment": "Looks good"} | Audit trail with reasoning |
| Edited Output | {"text": "corrected version...", "approved": True} | Content review & edit |
| New Instructions | {"plan": "Use alternative vendor X"} | Escalation, human takes over |
Always define a schema for both the interrupt payload and the expected resume value. This can be done with Pydantic models, shared between your graph and your UI/API.
Workflow Control in HITL
You have fine‑grained control over when and how pauses happen.
- Conditional pause points – Decide at runtime whether to call
interrupt. For example, only ask for approval if the transaction amount exceeds $10,000. - Dynamic interruptions – The graph can pause in response to an LLM’s decision (e.g., the agent itself asks for help).
- Multiple review stages – A single workflow can have several HITL steps (e.g., draft review by a junior, final approval by a senior). Each pause is independent and checkpointed.
- Interrupt before/after nodes – Use
compile(interrupt_before=["node_name"])to pause before a node runs, without modifying the node code. This is useful for retrofitting HITL onto existing graphs.
graph = builder.compile(checkpointer=checkpointer, interrupt_before=["critical_action"])
HITL in Production Systems
Production HITL moves from “cool feature” to “critical infrastructure.” Key considerations:
- Auditability – Log every pause and resume:
thread_id, timestamp, human ID, decision. The checkpoint table itself provides an immutable record. - Compliance logging – Store the interrupt payload and the resume value in a separate audit log for regulatory purposes.
- User experience design – The human reviewer needs clear context. Send a well‑formatted prompt (derived from state) to a dashboard, Slack, email, or a custom queue.
- Latency considerations – The graph pauses; the human may take minutes to days. The system must not hold resources during this time. The checkpointer acts as the durable state store.
- Scaling human review pipelines – Multiple agents can share a common review queue. Workers pull tasks, see the interrupt prompt, and push back a
Command(resume=…)to the graph API. Ensure the API can route the samethread_idto the same graph instance or use a distributed database.
A common production setup:
- Graph pauses → interrupt payload is pushed to a message queue (e.g., SQS, Kafka).
- A human review UI picks up the task, renders the state.
- Human submits decision → backend calls
graph.invoke(Command(resume=decision), config). - Graph finishes and returns final result.
Error Handling in HITL
Humans are unpredictable; your workflow must be resilient.
- No response scenarios – Implement a timeout. If the human doesn’t respond within a SLA, you can resume the graph with a default
resumevalue (e.g.,{"approved": False}) or an escalation flag. - Timeout handling – A separate scheduler can check for stalled threads and programmatically invoke the graph with a timeout decision.
- Rejection flows – The node must handle a rejected decision gracefully (e.g., send a polite message, roll back partial work).
- Retry logic – The human may ask for more information. Build a loop: the node can call
interruptagain if the input is incomplete.
def approval_with_retry(state):
while True:
decision = interrupt({"question": state["question"]})
if decision.get("ready"):
state["decision"] = decision
break
# Otherwise, add a message and loop again
state["messages"].append(("human", "Please provide more detail."))
return state
Common Beginner Mistakes
- Too many pause points – Every
interruptadds latency and human effort. Only pause where truly necessary. - Poor state design – The resuming node needs enough context. Don’t rely on variables that aren’t in state.
- Not persisting checkpoints – Testing HITL with an in‑memory checkpointer works until you restart the server. Then all paused threads are lost.
- Blocking critical paths unnecessarily – A pause that occurs on every invocation in a high‑traffic flow creates a bottleneck. Use conditional pauses.
- No fallback mechanism – If the human never responds, the thread is stuck forever. Always have a timeout or default path.
- Misunderstanding resume invocation – Forgetting to use the same
thread_idor not passing aCommandwill start a new run, not resume the old one.
Best Practices
- Define clear pause points – Name your HITL nodes descriptively (
approval_node,review_node) and document what decision is required. - Keep human input structured – Use
TypedDictor Pydantic for the resume payload. Validate it in the node. - Use persistent checkpoints for every pause – Never run HITL in production without a database‑backed checkpointer.
- Minimise latency in resume flows – The resume call should be fast. Avoid heavy computation in the same node that calls
interrupt; do it before or after. - Log all human decisions – Store the
thread_id, the human ID, the prompt, and the response. This is your audit trail. - Design fallback paths – Every HITL step should have a “no response” or “rejection” edge.
- Separate the HITL UI from the graph logic – The graph just calls
interrupt. The frontend reads the interrupt payload from the state and presents it; it doesn’t need to know the graph internals.
Practical Example: Financial Transaction Approval Agent
We’ll build an agent that validates a transaction, performs a risk assessment, pauses for human approval if the amount exceeds $5,000, and then executes or rejects the transfer.
State
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
class TxState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
transaction: dict # {amount, recipient, ...}
risk_score: float
approved: bool
executed: bool
final_message: str
Nodes
def validation_node(state: TxState):
tx = state["transaction"]
# Simulate basic validation
if tx.get("amount", 0) <= 0:
state["final_message"] = "Invalid amount."
state["executed"] = False
return state
return state
def risk_assessment_node(state: TxState):
# Simulated risk score
amount = state["transaction"]["amount"]
state["risk_score"] = min(amount / 10000, 1.0)
return state
def human_approval_node(state: TxState):
if state["transaction"]["amount"] < 5000:
state["approved"] = True # auto-approve low-value
return state
# Pause for high-value transactions
decision = interrupt({
"type": "approval",
"transaction": state["transaction"],
"risk_score": state["risk_score"]
})
state["approved"] = decision.get("approved", False)
return state
def execution_node(state: TxState):
if state["approved"]:
# Execute the transfer (mock)
state["executed"] = True
state["final_message"] = f"Transaction {state['transaction']['id']} executed."
else:
state["executed"] = False
state["final_message"] = "Transaction rejected by human."
return state
Graph assembly
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
builder = StateGraph(TxState)
builder.add_node("validate", validation_node)
builder.add_node("risk", risk_assessment_node)
builder.add_node("approval", human_approval_node)
builder.add_node("execute", execution_node)
builder.add_edge(START, "validate")
builder.add_edge("validate", "risk")
builder.add_edge("risk", "approval")
builder.add_edge("approval", "execute")
builder.add_edge("execute", END)
checkpointer = MemorySaver() # use PostgresSaver in production
graph = builder.compile(checkpointer=checkpointer)
Execution with human pause
config = {"configurable": {"thread_id": "tx-456"}}
# Start the workflow
graph.invoke({
"messages": [("user", "Transfer $7,500 to account 12345")],
"transaction": {"id": "tx-456", "amount": 7500, "recipient": "12345"}
}, config)
# Graph pauses at human_approval_node. The invoke call returns state up to the pause.
# Human reviews and approves (from a different process)
from langgraph.types import Command
graph.invoke(Command(resume={"approved": True}), config)
# Workflow continues: execution_node runs, final_message set to "Transaction tx-456 executed."
State during the pause: transaction, risk_score, and all messages are saved. The human approval prompt includes the transaction details. Resumption is seamless.
HITL and Other LangGraph Concepts
HITL is not a standalone feature; it’s woven into the fabric of LangGraph.
- Core Concepts – HITL builds directly on the graph state, nodes, and edges.
interruptis a core graph primitive. - Workflows – HITL patterns extend workflow design. Every pause is a branching point controlled by human input.
- Memory – Persistent checkpointing is the enabler; human‑approved decisions become part of the agent’s long‑term memory.
- Tool Calling – Often the action being approved is a tool call (e.g., a database write). Pause before the tool node.
- Production – HITL demands production‑grade checkpointing, observability, and error handling.
Every production‑ready LangGraph application that interacts with sensitive systems will likely use human‑in‑the‑loop. Mastering these patterns is essential.
FAQ
1. What is Human-in-the-Loop in LangGraph?
It is the mechanism that lets a graph pause at a node, wait for a human’s input, and then resume execution from that exact point with the human’s answer.
2. How do you pause execution?
Call the interrupt(value) function inside a node. The graph stops, saves a checkpoint, and waits.
3. How is state preserved during HITL?
The checkpointer automatically saves the full state when the interrupt occurs. On resumption, the state is restored.
4. Can multiple human approvals be used in one workflow?
Yes. You can call interrupt in multiple nodes. Each pause is independent and requires a separate Command(resume=…) to continue.
5. How does resume work?
Invoke the graph with the same thread_id and pass a Command(resume=…). The graph reloads the checkpoint and returns the resume value from the interrupt call.
6. What happens if a human never responds?
The thread stays paused indefinitely. You should implement a timeout mechanism that programmatically resumes with a default or rejection value.
7. Is HITL suitable for production?
Yes, provided you use a persistent checkpointer (Postgres), log decisions, and handle timeout/rejection scenarios.
8. Do I need to modify my node to support HITL?
You insert a call to interrupt(). Everything before the call must be re‑executable; everything after will run after the human responds.
9. Can I pause before a node without modifying it?
Yes. Use interrupt_before=["node_name"] when compiling the graph. The graph will pause before entering that node.
10. What does interrupt return after resume?
It returns exactly the value passed in Command(resume=…) — typically a dictionary with the human’s decision.
11. How do I pass context to the human reviewer?
The argument to interrupt becomes part of the checkpoint. You can include any state fields needed for the review.
12. Can the human input be validated?
Yes, inside the node after resumption. The node can call interrupt again if the input is invalid.
13. Does HITL work with streaming?
Yes. Streaming will deliver events up to the pause. After resume, streaming continues with the subsequent nodes.
14. How do I restart a graph that was interrupted but the resume got lost?
If you know the thread_id, you can replay the graph from the beginning (if no side‑effects) or manually supply a resume command. In practice, always store pending interrupts in a queue.
15. Can I use HITL with sub‑graphs?
Yes. An interrupt inside a sub‑graph pauses the entire parent graph. The parent’s checkpointer captures the state.
Conclusion
LangGraph’s Human‑in‑the‑Loop mechanism transforms autonomous agents into reliable, human‑governed systems. By pausing execution at precisely the right nodes and resuming with structured human input, you can build agents that are both powerful and trustworthy.
Key takeaways:
- HITL =
interrupt+ persistent checkpointing +Command(resume=…). - Patterns like approval, review‑edit, escalation, and safety gates cover real‑world requirements.
- State is checkpointed automatically; resumption is seamless.
- Production deployment requires persistent storage, timeout handling, and audit logging.
Now, take your agent workflows to the next level:
- Taking LangGraph to Production – deploy your HITL agents at scale.
- LangGraph Workflows – design robust, composable workflows.
- LangGraph Memory – understand checkpointing and state persistence.
- LangGraph Tool Calling – securely call tools with human approval.
For fundamentals, revisit the LangGraph Core Concepts and the framework overview. Build agents that pause, reflect, and earn human trust.