Issue 13 | Human in the Loop (HITL): Setting Up an Approval Node

Updated on 4/14/2026

Subtitle of this issue: Before the writer starts writing based on the outline, use interrupt_before to wait for a human to click "Approve" before executing.

Welcome back, geeks, to the LangGraph Multi-Agent Masterclass. I'm your old friend, an AI architect who has been in the coding trenches for a decade.

In the previous 12 issues, our "AI Content Agency" has taken shape. We have a strategizing Planner, a well-read Researcher, and a hard-working Writer. Watching them automatically flow through the code, do you feel the thrill of being a cyber capitalist?

However, reality often gives you a resounding slap in the face.

Yesterday, my Planner had a wild idea and created an outline for an article on "Quantum Computing", which surprisingly included a chapter titled "How to Use Quantum Computing to Trade Stocks and Achieve Financial Freedom". Then, the Writer unhesitatingly wrote 3,000 words of nonsense following this absurd outline. The result? Not only did it waste a massive amount of Tokens (which is cold, hard cash!), but it also produced a pile of unusable garbage.

What does this show? A completely unbridled AI is dangerous and expensive. At critical decision nodes, we need to introduce human intelligence for control. This is our topic today: Human in the Loop (HITL).

Today, we will introduce a real "Editor-in-Chief" to our Agency—that is, you in front of the screen. After the Planner finishes the outline and before the Writer starts writing, the system must stop and wait for you to shout "Approved" before the workflow can continue.


🎯 Learning Objectives for this Issue

Through this practical session, you will master the following skills:

  1. Understand the underlying logic of HITL: Why is the persistence of Graph State a prerequisite for implementing Human in the Loop?
  2. Master the magic of interrupt_before: How to precisely set breakpoints during the LangGraph compilation phase, making the workflow hit the brakes at the edge of the cliff.
  3. Proficiently use Thread ID: Learn how to wake up a sleeping (suspended) graph and let it continue sprinting along its previous state.
  4. Refactor the Agency workflow: Build an indestructible "human approval wall" between the Planner and the Writer.

📖 Principle Analysis

In traditional code logic, a program is either running or terminated. If you want the program to stop halfway and wait for you, you usually can only use input() to block the thread, but this is absolutely unfeasible in modern asynchronous network services (like Web APIs).

How does LangGraph elegantly solve this problem? The answer is: State Snapshot + Checkpointer mechanism.

Imagine you are playing a single-player game (like Black Myth: Wukong). You just defeated the first Boss (Planner generated the outline), and ahead is an even harder Boss (Writer starts writing the long article). To prevent failing, what would you do? Save Game!

LangGraph's HITL follows this exact logic:

  1. When the graph runs to the breakpoint you set (e.g., interrupt_before=["writer"]).
  2. LangGraph will use a Checkpointer (like MemorySaver or a database) to take a snapshot of all current States and save it.
  3. Then, the graph's execution terminates directly (suspends), releasing computing resources.
  4. Wait for the human to review the outline on the frontend UI and click the "Approve" button.
  5. We re-invoke the graph with the corresponding thread_id (the save slot), and LangGraph will read the snapshot and seamlessly resume execution from the breakpoint.

As always, a picture is worth a thousand words. Let's look at our refactored Agency workflow today:

stateDiagram-v2
    %% Define styles
    classDef aiNode fill:#e1f5fe,stroke:#0288d1,stroke-width:2px,color:#000
    classDef humanNode fill:#fff3e0,stroke:#f57c00,stroke-width:2px,stroke-dasharray: 5 5,color:#000
    classDef stateNode fill:#e8f5e9,stroke:#388e3c,stroke-width:2px,color:#000

    %% Node definitions
    Start((START))
    Planner[Planner Agent\n(Generate content outline)]:::aiNode
    Checkpoint[(Checkpointer\nSave state snapshot)]:::stateNode
    Human{Human Approval\n(HITL)}:::humanNode
    Writer[Writer Agent\n(Write article based on outline)]:::aiNode
    End((END))

    %% Workflow connections
    Start --> Planner : Receive user topic
    Planner --> Checkpoint : Outline generated
    
    Checkpoint --> Human : interrupt_before='Writer'
    
    Human --> Writer : Human clicks "Approve"\n(Pass thread_id to continue)
    Human --> Planner : Human clicks "Reject"\n(Modify state, regenerate)
    
    Writer --> End : Article writing completed

Pay attention! The core of this diagram is not those Agents, but the dashed-box human node and the Checkpointer. Without the Checkpointer, the graph suffers from amnesia; even if you approve, it won't know what the previous outline was.


💻 Practical Code Drill

Enough talk, show me the code. Today, we will use Python along with LangGraph's latest API to implement this feature. To help everyone see the essence, I will use Mock LLM calls, focusing entirely on the graph's control flow.

Prerequisites: Please ensure you have installed langgraph. pip install langgraph

1. Define State and Nodes

First, we define the Agency's state, as well as the Planner and Writer nodes.

import time
from typing import TypedDict, Optional
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver

# ==========================================
# 1. Define State - Our "game save" data structure
# ==========================================
class AgencyState(TypedDict):
    topic: str              # User's initial topic
    outline: Optional[str]  # Outline generated by Planner
    article: Optional[str]  # Article generated by Writer
    approval_status: str    # Human approval status: 'pending', 'approved', 'rejected'

# ==========================================
# 2. Define Nodes - Our workers
# ==========================================
def planner_node(state: AgencyState) -> dict:
    """
    [EN] Planner Node: Responsible for generating an outline based on the topic.
    """
    print("\n[Planner 👨‍💼] Frantically brainstorming the outline...")
    time.sleep(1) # Simulate thinking time
    
    topic = state["topic"]
    # Simulate outline generated by LLM
    mock_outline = f"""
    Topic: "{topic}"
    1. What is {topic}?
    2. Core principles of {topic}
    3. Future prospects of {topic}
    """
    print("[Planner 👨‍💼] Outline generated!")
    
    # Return updated state
    return {
        "outline": mock_outline,
        "approval_status": "pending" # Set to pending approval
    }

def writer_node(state: AgencyState) -> dict:
    """
    [EN] Writer Node: Responsible for writing the article based on the outline.
    """
    print("\n[Writer ✍️] Received approval from the Editor-in-Chief, starting to write based on the outline...")
    time.sleep(1)
    
    outline = state["outline"]
    # Simulate LLM writing an article based on the outline
    mock_article = f"This is the detailed article for the outline [{outline.strip()}]...\n(10,000 words omitted here)"
    print("[Writer ✍️] Article writing completed! Time to clock out!")
    
    return {"article": mock_article}

2. Build the Computation Graph with Breakpoints

This is the highlight of this issue. Watch closely how we combine MemorySaver and interrupt_before.

# ==========================================
# 3. Graph Construction
# ==========================================
builder = StateGraph(AgencyState)

# Add nodes
builder.add_node("planner", planner_node)
builder.add_node("writer", writer_node)

# Define edges (workflow direction)
builder.add_edge(START, "planner")
builder.add_edge("planner", "writer") # Note: Although this connects directly to writer, we will interrupt it during compilation
builder.add_edge("writer", END)

# Core Step 1: Instantiate a Checkpointer (In-memory version, PostgresSaver can be used in production)
memory = MemorySaver()

# Core Step 2: Compile the graph and set breakpoints!
# interrupt_before=["writer"] means: Pause the graph's execution [BEFORE] executing the writer node.
graph = builder.compile(
    checkpointer=memory, 
    interrupt_before=["writer"]
)

print("✅ Agency workflow compiled successfully, HITL mode enabled.")

3. Simulate Actual Execution: From Pause to Resume

Now, let's simulate a real business scenario: User submits a request -> Graph pauses -> Terminal prompts user for confirmation -> User inputs approval -> Graph continues execution.

# ==========================================
# 4. Simulation Loop
# ==========================================
def run_agency():
    # thread_id must be configured; it is the unique credential the graph uses to identify the "save slot"
    # [EN] thread_id is crucial. It's the unique identifier for the "save slot".
    thread_config = {"configurable": {"thread_id": "agency_task_001"}}
    
    initial_state = {
        "topic": "LangGraph Multi-Agent Architecture",
        "outline": None,
        "article": None,
        "approval_status": "none"
    }

    print("\n🚀 [System] Starting Agency, new task received...")
    
    # Phase 1 execution: It will automatically stop before the writer node
    for event in graph.stream(initial_state, config=thread_config):
        for node_name, node_state in event.items():
            pass # Nodes have already printed info internally, no extra processing needed here
            
    # At this point, the graph is paused. We can check the current state of the graph
    current_state = graph.get_state(thread_config)
    
    # The 'next' attribute tells you what the next node to be executed is if execution resumes
    # If 'next' has a value, it means the graph was interrupted
    if current_state.next:
        print(f"\n⏸️ [System] Workflow paused. The next node to execute is: {current_state.next}")
        print(f"📄 [System] The currently generated outline is as follows:\n{current_state.values.get('outline')}")
        
        # Simulate human approval process
        user_input = input("👨‍⚖️ [Human Editor] Outline is as above. Do you approve the Writer to start writing? (Enter 'y' to approve, any other key to exit): ")
        
        if user_input.strip().lower() == 'y':
            print("\n👨‍⚖️ [Human Editor] Approval granted! Proceed!")
            
            # Core Step 3: Resume execution!
            # Passing None as the input state means "no need to modify the current state, just continue down the line"
            # The same thread_config must be passed to find the previous save slot
            for event in graph.stream(None, config=thread_config):
                pass
        else:
            print("\n👨‍⚖️ [Human Editor] Approval rejected! Task terminated.")
    else:
        print("\n✅ [System] All tasks have been fully executed.")

