LangChain (YT style)LangChain 1.4 · Python 3.12+
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 →

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 supervisor state with more than messages · from the Building Agents And Multi Agents With LangGraph, Part 3 · 16:28 to 20:43

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:

python
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

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

python
from langchain.agents import AgentState

class DeskState(AgentState):   # keeps messages, adds one key
    lookups: int

Imports and the orders

A tool changes the state by returning a Command. Bring in the pieces it needs and the order data.

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

python
@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 state

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

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

ExampleAPI key
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. Put these lines in place of the two calls above. This thread starts the count at 10, and one lookup makes it 11.

ExampleAPI key
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 Command whose update carries 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

ContextState
Set whenOnce, at invokeOn input and by tools
SavedNoYes, by the checkpointer
Changes during a runNoYes
Read in a toolruntime.contextruntime.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.
Watch out. If two tool calls in one step both update 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.
Try it yourself
  • Ask about A17 on a new thread without passing lookups and print result["lookups"].
  • Add a key last_order: str to DeskState and set it in the tool's update.
  • Print agent.get_state(thread).values.keys().

Slow is fine. Stopping is the only problem.