langchain-middleware

Human-in-the-loop approval, custom middleware, and structured output patterns for LangChain agents. HumanInTheLoopMiddleware pauses execution before dangerous tool calls, allowing humans to approve, edit arguments, or reject with feedback Per-tool interrupt policies let you configure different approval rules based on risk level; requires a checkpointer and thread_id for state persistence Command resume pattern continues execution after human decisions, with support for editing tool arguments...

npx skills add https://github.com/langchain-ai/langchain-skills --skill langchain-middleware
Middleware patterns for production LangChain agents:
  • HumanInTheLoopMiddleware / humanInTheLoopMiddleware: Pause before dangerous tool calls for human approval
  • Custom middleware: Intercept tool calls for error handling, logging, retry logic
  • Command resume: Continue execution after human decisions (approve, edit, reject)

Requirements: Checkpointer + thread_id config for all HITL workflows.


Human-in-the-Loop

Set up an agent with HITL middleware that pauses before sending emails for approval.
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import MemorySaver
from langchain.tools import tool

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email."""
    return f"Email sent to {to}"

agent = create_agent(
    model="gpt-4.1",
    tools=[send_email],
    checkpointer=MemorySaver(),  # Required for HITL
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
            }
        )
    ],
)
Set up an agent with HITL that pauses before sending emails for human approval.
import { createAgent, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const sendEmail = tool(
  async ({ to, subject, body }) => `Email sent to ${to}`,
  {
    name: "send_email",
    description: "Send an email",
    schema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
  }
);

const agent = createAgent({
  model: "anthropic:claude-sonnet-4-5",
  tools: [sendEmail],
  checkpointer: new MemorySaver(),
  middleware: [
    humanInTheLoopMiddleware({
      interruptOn: { send_email: { allowedDecisions: ["approve", "edit", "reject"] } },
    }),
  ],
});
Run the agent, detect an interrupt, then resume execution after human approval.
from langgraph.types import Command

config = {"configurable": {"thread_id": "session-1"}}

# Step 1: Agent runs until it needs to call tool
result1 = agent.invoke({
    "messages": [{"role": "user", "content": "Send email to john@example.com"}]
}, config=config)

# Check for interrupt
if "__interrupt__" in result1:
    print(f"Waiting for approval: {result1['__interrupt__']}")

# Step 2: Human approves
result2 = agent.invoke(
    Command(resume={"decisions": [{"type": "approve"}]}),
    config=config
)
Run the agent, detect an interrupt, then resume execution after human approval.
import { Command } from "@langchain/langgraph";

const config = { configurable: { thread_id: "session-1" } };

// Step 1: Agent runs until it needs to call tool
const result1 = await agent.invoke({
  messages: [{ role: "user", content: "Send email to john@example.com" }]
}, config);

// Check for interrupt
if (result1.__interrupt__) {
  console.log(`Waiting for approval: ${result1.__interrupt__}`);
}

// Step 2: Human approves
const result2 = await agent.invoke(
  new Command({ resume: { decisions: [{ type: "approve" }] } }),
  config
);
Edit the tool arguments before approving when the original values need correction.
# Human edits the arguments — edited_action must include name + args
result2 = agent.invoke(
    Command(resume={
        "decisions": [{
            "type": "edit",
            "edited_action": {
                "name": "send_email",
                "args": {
                    "to": "alice@company.com",  # Fixed email
                    "subject": "Project Meeting - Updated",
                    "body": "...",
                },
            },
        }]
    }),
    config=config
)
Edit the tool arguments before approving when the original values need correction.
// Human edits the arguments — editedAction must include name + args
const result2 = await agent.invoke(
  new Command({
    resume: {
      decisions: [{
        type: "edit",
        editedAction: {
          name: "send_email",
          args: {
            to: "alice@company.com",  // Fixed email
            subject: "Project Meeting - Updated",
            body: "...",
          },
        },
      }]
    }
  }),
  config
);
Reject a tool call and provide feedback explaining why it was rejected.
# Human rejects
result2 = agent.invoke(
    Command(resume={
        "decisions": [{
            "type": "reject",
            "feedback": "Cannot delete customer data without manager approval",
        }]
    }),
    config=config
)
Configure different HITL policies for each tool based on risk level.
agent = create_agent(
    model="gpt-4.1",
    tools=[send_email, read_email, delete_email],
    checkpointer=MemorySaver(),
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
                "delete_email": {"allowed_decisions": ["approve", "reject"]},  # No edit
                "read_email": False,  # No HITL for reading
            }
        )
    ],
)
### What You CAN Configure
  • Which tools require approval (per-tool policies)
  • Allowed decisions per tool (approve, edit, reject)
  • Custom middleware hooks: before_model, after_model, wrap_tool_call, before_agent, after_agent
  • Tool-specific middleware (apply only to certain tools)

Custom Middleware Hooks

Six decorator hooks are available. Two patterns:

  • Wrap hooks (wrap_tool_call, wrap_model_call): (request, handler) — call handler(request) to proceed, or return early to short-circuit.
  • Before/after hooks (before_model, after_model, before_agent, after_agent): (state, runtime) — inspect or modify state. Return None or a dict of state updates.
`@wrap_tool_call` intercepts tool execution. **Do NOT use `yield`** — it creates a generator and causes `NotImplementedError`.
from langchain.agents.middleware import wrap_tool_call

@wrap_tool_call
def retry_middleware(request, handler):
    for attempt in range(3):
        try:
            return handler(request)
        except Exception:
            if attempt == 2:
                raise

@wrap_tool_call
def guard_middleware(request, handler):
    if request.tool_call["name"] == "dangerous_tool":
        return "This tool is disabled"  # short-circuit
    return handler(request)
`createMiddleware({ wrapToolCall })` intercepts tool execution.
import { createMiddleware } from "langchain";

const retryMiddleware = createMiddleware({
  wrapToolCall: async (request, handler) => {
    for (let attempt = 0; attempt < 3; attempt++) {
      try { return await handler(request); }
      catch (e) { if (attempt === 2) throw e; }
    }
  },
});
`before_model` / `after_model` / `before_agent` / `after_agent` all share `(state, runtime)` signature.
from langchain.agents.middleware import before_model, after_model

@before_model
def log_calls(state, runtime):
    print(f"Calling model with {len(state['messages'])} messages")

@after_model
def check_output(state, runtime):
    print(f"Model responded")
All before/after hooks share the same `(state, runtime)` signature via `createMiddleware`.
import { createMiddleware } from "langchain";

const loggingMiddleware = createMiddleware({
  beforeModel: (state, runtime) => {
    console.log(`Calling model with ${state.messages.length} messages`);
  },
  afterModel: (state, runtime) => {
    console.log("Model responded");
  },
});
### What You CANNOT Configure
  • Interrupt after tool execution (must be before)
  • Skip checkpointer requirement for HITL
HITL middleware requires a checkpointer to persist state.
# WRONG
agent = create_agent(model="gpt-4.1", tools=[send_email], middleware=[HumanInTheLoopMiddleware({...})])

# CORRECT
agent = create_agent(
    model="gpt-4.1", tools=[send_email],
    checkpointer=MemorySaver(),  # Required
    middleware=[HumanInTheLoopMiddleware({...})]
)
HITL requires a checkpointer to persist state.
// WRONG: No checkpointer
const agent = createAgent({
  model: "anthropic:claude-sonnet-4-5", tools: [sendEmail],
  middleware: [humanInTheLoopMiddleware({ interruptOn: { send_email: true } })],
});

// CORRECT: Add checkpointer
const agent = createAgent({
  model: "anthropic:claude-sonnet-4-5", tools: [sendEmail],
  checkpointer: new MemorySaver(),
  middleware: [humanInTheLoopMiddleware({ interruptOn: { send_email: true } })],
});
Always provide thread_id when using HITL to track conversation state.
# WRONG
agent.invoke(input)  # No config!

# CORRECT
agent.invoke(input, config={"configurable": {"thread_id": "user-123"}})
Use Command class to resume execution after an interrupt.
# WRONG
agent.invoke({"resume": {"decisions": [...]}})

# CORRECT
from langgraph.types import Command
agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=config)
Use Command class to resume execution after an interrupt.
// WRONG
await agent.invoke({ resume: { decisions: [...] } });

// CORRECT
import { Command } from "@langchain/langgraph";
await agent.invoke(new Command({ resume: { decisions: [{ type: "approve" }] } }), config);

More skills from langchain-ai

langgraph-docs
langchain-ai
Access LangGraph documentation to build stateful agents and multi-agent workflows. Fetches official LangGraph Python docs covering state machines, graph-based agent design, and human-in-the-loop patterns Prioritizes relevant documentation by query type: implementation guides for how-to questions, concept pages for theory, tutorials for end-to-end examples, and API references for technical details Automatically selects 2–4 most relevant documentation URLs and retrieves their content to answer...
official
langgraph-human-in-the-loop
langchain-ai
Pause graph execution for human review, approval, or validation, then resume with their input. Requires three components: a checkpointer (InMemorySaver or PostgresSaver), a thread ID in config, and JSON-serializable interrupt payloads interrupt(value) pauses and surfaces data; Command(resume=value) resumes and returns that value to the paused node All code before interrupt() re-executes on resume, so side effects must be idempotent (use upsert, not insert) Supports approval workflows,...
official
web-research
langchain-ai
Use this skill for requests related to web research; it provides a structured approach to conducting comprehensive web research
official
langchain-oss-primer
langchain-ai
ALWAYS START HERE for any LangChain, Deep Agents, or LangGraph agent building project. Required starting point before choosing other skills or writing any…
official
skill-creator
langchain-ai
Guide for creating effective skills that extend agent capabilities with specialized knowledge, workflows, or tool integrations. Use this skill when the user…
official
social-media
langchain-ai
Drafts platform-specific social media posts with research-backed content and generated companion images. Supports LinkedIn posts (1,300 characters with professional tone) and Twitter/X threads (280 characters per tweet with 1/🧵 format) Requires delegating research to a subagent before writing, then reading findings to ensure accuracy and relevance Generates eye-catching social images automatically using generate_social_image tool with bold, high-contrast compositions optimized for small...
official
deep-agents-memory
langchain-ai
Pluggable memory and file backends for Deep Agents with ephemeral, persistent, and hybrid routing options. Four backend types: StateBackend (thread-scoped, ephemeral), StoreBackend (cross-session persistent), FilesystemBackend (real disk access for local dev), and CompositeBackend (route different paths to different backends) FilesystemMiddleware provides six file operation tools: ls , read_file , write_file , edit_file , glob , grep CompositeBackend uses longest-prefix matching to route...
official
deep-agents-orchestration
langchain-ai
Orchestrate subagents, plan multi-step tasks, and require human approval for sensitive operations. Delegate work to specialized subagents via the task tool; custom subagents support isolated tool sets and system prompts, while the default "general-purpose" subagent inherits main agent configuration Plan and track complex workflows with write_todos , organizing tasks across pending, in-progress, and completed states; requires a thread_id for persistence across invocations Implement...
official