This is the fourth post in our LangGraph fundamentals series, building on conditional edges and the agent-loop concepts from earlier in this series.
We flagged in our LangChain series that a tool taking a consequential action — a refund, an account change — needs safeguards beyond just trusting an agent's judgment. Pausing for actual human approval before a consequential step proceeds is one of the most direct safeguards available, and it needs real infrastructure: a graph has to genuinely stop, potentially for hours or days, and later resume in exactly the state it left off in. That's what LangGraph's checkpointing provides.
Interrupts: pausing a graph deliberately
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
class RefundState(TypedDict):
request_amount: float
customer_id: str
approved: bool
result: str
def evaluate_refund(state: RefundState) -> RefundState:
if state["request_amount"] > 500:
state["approved"] = False # requires human review — set explicitly, not assumed
else:
state["approved"] = True
return state
def process_refund(state: RefundState) -> RefundState:
state["result"] = f"Refund of ${state['request_amount']} processed for {state['customer_id']}"
return state
graph = StateGraph(RefundState)
graph.add_node("evaluate_refund", evaluate_refund)
graph.add_node("process_refund", process_refund)
graph.set_entry_point("evaluate_refund")
graph.add_edge("evaluate_refund", "process_refund")
graph.add_edge("process_refund", END)
checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer, interrupt_before=["process_refund"])interrupt_before=["process_refund"] tells the graph to stop before that specific node runs, no matter what any conditional logic decided — the graph reaches that point and genuinely pauses, returning control to your application rather than continuing automatically.
Resuming after a real decision
config = {"configurable": {"thread_id": "refund-request-4471"}}
result = app.invoke(
{"request_amount": 750, "customer_id": "cust-123", "approved": False, "result": ""},
config=config,
)
print("Paused. Current state:", app.get_state(config).values)Paused. Current state: {'request_amount': 750, 'customer_id': 'cust-123', 'approved': False, 'result': ''}
Execution has genuinely stopped here — not just slowed down, actually halted, with its exact state persisted via the checkpointer and addressable later through thread_id. A human reviewer — hours or days later, in an entirely separate process, possibly on an entirely different machine — can approve and resume:
# Later, potentially in a completely different process, after a human reviews the request
app.update_state(config, {"approved": True})
final_result = app.invoke(None, config=config)
print(final_result["result"])Refund of $750 processed for cust-123
app.invoke(None, config=config) — passing None as input — resumes a graph from exactly where it left off, using the thread_id to look up its persisted state, rather than starting over from the entry point. This is the mechanism that makes "pause for a genuinely long time, then resume exactly where you stopped" actually work, rather than requiring the whole workflow to somehow stay running and blocked the entire time a human takes to respond.
Persisting checkpoints beyond a single process
MemorySaver stores checkpoints in memory — genuinely fine for local development and testing, and gone the instant the process restarts, which is a real problem for anything actually waiting on a human who might take hours. Production deployments use a persistent checkpointer instead:
from langgraph.checkpoint.postgres import PostgresSaver
checkpointer = PostgresSaver.from_conn_string("postgresql://localhost/langgraph_checkpoints")
app = graph.compile(checkpointer=checkpointer, interrupt_before=["process_refund"])The same swap-the-backend pattern covered repeatedly throughout this blog — Terraform's remote state backend, LangChain's swappable vector stores and chat history backends — applied here to checkpoint persistence: a graph paused on a request now survives a server restart, a redeploy, or the application running across multiple instances, because its actual state lives in Postgres, not in one specific process's memory.
Designing what actually requires human approval
Not every action needs a human in the loop, and treating every single step as needing approval defeats most of the point of automating a workflow at all. The pattern worth adopting deliberately: identify the specific, genuinely consequential decisions — above a dollar threshold, an irreversible action, anything with real legal or safety weight — and interrupt specifically before those, while everything else in the same graph runs fully automated. The evaluate_refund node above already embodies this: small refunds proceed automatically; only the larger, more consequential ones reach the interrupt point at all.
What to actually remember from this post
interrupt_beforegenuinely pauses a graph's execution at a specific node, not just an optional slowdown — real infrastructure is required to make this actually work.- A checkpointer persists a graph's exact state, addressable later by
thread_id, which is what allows resuming a graph that paused for hours or days, from a different process entirely. invoke(None, config=...)resumes from a checkpoint rather than restarting a graph's execution from its entry point.- Use a persistent checkpointer (Postgres or similar) in production —
MemorySaverdoesn't survive a process restart, a real problem for anything genuinely waiting on human timescales.
Next in the series: Multi-Agent Systems with LangGraph, the final post — covering how multiple distinct agents, each with their own responsibilities, coordinate within a single graph.
