Generated banner for Langchain Middleware
Generated logo for Langchain Middleware

Langchain Middleware

LangChainOfficialPublished by LangChain

  • Skill
  • Tool integration
  • Software development
  • MIT
  • No issues found

From the author

INVOKE THIS SKILL when you need human-in-the-loop approval, custom middleware, or structured output. Covers HumanInTheLoopMiddleware for human approval of dangerous tool calls, creating custom middleware with hooks, Command resume patterns, and structured output with Pydantic/Zod.
— the description by LangChain

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides middleware patterns for LangChain agents, focusing on Human-in-the-Loop (HITL) workflows and custom execution hooks. It enables developers to intercept tool calls for approval, logging, or error handling, and manage state persistence using checkpointers. The skill covers configuring interrupt policies for specific tools, resuming agent execution via Command patterns, and implementing custom logic through decorator hooks like wrap_tool_call, before_model, and after_agent.

What it does

  • Configures HumanInTheLoopMiddleware to pause agent execution before specific tool calls.
  • Defines per-tool approval policies including approve, edit, and reject decisions.
  • Implements custom middleware hooks to intercept model and tool calls.
  • Supports Command resume patterns to continue agent execution after human intervention.
  • Provides mechanisms for short-circuiting tool execution or adding retry logic.
  • Requires checkpointer integration to maintain state across interrupts.

When to use it

  • When you need human oversight for sensitive or dangerous tool actions.
  • When you need to modify tool arguments dynamically before execution.
  • When you need to implement custom logging or error handling around model and tool calls.
  • When you need to short-circuit tool execution based on specific conditions.
  • When you need to maintain state persistence for complex agent interactions.

Advantages

  • Granular control over tool execution policies.
  • Flexible hook system for intercepting agent lifecycle events.
  • Structured approach to handling human-in-the-loop interactions.
  • Consistent patterns for resuming interrupted agent workflows.

Usage examples

“Set up an agent that pauses before calling the send_email tool for human approval.”
The agent is configured with HumanInTheLoopMiddleware and a checkpointer, ensuring it interrupts execution when send_email is invoked.
“How do I resume an agent after a human approves a tool call?”
Use the Command class with the resume parameter to provide the approval decision and continue the agent's execution flow.
“Create a middleware that retries a tool call three times if it fails.”
A wrap_tool_call hook is defined to wrap the tool handler in a retry loop, catching exceptions and retrying up to three times.
“How can I edit tool arguments during an interrupt?”
The human provides an edit decision in the Command resume object, including the updated name and arguments for the tool call.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/langchain-ai/langchain-middleware/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip langchain-middleware.zip -d .agents/skills

Documentation by LangChain

<overview> 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. </overview>


Human-in-the-Loop

<ex-basic-hitl-setup> <python> 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"]},
            }
        )
    ],
)

</python> <typescript> 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"] } },
    }),
  ],
});

</typescript> </ex-basic-hitl-setup>

<ex-running-with-interrupts> <python> 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 [email protected]"}]
}, 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
)

</python> <typescript> 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 [email protected]" }]
}, 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
);

</typescript> </ex-running-with-interrupts>

<ex-editing-tool-arguments> <python> 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": "[email protected]",  # Fixed email
                    "subject": "Project Meeting - Updated",
                    "body": "...",
                },
            },
        }]
    }),
    config=config
)

</python> <typescript> 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: "[email protected]",  // Fixed email
            subject: "Project Meeting - Updated",
            body: "...",
          },
        },
      }]
    }
  }),
  config
);

</typescript> </ex-editing-tool-arguments>

<ex-rejecting-with-feedback> <python> 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
)

</python> </ex-rejecting-with-feedback>

<ex-multiple-tools-different-policies> <python> 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
            }
        )
    ],
)

</python> </ex-multiple-tools-different-policies>

<boundaries> ### 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)

    </boundaries>


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.

<ex-wrap-tool-call> <python> `@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)

</python> <typescript> `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; }
    }
  },
});

</typescript> </ex-wrap-tool-call>

<ex-before-after-hooks> <python> `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")

</python> <typescript> 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");
  },
});

</typescript> </ex-before-after-hooks>

<boundaries> ### What You CANNOT Configure

  • Interrupt after tool execution (must be before)

  • Skip checkpointer requirement for HITL

    </boundaries>

<fix-missing-checkpointer> <python> 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({...})]
)

</python> <typescript> 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 } })],
});

</typescript> </fix-missing-checkpointer>

<fix-no-thread-id> <python> 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"}})

</python> </fix-no-thread-id>

<fix-wrong-resume-syntax> <python> 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)

</python> <typescript> 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);

</typescript> </fix-wrong-resume-syntax>

Files

LICENSE

MIT License

Copyright (c) LangChain, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Python

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
LangChain
Publisher
LangChain (Official)
Source
langchain-ai/langchain-skills · config/skills/langchain-middleware · commit 126c384
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by LangChain or LangChain.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download langchain-middleware.zip