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 →

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

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.

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.

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

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

Example
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.
  • 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 is refused: a plain key takes one value per step. A key that several writers touch at once needs a reducer.
Try it yourself
  • Ask about A17 and C40 in one message and read the error when both calls update lookups in the same step.
  • 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.