# Run our test
if __name__ == "__main__":
    run_agency()

Execution Output Demonstration

When you run this code, your terminal will have the following interaction:

✅ Agency workflow compiled successfully, HITL mode enabled.

🚀 [System] Starting Agency, new task received...

[Planner 👨‍💼] Frantically brainstorming the outline...
[Planner 👨‍💼] Outline generated!

⏸️ [System] Workflow paused. The next node to execute is: ('writer',)
📄 [System] The currently generated outline is as follows:

    Topic: "LangGraph Multi-Agent Architecture"
    1. What is LangGraph Multi-Agent Architecture?
    2. Core principles of LangGraph Multi-Agent Architecture
    3. Future prospects of LangGraph Multi-Agent Architecture
    
👨‍⚖️ [Human Editor] Outline is as above. Do you approve the Writer to start writing? (Enter 'y' to approve, any other key to exit): y

👨‍⚖️ [Human Editor] Approval granted! Proceed!

[Writer ✍️] Received approval from the Editor-in-Chief, starting to write based on the outline...
[Writer ✍️] Article writing completed! Time to clock out!

See that? The graph hit the brakes hard right before executing writer! The entire context (outline content) was perfectly saved in thread_id: agency_task_001. When you input y and call stream(None, config) again, it wakes up directly from the breakpoint and finishes the remaining work.


Pitfalls and How to Avoid Them

When introducing HITL, many beginners (and even some veterans) fall into deep pitfalls. As your mentor, I must sound the alarm here:

💣 Pitfall 1: Forgetting to add a Checkpointer, causing the graph to suffer from "amnesia"

Symptom: You set interrupt_before, but once the graph finishes the first phase and you want to continue, it throws an error saying the state cannot be found, or it starts running from the beginning. Diagnosis: You did not pass a checkpointer into compile(). Prevention: HITL must rely on state persistence. Without MemorySaver (or Postgres/Redis Saver), LangGraph has no idea where the graph paused. Remember the formula: HITL = Checkpointer + interrupt_before/after.

💣 Pitfall 2: Thread ID confusion leading to "crossed wires"

Symptom: User A clicks approve, but User B's article gets published. Diagnosis: The wrong thread_id was passed when resuming the graph. Prevention: In actual Web backend development (like using FastAPI), you must strongly bind the thread_id to the Task ID or User ID in your database. When the graph suspends, the frontend polls or waits; after the user clicks the button, the frontend sends the Task ID back to the backend, and the backend assembles it into thread_config to wake up the graph. Never hardcode the thread_id in a production environment!

💣 Pitfall 3: Passing a new state overwrites the old state, causing logic collapse

Symptom: When resuming graph execution, the initial state is passed in, causing the already generated outline to disappear. Diagnosis: During the second phase invocation, it was written as graph.stream(initial_state, config). Prevention: If you just want the graph to simply continue executing, do not pass any new state, pass None! That is, graph.stream(None, config). When LangGraph sees None, it knows: "Oh, the boss has no new instructions, I'll just keep working."

(Advanced spoiler: If you want to modify the outline during the pause, you can achieve this via graph.update_state(). We will analyze this advanced operation in detail in the next issue, "State Time Travel"!)


📝 Summary of this Issue

Today, we put reins on the runaway AI. We learned:

  1. The essence of HITL: State snapshots and suspension based on Checkpointer.
  2. Breakpoint setting: Building a human approval wall via interrupt_before=["writer"].
  3. Graph awakening: Letting the workflow continue via the same thread_id and stream(None).

In our AI Content Agency, introducing the "Editor-in-Chief Approval" node is a qualitative leap. It means we have officially moved from "blind box" style AI generation to an industrial-grade workflow of "Human-AI Collaboration".

Post-class Reflection Question: If the human editor reviews the outline, feels unsatisfied, and doesn't want to exit directly, but instead wants to send it back to the Planner to rewrite, how should our graph and code be modified?

Unleash your geek spirit and give it a try! See you in Issue 14, where I will guide you to unlock one of the coolest features in LangGraph: Graph State Modification and Time Travel. Class dismissed!