LangChainLangChain 1.4 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
46 small wins to finish your pathNext lesson →

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

python
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.

python
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=.

python
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.

Example
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.

Example
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_agent and after_agent run once per invoke; before_model and after_model run 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_modelafter_model
RunsBefore the model callAfter the model call
SeesThe messages going inThe message that came back
Typical useTrim or check the inputLog or inspect the reply
How oftenOnce per model callOnce 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.
Watch out. A hook that returns a state update changes the conversation for every later step. Return nothing when you mean to observe, and return an update only when you mean to change what the model sees.
Try it yourself
  • Add an @after_agent hook that prints the number of messages at the end.
  • Return {"messages": []} from count and 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.