Skip to main content
A middleware wraps the agent’s LLM call. Return one from getMiddlewares(ctx) and it runs on every model turn.
1

Build the middleware with createMiddleware

Import createMiddleware from langchain. Set a name (appears in error messages) and any of the six hooks.
Canonical source: weather-middleware.ts.
2

Pick the right hook

createMiddleware accepts exactly six hooks. Pick the one whose timing matches your work — there is no onError, no beforeToolCall, no afterToolCall.
Error handling lives in wrapModelCall: wrap the handler(request) call in a try/catch and retry with a fallback model. The hook owns the call, so you decide what happens on failure.
3

Return undefined or jumpTo — never a state object

The flow-control hooks (beforeAgent, beforeModel, afterModel, afterAgent) must return undefined (pass through) or { jumpTo: 'end' as const } (short-circuit the turn). Never return { messages: ... } or any other state channel. (The wrap* hooks are different — they return the result of calling handler, as in the fallback example above.)
Returning a partial state object from a plugin middleware breaks LangGraph checkpointer thread continuity — the observed symptom is a brand-new thread spawned on every message. Message and state rewrites belong in the transport layer (the messages controller), not in a middleware hook.
4

Return it from getMiddlewares(ctx)

The runtime appends your middleware after the framework’s built-in middleware (see the list below).
See getMiddlewares in weather.plugin.ts.
5

Attach a middleware to a sub-agent (optional)

A sub-agent can carry its own middleware — it only runs inside that sub-agent’s loop.

What to know before shipping

  • Four always-on middleware run first and are not removable, in this order: capability-gate, tool-validation, tool-repetition-guard, tool-retry. Two more run only when the matching hook is set on createOracleApp: page-context (when hooks.getRoomTitle is provided) and safety-guardrail (when hooks.safetyModel is provided). Your plugin middleware is appended after all of these.
  • Plugin middleware runs in topological dependency order — encode ordering via dependsOn if it matters.
  • Middleware fires per LLM turn, not per individual tool invocation. Wrap the tool handler directly for per-tool behaviour.
  • Closure-scoped timers interleave across concurrent model calls. For accurate timing, push start times onto a per-call ID.
  • Don’t reimplement auth in a middleware — auth runs at the HTTP layer (AuthHeaderMiddleware), not in the agent loop.

Add a sub-agent

Sub-agent-scoped middleware.

State schema

What’s in state when your hooks fire.