LangGraph Tutorial (Python): adding human-in-the-loop for beginners

By Cyprian AaronsUpdated 2026-04-22
langgraphadding-human-in-the-loop-for-beginnerspython

This tutorial shows you how to add a human approval step to a LangGraph workflow in Python. You need this when an agent is about to do something risky, like send an email, approve a refund, or call an external API that changes data.

What You'll Need

  • Python 3.10+
  • langgraph
  • langchain-core
  • langchain-openai
  • An OpenAI API key set as OPENAI_API_KEY
  • Basic familiarity with LangGraph nodes, edges, and state
  • A terminal and a virtual environment

Step-by-Step

  1. Start by installing the packages and setting your API key. If you already have LangGraph basics working, this is just making sure the dependencies are current and the model can be called.
pip install -U langgraph langchain-core langchain-openai
export OPENAI_API_KEY="your-api-key"
  1. Define the graph state and the node that asks for human approval. The key idea is simple: the graph produces a draft action, then pauses so a person can inspect it before execution.
from typing import TypedDict, Annotated
from operator import add

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt

class AgentState(TypedDict):
    messages: Annotated[list, add]
    draft_action: str
    approved: bool

llm = ChatOpenAI(model="gpt-4o-mini")

def create_draft(state: AgentState):
    response = llm.invoke(state["messages"])
    return {"messages": [response], "draft_action": response.content}

def human_review(state: AgentState):
    decision = interrupt({
        "draft_action": state["draft_action"],
        "question": "Approve this action? Reply yes or no."
    })
    return {"approved": str(decision).strip().lower() == "yes"}
  1. Add the execution node and wire the graph together. In production, this is where you keep the dangerous side effect behind the approval gate.
def execute_action(state: AgentState):
    if not state["approved"]:
        return {"messages": [AIMessage(content="Action rejected by human reviewer.")]}
    
    result = f"Executed approved action: {state['draft_action']}"
    return {"messages": [AIMessage(content=result)]}

builder = StateGraph(AgentState)
builder.add_node("create_draft", create_draft)
builder.add_node("human_review", human_review)
builder.add_node("execute_action", execute_action)

builder.add_edge(START, "create_draft")
builder.add_edge("create_draft", "human_review")
builder.add_edge("human_review", "execute_action")
builder.add_edge("execute_action", END)

graph = builder.compile()
  1. Run the graph with a checkpoint so it can pause and resume after human input. Without persistence, the interrupt is not useful because you need a way to continue from the paused state.
from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "review-001"}}
initial_state = {
    "messages": [HumanMessage(content="Draft a refund approval message for customer ACME.")],
    "draft_action": "",
    "approved": False,
}

result = graph.invoke(initial_state, config=config)
print(result)
  1. Resume after review by sending the human decision back into the same thread. This pattern works well for CLI tools, internal dashboards, or queue-based review systems where a reviewer approves work asynchronously.
# First call pauses at interrupt; inspect payload in your app/UI.
paused = graph.invoke(initial_state, config=config)

# Resume with reviewer decision.
resumed = graph.invoke(
    None,
    config=config,
    interrupt={
        "decision": "yes"
    }
)

print(resumed["messages"][-1].content)

Testing It

Run the script once with a simple prompt like “Draft a refund approval message for customer ACME.” The graph should stop at human_review and expose the draft action for review.

If you are wiring this into a UI or backend service, verify that the paused state is tied to a stable thread_id. That is what lets you resume the exact workflow later instead of starting over.

Test both paths: approve and reject. On approval, the final node should execute; on rejection, it should return a rejection message without performing the side effect.

If your app uses multiple reviewers or role-based approvals, make sure only authorized users can resume interrupted runs.

Next Steps

  • Add structured review payloads instead of plain yes/no decisions.
  • Store interrupts in Postgres or Redis using a durable checkpointer.
  • Wrap risky tools behind approval gates so only specific actions require human review.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides