State beyond messages
A custom state is the agent's saved dictionary with keys of your own, declared by subclassing AgentState and passed as state_schema.
Last updated: 27 Sep, 2026 · LangChain 1.4
The shop wants to count how many orders a customer looked up in one conversation. That count belongs to the thread, like the messages, so it goes in the state the checkpointer saves after each step.
Subclassing AgentState
from langchain.agents import AgentState
class DeskState(AgentState): # AgentState already has messages
lookups: int # your extra key
agent = create_agent(model, tools=[...], state_schema=DeskState)The DeskState schema
Subclass AgentState to keep its messages key and add one of your own.
from langchain.agents import AgentState
class DeskState(AgentState): # keeps messages, adds one key
lookups: intImports and the orders
A tool changes the state by returning a Command. Bring in the pieces it needs and the order data.
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.types import Command
ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}A tool that writes to state
runtime.state is the current state. The tool reads the count, adds one, and returns a Command whose update carries the new count and the tool message it would otherwise have returned.
@tool
def lookup_order(order_id: str, runtime: ToolRuntime) -> Command:
"""Look up an order's shipping status by its id, such as A17."""
count = runtime.state.get("lookups", 0) + 1 # read then add one
text = f"{order_id} {ORDERS.get(order_id, 'is not an order we have')}."
reply = ToolMessage(text, tool_call_id=runtime.tool_call_id) # the result
return Command(update={"lookups": count, "messages": [reply]}) # update stateCreating the agent
Give the agent the bigger shape with state_schema and a checkpointer so the count is saved between calls.
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from shop_model import ShopModel
agent = create_agent(ShopModel(), tools=[lookup_order], state_schema=DeskState,
checkpointer=InMemorySaver())
thread = {"configurable": {"thread_id": "ravi-1"}}Counting lookups across two turns
Two lookups in two calls on one thread. The count comes back with the conversation.
agent.invoke({"messages": [{"role": "user", "content": "Where is A17?"}]}, thread)
result = agent.invoke({"messages": [{"role": "user", "content": "And C40?"}]}, thread)
print(result["lookups"])
print(result["messages"][-1].text)Setting a key when you invoke
Keys other than messages can be passed in with the input. This thread starts the count at 10, and one lookup makes it 11.
result = agent.invoke({"messages": [{"role": "user", "content": "Where is A17?"}], "lookups": 10},
{"configurable": {"thread_id": "fresh"}})
print(result["lookups"])How the count carried over
- runtime.state is the current state; to change it the tool returns a
Commandwhoseupdatecarries the new count. - The tool message must be in the update, tagged with the call's id, because every tool call needs its result.
- The count is 2 after two lookups in two calls: it came back with the conversation because it lives in the same state, saved by the same checkpointer.
- Keys other than messages can be set on input. Context is fixed for one call and never saved; state is saved and can change as the conversation goes on.
Context vs state
| Context | State | |
|---|---|---|
| Set when | Once, at invoke | On input and by tools |
| Saved | No | Yes, by the checkpointer |
| Changes during a run | No | Yes |
| Read in a tool | runtime.context | runtime.state |
Where custom state fits
- Counting or tracking something across a conversation, such as lookups or steps used.
- Any value a tool needs to update and a later step needs to read.
lookups, the run is refused: a plain key takes one value per step. A key that several writers touch at once needs a reducer.Related
- Previous: Semantic search in long-term memory
- Next: Middleware: code around the model
- Reference: Agent state
- Ask about A17 and C40 in one message and read the error when both calls update
lookupsin the same step. - Add a key
last_order: strtoDeskStateand set it in the tool's update. - Print
agent.get_state(thread).values.keys().
Slow is fine. Stopping is the only problem.