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
A state with more than messages
The video builds a supervisor multi-agent system. One supervisor node holds control and, for each input, decides whether the researcher, the analyst or the writer works next. Asked to write an article on agentic AI, it can send the job to the researcher, who passes the research to the writer. To track that progress, the state needs more than messages. The state definition extends MessagesState, which brings the messages variable, with keys of its own. After a first draft with task assignments and agent outputs, the video settles on these fields:
class SupervisorState(MessagesState):
"""State for the multi-agent system"""
next_agent: str = ""
research_data: str = ""
analysis: str = ""
final_report: str = ""
task_complete: bool = False
current_task: str = ""next_agent says who works next; research_data, analysis and final_report hold each agent's work; task_complete and current_task track the job. The supervisor chain's prompt reads the state as three flags, has research, has analysis and has report, and answers with the next agent's name, or "done" once all the work is finished. MessagesState is LangGraph's base class for a graph you build yourself. create_agent uses AgentState instead, which also brings messages, so the pattern is the same.
Now the shop. It 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. The pieces in this section go, in order, in one file, state_agent.py.
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. Add these lines to the end of state_agent.py; DeskState and lookup_order are the ones defined above them in the same file.
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from langchain.chat_models import init_chat_model
agent = create_agent(init_chat_model("groq:openai/gpt-oss-120b", temperature=0), tools=[lookup_order], state_schema=DeskState, # uses your GROQ_API_KEY
system_prompt="You are the support assistant for a small online shop. Answer in one or two short sentences, using only what the tools returned.",
checkpointer=InMemorySaver())
thread = {"configurable": {"thread_id": "ravi-1"}}Counting lookups across two turns
Add these lines to the end of state_agent.py and run it: 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)2 C40 is currently waiting for stock.
Setting a key when you invoke
Keys other than messages can be passed in with the input. Put these lines in place of the two calls above. 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"])11
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; leave it out and the run raises
ValueError. - 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 stops with an error: a plain key takes one value per step. A key that several writers touch at once needs a reducer, a function that says how to combine two updates; messages has one that appends.Related
- Previous: Semantic search in long-term memory
- Next: Middleware: code around the model
- Reference: Agent state
- Ask about A17 on a new thread without passing
lookupsand printresult["lookups"]. - 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.