| name | langchain-middleware |
| description | 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. |
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.
```python
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.
```typescript
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.
```python
from langgraph.types import Command
Edit the tool arguments before approving when the original values need correction.
```python
# 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
)
```
Edit the tool arguments before approving when the original values need correction.
```typescript
// 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
);
```
Reject a tool call and provide feedback explaining why it was rejected.
```python
# 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.
```python
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
`@wrap_tool_call` intercepts tool execution. **Do NOT use `yield`** — it creates a generator and causes `NotImplementedError`.
`createMiddleware({ wrapToolCall })` intercepts tool execution.
`before_model` / `after_model` / `before_agent` / `after_agent` all share `(state, runtime)` signature.
All before/after hooks share the same `(state, runtime)` signature via `createMiddleware`.
### What You CANNOT Configure
HITL middleware requires a checkpointer to persist state.
```python
# WRONG
agent = create_agent(model="gpt-4.1", tools=[send_email], middleware=[HumanInTheLoopMiddleware({...})])
Always provide thread_id when using HITL to track conversation state.
```python
# WRONG
agent.invoke(input) # No config!
Use Command class to resume execution after an interrupt.
```typescript
// WRONG
await agent.invoke({ resume: { decisions: [...] } });