Labsco
langchain-ai logo

langchain-middleware

108

by langchain-ai · part of langchain-ai/skills-benchmarks

INVOKE THIS SKILL when you need human-in-the-loop approval, custom middleware, or structured output. Covers HumanInTheLoopMiddleware for human approval of…

🔥🔥🔥🔥✓ VerifiedFreeQuick setup
🧩 One of 7 skills in the langchain-ai/skills-benchmarks package — works on its own, and pairs well with its siblings.

This is the playbook your agent receives when the skill activates — you don't need to read it to use the skill, but it's here to audit before installing.

by langchain-ai

INVOKE THIS SKILL when you need human-in-the-loop approval, custom middleware, or structured output. Covers HumanInTheLoopMiddleware for human approval of… npx skills add https://github.com/langchain-ai/skills-benchmarks --skill langchain-middleware Download ZIPGitHub108

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 [[email protected]](https://github.com/langchain-ai/skills-benchmarks/blob/main/skills/main/langchain-middleware/mailto: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 [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
);

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
)

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
);

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 
 
 
 
`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.
```python
# 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);