All skills
langchain-ai avatar

/langchain-middleware

@d00eed0 official

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.

Use this Skill: https://skilld.dev/gh/langchain-ai/langchain-skills/langchain-middleware

This session only. Nothing lands on disk.

SKILL.md

≈76 tokens always: the name and description. ≈2.6k when used: this file.

<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 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
)
</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 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
);
</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": "alice@company.com",  # 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: "alice@company.com",  // 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>

Source: SKILL.md on GitHub

No alerts17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    This skill provides educational patterns and best practices for implementing middleware in LangChain agents, focusing on security-enhancing features like human-in-the-loop approvals. It contains no identified security issues and promotes safe agent execution practices.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    1 file scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at d00eed0. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Activeupdated 5 months ago
  • TypeScript
  • Python
  • langchain
  • middleware
  • human-in-the-loop
  • agents
  • tool-approval
  • checkpointing
  • langgraph

README badge

README badge for langchain-ai/langchain-skills/langchain-middleware

Adds human-in-the-loop approval middleware to LangChain agents, pausing before dangerous tool calls (e.g., sending emails, deleting data) and allowing humans to approve, edit arguments, or reject with feedback. Also covers custom middleware hooks for logging, retry logic, and error handling across the agent lifecycle.

Generated from the current SKILL.md.

Does this skill require a checkpointer?
Yes. Human-in-the-loop middleware requires a checkpointer (e.g. MemorySaver) to persist agent state across interrupts and resumptions.
Can I configure different approval policies for different tools?
Yes. HumanInTheLoopMiddleware accepts a per-tool interrupt_on dict where you can set allowed decisions (approve, edit, reject) for each tool, or disable HITL entirely for low-risk tools.
How do I resume an agent after a human approval or rejection?
Use the Command class with a resume dict containing a decisions array. Each decision specifies the type (approve, edit, or reject) and optionally edited_action or feedback.
What middleware hooks are available?
Six hooks: wrap_tool_call and wrap_model_call (which wrap the handler), plus before_model, after_model, before_agent, and after_agent (which inspect or modify state). Use wrap hooks to short-circuit execution; before/after hooks to log or modify flow.
Do I need to provide a thread_id when invoking the agent?
Yes. Always pass thread_id in the config's configurable dict when using HITL to track and resume conversation state correctly.

Generated from the current SKILL.md. These answers refresh after source changes.