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 before approval or providing rejection feedback Custom middleware hooks (before_model, after_model, wrap_tool_call, before_agent, after_agent) enable error handling, logging, and retry logic across the agent lifecycle

INSTALLATION
npx skills add https://github.com/langchain-ai/langchain-skills --skill langchain-middleware
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

  • 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

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"]},

            }

        )

    ],

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

    }),

  ],

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

)
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

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

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

);
# Human rejects

result2 = agent.invoke(

    Command(resume={

        "decisions": [{

            "type": "reject",

            "feedback": "Cannot delete customer data without manager approval",

        }]

    }),

    config=config

)
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

            }

        )

    ],

)
  • 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.
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)
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; }

    }

  },

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

  },

});
  • Interrupt after tool execution (must be before)
  • Skip checkpointer requirement for HITL
# 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({...})]

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

});
# WRONG

agent.invoke(input)  # No config!

# CORRECT

agent.invoke(input, config={"configurable": {"thread_id": "user-123"}})
# WRONG

agent.invoke({"resume": {"decisions": [...]}})

# CORRECT

from langgraph.types import Command

agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=config)
// WRONG

await agent.invoke({ resume: { decisions: [...] } });

// CORRECT

import { Command } from "@langchain/langgraph";

await agent.invoke(new Command({ resume: { decisions: [{ type: "approve" }] } }), config);
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card