CTCaliberTek
Course: LangGraph: Step by Step (9 Modules) · Lesson 2 of 9Log in to track your progress and earn a certificate when you finish the course.
AI & Machine Learning

LangGraph Module 2: State Schemas and Reducers

Published October 17, 2026·Updated October 29, 2026·8 min read

Edited by , Senior Data Analyst. How we make our content

Like SaveLog in to like, save, and track progress.

Introduction

State is the heart of a graph, and how updates are combined is controlled by reducers. By default an update overwrites the old value. With a reducer you can append instead, which is what you need for message histories and for parallel branches. This module explains the options.

Prerequisites

  • Module 1 completed

Step-by-Step Guide

Step 1: Understand the default: overwrite

With no reducer, a node's returned value replaces the existing value for that key. That is fine for single values such as a counter or a final answer.

Step 2: Add a reducer to append instead

Annotate a key with a reducer function. A reducer receives the current value and the update, and returns the merged value. operator.add on lists appends the new items to the existing ones.

from operator import add
from typing import Annotated
from typing_extensions import TypedDict

class State(TypedDict):
    count: int
    log: Annotated[list[str], add]

def step_one(state: State) -> dict:
    return {"count": state["count"] + 1, "log": ["step_one ran"]}

def step_two(state: State) -> dict:
    return {"count": state["count"] * 10, "log": ["step_two ran"]}

Step 3: Run it and compare the keys

The count key was overwritten at each step, while the log key accumulated entries from both nodes.

from langgraph.graph import StateGraph, START, END

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

print(graph.invoke({"count": 1, "log": []}))
# {'count': 20, 'log': ['step_one ran', 'step_two ran']}

Step 4: Use message state for chat

Conversation state is so common that LangGraph ships MessagesState, which holds a messages list with the add_messages reducer. It appends new messages and accepts plain dictionaries such as role and content pairs, converting them into message objects, so inside nodes use attribute access like state["messages"][-1].content. Subclass it to add your own fields.

from langgraph.graph import MessagesState

class State(MessagesState):
    documents: list[str]

Step 5: Separate input and output schemas

A graph can accept a narrow input, hold richer internal state, and return a narrow output. Pass input_schema and output_schema when creating the StateGraph. To clear a list that uses an append reducer, wrap the new value in Overwrite from langgraph.types, because an empty list would simply append nothing.

class InputState(TypedDict):
    question: str

class OutputState(TypedDict):
    answer: str

class OverallState(InputState, OutputState):
    notes: str

builder = StateGraph(
    OverallState, input_schema=InputState, output_schema=OutputState
)

FAQs

Why do parallel branches need reducers?
If two nodes update the same key in the same step, LangGraph needs a rule for combining the updates. A reducer such as add provides it. Without one, concurrent writes to the same key raise an error.
Can I mutate the state inside a node?
Don't. Return a dictionary of updates instead. LangGraph applies the reducers and records the change, which is what makes checkpointing and replay work.

Summary

Overwrite is the default merge. Use a reducer like add to accumulate values, MessagesState for chat history, and input and output schemas to keep a graph's interface small.

Related tutorials

Comments

Log in or create a free account to join the discussion.

  • No comments yet. Be the first to comment.