Custom Middleware
Build production middleware that intercepts model calls, gates tool execution, injects dynamic context, and writes state — using node-style hooks for sequential logic and wrap-style hooks when you need control over whether and how many times an operation runs.
Quick Reference
- →@before_model / @after_model — node-style hooks; return dict to update state or None to pass through
- →@wrap_model_call — wrap-style; you call handler(request) to execute the model
- →@wrap_tool_call — wrap-style around each tool execution
- →@dynamic_prompt — convenience hook that returns a system prompt string
- →request.override(model=, tools=, system_message=) — returns immutable modified copy
- →ExtendedModelResponse(model_response, command=Command(update={...})) — write state from wrap hooks
- →@before_model(can_jump_to=['end']) + return {'jump_to': 'end'} — exit agent loop early
When to Write Custom Middleware (and When Not To)
Before writing custom middleware, check whether a prebuilt covers your case — LangChain ships retry, fallback, PII, call limits, tool selection, summarization, and HITL out of the box. If prebuilt covers it, use it. Write custom middleware when you need cross-cutting behavior that doesn't fit a prebuilt, but would be wrong as a graph node because it must run on every model or tool call regardless of which node calls it.
| Situation | Use |
|---|---|
| Retry, fallback, PII redaction, call limits | Prebuilt middleware (ModelRetryMiddleware, PIIMiddleware, …) |
| Logic tied to a specific graph node | Graph node — not middleware |
| Cross-cutting: log/validate every model call | Node-style hook (@before_model / @after_model) |
| Control whether/how many times model runs | Wrap-style hook (@wrap_model_call) |
| Intercept every tool call | Wrap-style hook (@wrap_tool_call) |
| Multiple hooks + config at init time | Class-based AgentMiddleware subclass |
prebuilt first → wrap if you control execution → class if you need state/config → node-style otherwise