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

By Cyprian AaronsUpdated 2026-04-21
langchainadding-human-in-the-loop-for-beginnerspython

This tutorial shows how to pause a LangChain workflow, send a tool decision or draft response to a human for review, and then continue only after approval. You need this when the model is handling risky actions like sending emails, updating customer records, or generating compliance-sensitive output.

What You'll Need

  • Python 3.10+
  • langchain
  • langchain-openai
  • langgraph
  • An OpenAI API key in OPENAI_API_KEY
  • Basic familiarity with LangChain chat models and messages
  • A terminal and a virtual environment

Install the packages:

pip install langchain langchain-openai langgraph

Set your API key:

export OPENAI_API_KEY="your-key-here"

Step-by-Step

  1. First, build a simple agent state that can hold the user message, the model draft, and the human decision. LangGraph is the cleanest way to add human-in-the-loop because it lets you pause execution between nodes.
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    draft: str
    approved: bool

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
  1. Next, create a node that generates a draft response. In a real app this could be an email reply, a policy explanation, or a proposed database action.
def generate_draft(state: AgentState):
    messages = state["messages"]
    response = llm.invoke(messages)
    return {
        "draft": response.content,
        "messages": [AIMessage(content=response.content)],
        "approved": False,
    }
  1. Now add the human review step. For beginners, the simplest pattern is to print the draft and ask for approval in the terminal before continuing.
def human_review(state: AgentState):
    print("\n--- DRAFT FOR REVIEW ---")
    print(state["draft"])
    decision = input("\nApprove? (y/n): ").strip().lower()
    approved = decision == "y"
    return {"approved": approved}
  1. After review, route execution based on the human decision. If approved, continue to the final node; otherwise stop and return a rejection message.
def finalize(state: AgentState):
    return {
        "messages": [AIMessage(content="Approved by human reviewer. Proceeding.")],
    }

def route_after_review(state: AgentState):
    return "finalize" if state["approved"] else END
  1. Wire the graph together and run it with an initial user message. This gives you a working human-in-the-loop flow you can extend later with tools or structured outputs.
workflow = StateGraph(AgentState)

workflow.add_node("generate_draft", generate_draft)
workflow.add_node("human_review", human_review)
workflow.add_node("finalize", finalize)

workflow.set_entry_point("generate_draft")
workflow.add_edge("generate_draft", "human_review")
workflow.add_conditional_edges("human_review", route_after_review)

app = workflow.compile()

result = app.invoke({
    "messages": [HumanMessage(content="Write a polite reply to a customer asking for refund status.")],
    "draft": "",
    "approved": False,
})

print("\n--- FINAL STATE ---")
print(result)
  1. If you want this pattern in production, move the review step out of input() and into your UI or ticketing system. The graph stays the same; only the approval source changes.
# Example shape for external approval handling:
# 1) run generate_draft
# 2) store state["draft"] in your DB
# 3) wait for reviewer action in your app/UI
# 4) resume graph with approved=True/False

def resume_after_human(draft: str, approved: bool):
    return {
        "messages": [HumanMessage(content="resume")],
        "draft": draft,
        "approved": approved,
    }

Testing It

Run the script and enter a user request that produces a draft you can inspect. When prompted, type y to approve or n to stop execution.

If you approve it, the graph should continue to the final node and print the final state with approved=True. If you reject it, execution should end after review without reaching finalize.

Test both paths at least once so you know routing works correctly. Also try changing the prompt to something sensitive like “Draft an account closure email” so you can see why human review matters.

Next Steps

  • Add structured output with Pydantic so reviewers approve fields instead of raw text.
  • Replace terminal input with a web UI or internal admin panel.
  • Add tool calls before review so humans can approve actions like refunds, account updates, or outbound messages.

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