langgraph-middleware
v1.0.1
Published
Middleware utilities for LangGraph StateGraph — inject modules, before/after hooks, error catching, and callback observation
Maintainers
Readme
langgraph-middleware
English | 中文
langgraph-middleware brings middleware-style utilities to LangGraph's StateGraph — add before/after hooks, reusable inject modules, error catching, and observability callbacks to your graph nodes with a clean chainable API.
✨ Features
| Method | Description |
|--------|-------------|
| .middleware(name, before?, after?) | Wraps a node with before / after hooks, forming a subgraph Cannot be applied to the same node more than once before hook must not return a Command (use the main node or after hook for Command-based routing) |
| .inject(...modules) | Registers reusable structure modules executed before compile() |
| .observe(callbacks, ...nodes?) | Attaches LangChain BaseCallbackHandler instances for tracing/logging |
| .catch(handler, ...nodes?) | Wraps nodes with try/catch error recovery ⚠️ Do NOT use on nodes that call interrupt() — GraphInterrupt will be rejected |
| .withNodes(...names) | Type-level helper to inform TypeScript of dynamically-added node names |
| .compileAsync(...) | Async version of compile() — supports async inject modules |
- All methods support method chaining
- Compatible with checkpointing, interrupt, streaming, and retryPolicy
📦 Installation
npm install langgraph-middleware @langchain/langgraph @langchain/coreUse Cases
This library is ideal for:
- Quickly adding logging, validation, or state transformation before/after specific nodes
- Reusing the same graph structure across multiple projects (
.inject()) - Attaching tracing / callback handlers to specific nodes (
.observe()) - Rapidly adding/removing nodes and edges without verbose
addNode/addEdgechains (.inject(),.middleware()) - Wrapping a node in a try/catch block for graceful error recovery (
.catch()) - You accept that before/after hooks are implemented as a small internal subgraph (
.middleware())
Not Recommended
- Heavy reliance on
.catch()for error handling — prefer explicit error-handling nodes for complex logic
🚀 Quick Start
Middleware
.middleware() internally turns your node into a small subgraph (before_{name} → {name} → after_{name}).
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import "langgraph-middleware";
const GraphState = Annotation.Root({
input: Annotation<string>(),
logs: Annotation<string[]>({
reducer: (curr, next) => [...(curr ?? []), ...(next ?? [])],
default: () => [],
}),
});
const graph = new StateGraph(GraphState)
.addNode("process", async (state) => {
return { result: `done: ${state.input}` };
})
.addEdge(START, "process")
.addEdge("process", END)
.middleware(
"process",
// before hook
(state) => {
console.log("before:", state.input);
return { logs: ["starting"] };
},
// after hook
(state) => {
console.log("after:", state.result);
return { logs: ["finished"] };
}
)
.compile();
const result = await graph.invoke({ input: "hello" });
console.log(result);Inject Modules
Create reusable modules that modify any graph before compilation:
import { StateGraph, Annotation, START, END, type StateGraphWithInject } from "@langchain/langgraph";
import "langgraph-middleware";
const MyAnnotation = Annotation.Root({ value: Annotation<string>() });
// The second generic parameter tells TypeScript which node names the module adds
const addLoggingModule = (
graph: StateGraphWithInject<StateGraph<typeof MyAnnotation>, "logger" | "prepare_logger">
) => {
return graph
.addNode("prepare_logger", (state) => state)
.addNode("logger", async (state) => {
console.log("State:", state);
return {};
})
.addEdge(START, "prepare_logger")
.addEdge("prepare_logger", "logger");
};
const graph = new StateGraph(MyAnnotation)
.addNode("main", mainNode)
.addEdge("logger", "main")
.addEdge("main", END)
.inject(addLoggingModule)
.inject(state => state) // Inline usage
.inject(moduleA, moduleB, moduleC) // Multiple modules at once
.compile(); // Use compileAsync() if you injected async modulesImportant timing note:
.middleware(),.catch(), and.observe()scan nodes at builder-chain time. Nodes added via.inject()are registered at compile time (later). To apply middleware/catch/observe to inject-added nodes, call those methods inside the inject module itself. Seeexamples/inject.tsfor patterns.
Error Handling
const graph = new StateGraph(MyAnnotation)
.addNode("risky", riskyNode)
.addEdge(START, "risky")
.addEdge("risky", END)
.catch(
(error, state, meta) => {
console.error(`Node failed:`, error);
return { fallback: true, error: String(error) };
},
"risky", // Omit to wrap all existing nodes, or list specific ones: "node1", "node2", ...
)
.compile();Observability
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
import "langgraph-middleware";
class MyTimer extends BaseCallbackHandler {
name = "MyTimer";
async handleChainStart(_chain, _inputs, runId, _parentRunId, _tags, _metadata, _runType, name) {
console.log(`⏱️ [${name}] started`);
}
async handleChainEnd(_outputs, runId) {
console.log(`✅ [${runId.slice(0, 8)}] completed`);
}
}
const graph = new StateGraph(MyAnnotation)
.addNode("step1", step1)
.addNode("step2", step2)
.addEdge(START, "step1")
.addEdge("step1", "step2")
.addEdge("step2", END)
.observe([new MyTimer()]) // Omitting node names applies to all nodes
.compile();API Reference
.middleware(name, before?, after?, options?)
Attaches before/after hooks around a node. Internally creates a subgraph.
Restrictions:
- Called at most once per node name (repeated calls throw)
beforefunction must not return aCommand(throws at runtime)
.middleware(
"myNode",
(state) => { /* before */ return { logs: ["start"] }; },
(state) => { /* after */ return { logs: ["end"] }; }
)The after hook supports { func, options } form for retryPolicy or other node options. The before hook also supports this form. An optional 4th argument forwards compile options to the internal subgraph.
.inject(...modules)
Registers reusable modules executed in order just before compile(). Modules receive the StateGraph instance and should return it for chaining.
.inject((graph) => {
return graph
.addNode("logger", (s) => { console.log(s); return {}; })
.addEdge(START, "logger");
})Supports both sync and async modules. Use .compileAsync() when async modules are present — using .compile() with async modules throws.
.catch(handler, ...names?)
Wraps targeted nodes in try/catch. The handler receives (error, state, meta) where meta is the RunnableConfig (includes metadata, executionInfo, etc.).
- Handler returns a value → the node is treated as successful (short-circuits any remaining retries)
- Handler throws → the error propagates to LangGraph's normal retryPolicy logic
⚠️ Do NOT use on nodes that call interrupt(). If a GraphInterrupt is caught, it is immediately re-thrown as a regular Error to prevent checkpoint corruption.
If no node names are provided, all currently registered nodes are wrapped.
.observe(callbacks, ...names?)
Attaches LangChain BaseCallbackHandler instances to nodes. The runName in callback events is set to the node name for easy identification in traces.
If no node names are provided, all currently registered nodes are observed.
.withNodes(...names)
A type-level-only helper. Does nothing at runtime — it tells TypeScript that certain node names exist on the builder, enabling type inference after conditional addNode calls or .inject() usage.
const builder = new StateGraph(GraphState)
.addNode("node1", fn);
if (someCondition) {
builder.addNode("node2", fn).addNode("node3", fn);
}
builder
.withNodes("node2", "node3") // TypeScript now knows about these
.addEdge("__start__", "node1")
.addEdge("node1", "node2")
.addEdge("node2", "node3");.compileAsync(...args)
Async version of compile(). Awaits all registered inject modules (including async ones) before compilation.
const graph = await builder.compileAsync(checkpointerOptions);Examples
See the examples/ directory for runnable demos:
| File | Description |
|------|-------------|
| middleware.ts | Before/after hooks with traceable wrappers and retryPolicy |
| middleware-with-checkpointer.ts | Middleware + checkpoint persistence, xray inspection |
| inject.ts | Async inject modules, conditional edges, error handler injection |
| catch.ts | Error recovery with catch(), multiple target nodes |
| catch-with-retry.ts | catch() + retryPolicy: conditional re-throw pattern |
| catch-interrupt-limitation.ts | Documents the catch + interrupt() incompatibility |
| observe.ts | Callback-based timing/monitoring |
| withNodes.ts | Type-level helper for dynamically-added nodes |
Run any example with:
npx tsx examples/<name>.tsRun Tests
All features are verified with comprehensive tests:
npm testLicense
MIT © jakehkw
Links
- GitHub: https://github.com/jakehkw/langgraph-middleware
- npm: https://www.npmjs.com/package/langgraph-middleware
For Maintainers & AI Assistants
This repository includes a rich CONTEXT.md file containing:
- Complete domain model and architecture decisions
- Detailed explanation of timing (builder-time vs compile-time)
- All important limitations and gotchas with rationale
- Expanded keywords and recommendation triggers
- When (and when not) to recommend this package
When working on this codebase or answering questions about LangGraph middleware patterns, read CONTEXT.md first. It is designed to serve as high-fidelity persistent context for future RAG / AI-assisted work.
