Middleware: code around the model
Middleware is code that runs at fixed points in the agent loop, such as before or after every model call, and can read the state, change it, or end the run.
Last updated: 27 Sep, 2026 · LangChain 1.4
The agent loop is: call the model, run the tools it asks for, repeat. Middleware attaches to points in that loop. Four hooks run at those points: before_agent and after_agent once per invoke, and before_model and after_model around every model call.
The before_model and after_model hooks
from langchain.agents.middleware import before_model, after_model
@before_model # runs before each model call
def hook(state, runtime): # the current state and the runtime
... # return nothing to leave the state as it is
agent = create_agent(model, tools=[...], middleware=[hook])The before and after hooks
Write two hooks. A decorated function becomes middleware; it receives the current state, with its messages, and the runtime.
from langchain.agents.middleware import after_model, before_model
@before_model
def count(state, runtime):
print("before the model:", len(state["messages"]), "messages")
@after_model
def report(state, runtime):
last = state["messages"][-1]
print("after the model: ", last.text or last.tool_calls[0]["name"])The agent with the hooks
Attach both hooks to the agent with middleware=.
from langchain.agents import create_agent
from hooks import count, report
from shop_model import ShopModel
from tools import lookup_order
agent = create_agent(ShopModel(), tools=[lookup_order],
middleware=[count, report])Both hooks around a lookup
Ask one question. There are two model calls, so each hook runs twice.
from langchain.agents import create_agent
from hooks import count, report
from shop_model import ShopModel
from tools import lookup_order
agent = create_agent(ShopModel(), tools=[lookup_order], middleware=[count, report])
agent.invoke({"messages": [{"role": "user", "content": "Where is A17?"}]})The hooks as steps in the loop
Streaming the same agent shows the hooks as steps of their own, named after the function and the hook.
for step in agent.stream({"messages": [{"role": "user", "content": "Where is A17?"}]}, stream_mode="updates"):
print("step:", list(step))What the hooks printed
- Four hooks:
before_agentandafter_agentrun once per invoke;before_modelandafter_modelrun around every model call. - Two model calls, so each hook ran twice: the first saw one message and the model asked for
lookup_order; the second saw three and answered. - A decorated function becomes middleware. It receives the state and the runtime; returning nothing leaves the state unchanged.
- Streaming shows the hooks as steps of their own, so they sit inside the loop that memory and human approval also build on.
before_model vs after_model
| before_model | after_model | |
|---|---|---|
| Runs | Before the model call | After the model call |
| Sees | The messages going in | The message that came back |
| Typical use | Trim or check the input | Log or inspect the reply |
| How often | Once per model call | Once per model call |
Where middleware hooks fit
- Logging or measuring every model call in one place.
- Checking or trimming the messages before they reach the model.
Related
- Previous: State beyond messages
- Next: The order middleware runs in
- Reference: Middleware
- Add an
@after_agenthook that prints the number of messages at the end. - Return
{"messages": []}fromcountand see whether the conversation changes. - Ask a question with no order in it and count how many times each hook runs.
This is what real progress feels like.