LangGraph / Intermediate Track Module 6 / 10
LangGraph Intermediate ⏱ 28 min
DEV

Human-in-the-Loop: Intermediate

Interrupt, approve, edit

How to Use This Lesson

  • Start with the user problem, then map the pattern to architecture and failure modes.
  • If a code or design example is included, change one assumption and reason through the impact.
  • Use role callouts, checklists, and Q&A sections as implementation or interview prep notes.

This lesson focuses on Human-in-the-Loop at the intermediate level. Use it to move from definition to implementation-ready explanation.

Concept

The interrupt() function (v0.4+) enables dynamic HITL from inside any node - pause based on state conditions, pass a structured payload to the waiting client, receive structured feedback on resume. More flexible than compile-time interrupt_before/after. Combined with a task queue and async API, you can build batch approval workflows where agents queue work and humans review throughout the day.

Key Facts

  • interrupt(payload) suspends and returns the payload to the caller
  • Resume: graph.invoke(Command(resume=human_input), config)
  • Multiple interrupts: a graph can have many interrupt points across different nodes
  • Async HITL: agents queue work in database, humans review in batches and resume
  • Interrupt payload: any JSON-serializable dict - form data, documents, risk scores

Reference Implementation

from langgraph.types import Command, interrupt
from typing import TypedDict

class ContractState(TypedDict):
    contract_text: str
    risk_score: float
    human_decision: str
    amendments: list

def analyze_contract(state: ContractState):
    # Simulate risk analysis - replace with LLM call
    return {"risk_score": 0.85}

def conditional_review(state: ContractState):
    if state["risk_score"] > 0.7:
        # Dynamic interrupt: only pauses for high-risk contracts
        human_input = interrupt({
            "contract_preview": state.get("contract_text", "")[:200],
            "risk_score": state["risk_score"],
            "recommendation": "HIGH RISK - Legal review required",
            "options": ["approve", "reject", "amend"]
        })
        return {
            "human_decision": human_input.get("decision", "reject"),
            "amendments": human_input.get("amendments", [])
        }
    return {"human_decision": "auto_approved"}

# Resume:
# app.invoke(Command(resume={"decision": "approve"}), config)

Interview Q&A

Q1. How do you implement conditional HITL that only pauses for high-risk operations?

Use the interrupt() function inside the node, gated by a condition: if state[‘risk_score’] > threshold: human_input = interrupt(payload). For low-risk cases, return normally without interrupting. This is more efficient than compile-time interrupt_before which always pauses regardless of state values.

Q2. How do you build an async HITL workflow with a human review queue?

When interrupt() fires, the graph suspends and persists state. Store thread_id and interrupt payload in a review queue (database table). Human reviewers pick from the queue, review via UI, submit feedback via an API that authorizes the user and calls invoke(Command(resume=feedback), config). Agents create tasks; humans process throughout the day asynchronously.

Q3. What is the security model for HITL - who can resume a paused graph?

LangGraph has no built-in authorization for resume operations - you implement access control in your application layer. Store which user or role can resume each thread_id, validate on the resume API endpoint before calling invoke(). For multi-tenant systems, namespace thread_ids by tenant and enforce isolation in your resume handler.

Q4. What are the rules for placing interrupt() calls?

Do not wrap interrupt() in try/except, do not reorder multiple interrupt calls in the same node, and keep payloads JSON-serializable. Side effects before an interrupt must be idempotent because the node can re-enter around the pause/resume boundary.

Q5. When would you use NodeInterrupt directly?

Prefer interrupt() for new code. NodeInterrupt exists as the lower-level exception class raised by a node to interrupt execution; most applications should not raise it manually because interrupt() handles payload shape and resume behavior consistently.

Practice Task

Explain when this LangGraph pattern is safer than a linear chain, then name one production failure it prevents.