TechByteByByte

Messages and MessagesState: Connecting LangChain Messages to Graph State

Apply Module 11's reducer mechanism to the single most common real accumulation need — a growing conversation — and meet MessagesState, the prebuilt class that already wires it in for you.

#LangGraph#Messages#State#Reducers

Recall Module 11’s closing preview — add_messages is the exact same reducer mechanism you just learned, applied to conversation history specifically. This module makes that connection completely concrete, linking the HumanMessage, AIMessage, and ToolMessage objects you already know deeply from your LangChain course to real, working LangGraph state.

The problem, watched directly

from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain.messages import HumanMessage, AIMessage

class State(TypedDict):
    messages: list

def add_reply(state: State) -> dict:
    return {"messages": [AIMessage(content="Here's my reply.")]}

builder = StateGraph(State)
builder.add_node("add_reply", add_reply)
builder.add_edge(START, "add_reply")
builder.add_edge("add_reply", END)
graph = builder.compile()

result = graph.invoke({"messages": [HumanMessage(content="Hello")]})
print(result["messages"])

Run this, and notice result["messages"] contains only the AIMessage — the original HumanMessage is gone. This is Module 3’s “merge, not replace” rule applying exactly as it should to a plain list field with no reducer: the new value for messages simply replaced the old one entirely, exactly like any other unreduced field. For a growing conversation, that’s genuinely the wrong behavior.

flowchart LR
    A["Plain list field:\nnew value REPLACES old"] --> B["HumanMessage lost\nonly AIMessage remains"]
    C["Annotated[list, add_messages]:\nnew value APPENDS to old"] --> D["Both messages survive,\nin order"]

The fix: add_messages

from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain.messages import HumanMessage, AIMessage

class State(TypedDict):
    messages: Annotated[list, add_messages]

def add_reply(state: State) -> dict:
    return {"messages": [AIMessage(content="Here's my reply.")]}

builder = StateGraph(State)
builder.add_node("add_reply", add_reply)
builder.add_edge(START, "add_reply")
builder.add_edge("add_reply", END)
graph = builder.compile()

result = graph.invoke({"messages": [HumanMessage(content="Hello")]})
for m in result["messages"]:
    print(type(m).__name__, "-", m.content)

Now both messages genuinely survive — the original HumanMessage, and the new AIMessage, appended in order. add_messages is exactly Annotated[list, reducer_function] from Module 11, just prebuilt specifically for this exact case, and with one more real, practical detail: it also correctly handles updating an existing message with the same ID, rather than always blindly appending — genuinely useful when you need to edit a message already in history.

MessagesState: the prebuilt shortcut

Because this specific pattern — a messages field with add_messages — is so overwhelmingly common in agent-shaped graphs, LangGraph ships a ready-made state class for it.

from langgraph.graph import StateGraph, START, END, MessagesState
from langchain.chat_models import init_chat_model

model = init_chat_model("openai:gpt-4o-mini")

def call_model(state: MessagesState) -> dict:
    response = model.invoke(state["messages"])
    return {"messages": [response]}

builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")
builder.add_edge("call_model", END)
graph = builder.compile()

result = graph.invoke({"messages": [{"role": "user", "content": "What is LangGraph?"}]})
print(result["messages"][-1].content)

MessagesState already has exactly the schema from the previous example built in — you don’t need to write class State(TypedDict): messages: Annotated[list, add_messages] yourself every single time. Notice also that model.invoke(state["messages"]) works directly on the list stored in state — this is genuinely the same .invoke() call from your LangChain course, taking a real list of messages, exactly as it always has.

Building a genuine, growing conversation across multiple invocations

from langgraph.graph import StateGraph, START, END, MessagesState
from langchain.chat_models import init_chat_model

model = init_chat_model("openai:gpt-4o-mini")

def call_model(state: MessagesState) -> dict:
    return {"messages": [model.invoke(state["messages"])]}

builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")
builder.add_edge("call_model", END)
graph = builder.compile()

state = graph.invoke({"messages": [{"role": "user", "content": "My favorite color is teal."}]})
state = graph.invoke({"messages": state["messages"] + [{"role": "user", "content": "What's my favorite color?"}]})
print(state["messages"][-1].content)

Notice each call feeds the entire growing message list back in — genuinely the exact same principle from your LangChain course’s own conversation-building example, just now flowing through a graph’s state instead of a plain Python list you managed by hand. The model correctly answers the second question, because the full, real history — including the first exchange — is present every time.

Extending MessagesState with your own fields

Real graphs almost always need more than just messages — recall the customer resolution workflow’s other fields from Module 3.

from langgraph.graph import StateGraph, START, END, MessagesState

class State(MessagesState):
    customer_id: str
    resolved: bool

def process(state: State) -> dict:
    return {"resolved": True}

builder = StateGraph(State)
builder.add_node("process", process)
builder.add_edge(START, "process")
builder.add_edge("process", END)
graph = builder.compile()

result = graph.invoke({"messages": [{"role": "user", "content": "Help!"}], "customer_id": "c_1", "resolved": False})
print(result["customer_id"], result["resolved"], len(result["messages"]))

class State(MessagesState) genuinely inherits the built-in messages field, with its add_messages reducer already attached, while adding customer_id and resolved as ordinary, unreduced fields alongside it. This is the real, practical shape most agent-style graphs in this course from here forward will actually use.

Common mistakes worth avoiding

Using a plain list for messages and being surprised history disappears. Recall this module’s opening example — without add_messages, a messages field behaves exactly like any other unreduced field from Module 3: the newest write replaces everything before it.

Forgetting that add_messages can update, not just append. If a returned message shares an ID with one already in state, add_messages replaces that specific message in place rather than adding a duplicate — genuinely useful, but worth knowing about deliberately rather than discovering by accident.

Passing a fresh, single-message list on every invocation, expecting memory to persist automatically. Recall this module’s growing-conversation example — each call has to explicitly include the prior history in its input. Real, automatic persistence across separate invocations is a different mechanism entirely, covered properly once checkpointing is introduced.

What you should take away from this module

  • A plain list field for messages has no special behavior — it’s replaced, not appended, exactly like Module 3 taught for any unreduced field.
  • add_messages is Module 11’s reducer mechanism, purpose-built for conversation history — appending new messages, and correctly updating existing ones sharing an ID.
  • MessagesState is the prebuilt shortcut already wiring add_messages into a messages field, saving you from redeclaring it every time.
  • class State(MessagesState) lets you add your own, ordinary fields alongside the built-in, reducer-backed messages field.

Where this goes next

The next module covers Loops properly — generate, review, revise, with a genuine, bounded retry count — building directly on the message-handling and reducer mechanics you now understand completely.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed