Labsco
langchain-ai logo

langchain-middleware

846

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

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

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

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

Inspect the full instructions your agent will receiveExpand

This is the exact playbook injected into your agent when the skill activates — shown here so you can audit it before installing. You don't need to read it to use the skill.

by langchain-ai

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 Download ZIPGitHub846

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.

Copy & paste — that's it
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.

Copy & paste — that's it
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.

Copy & paste — that's it
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
)

Run the agent, detect an interrupt, then resume execution after human approval.

Copy & paste — that's it
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.

Copy & paste — that's it
# 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.

Copy & paste — that's it
// 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.

Copy & paste — that's it
# 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.

Copy & paste — that's it
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.

Copy & paste — that's it
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.

Copy & paste — that's it
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")

Copy & paste — that's it

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

Copy & paste — that's it

 
 
 
### 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({...})] )

Copy & paste — that's it

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

Copy & paste — that's it

 
 
 
 
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"}})

Copy & paste — that's it

 
 
 
 
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)

Copy & paste — that's it

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

Copy & paste — that's it