langgraph-state

star 3

Managing state in LangGraph: schemas, reducers, channels, and message passing for coordinating agent execution

christian-bromann By christian-bromann schedule Updated 2/11/2026

name: langgraph-state description: Managing state in LangGraph: schemas, reducers, channels, and message passing for coordinating agent execution language: python

langgraph-state (Python)


name: langgraph-state description: Managing state in LangGraph - schemas, reducers, channels, and message passing for coordinating agent execution

Overview

State is the central data structure in LangGraph that persists throughout graph execution. Proper state management is crucial for building reliable agents.

Key Concepts:

  • State Schema: Defines the structure and types of your state
  • Reducers: Control how state updates are applied
  • Channels: Low-level state management primitives
  • Message Passing: How nodes communicate via state updates

Decision Table: State Update Strategies

Need Solution Use Case
Overwrite value No reducer (default) Simple fields like counters
Append to list operator.add Message history, logs
Custom logic Custom reducer Complex merging, validation
Messages Annotated[list, add_messages] Chat applications

Key Concepts

1. State Schema with TypedDict

from typing_extensions import TypedDict

class State(TypedDict):
    input: str
    output: str
    count: int

2. Reducers

Reducers determine how updates are merged with existing state:

from typing import Annotated
import operator

class State(TypedDict):
    # Default: overwrites
    name: str
    
    # Reducer: appends to list
    messages: Annotated[list, operator.add]
    
    # Reducer: sums integers
    total: Annotated[int, operator.add]

3. Channels API

For advanced state control:

Channel Type Behavior
LastValue Stores most recent value
BinaryOperatorAggregate Combines with reducer
Topic Collects all values
EphemeralValue Resets between supersteps

Code Examples

Basic State Management

from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict

class State(TypedDict):
    input: str
    processed: str
    count: int

def process(state: State) -> dict:
    return {
        "processed": state["input"].upper(),
        "count": state.get("count", 0) + 1
    }

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

result = graph.invoke({"input": "hello", "count": 0})
print(result)  # {'input': 'hello', 'processed': 'HELLO', 'count': 1}

Messages with Reducer

from typing import Annotated
import operator
from langchain.messages import BaseMessage, HumanMessage, AIMessage

class MessagesState(TypedDict):
    messages: Annotated[list[BaseMessage], operator.add]

def add_response(state: MessagesState) -> dict:
    user_msg = state["messages"][-1].content
    return {"messages": [AIMessage(content=f"Response to: {user_msg}")]}

graph = (
    StateGraph(MessagesState)
    .add_node("respond", add_response)
    .add_edge(START, "respond")
    .add_edge("respond", END)
    .compile()
)

result = graph.invoke({
    "messages": [HumanMessage(content="Hello!")]
})
print(len(result["messages"]))  # 2 (original + response)

Custom Reducer

from typing import Annotated

def merge_dicts(current: dict, update: dict) -> dict:
    """Custom reducer to merge dictionaries."""
    return {**current, **update}

class State(TypedDict):
    metadata: Annotated[dict, merge_dicts]
    data: str

def update_metadata(state: State) -> dict:
    return {"metadata": {"timestamp": "2024-01-01"}}

graph = (
    StateGraph(State)
    .add_node("update", update_metadata)
    .add_edge(START, "update")
    .add_edge("update", END)
    .compile()
)

result = graph.invoke({
    "metadata": {"user": "alice"},
    "data": "test"
})
# metadata is merged: {"user": "alice", "timestamp": "2024-01-01"}

Bypassing Reducers with Overwrite

from langgraph.types import Overwrite

class State(TypedDict):
    items: Annotated[list, operator.add]  # Has reducer

def reset_items(state: State) -> dict:
    # Bypass reducer and replace entire list
    return {"items": Overwrite(["new_item"])}

graph = (
    StateGraph(State)
    .add_node("reset", reset_items)
    .add_edge(START, "reset")
    .add_edge("reset", END)
    .compile()
)

result = graph.invoke({"items": ["old1", "old2"]})
print(result["items"])  # ['new_item'] (not appended)

Using Channels API

from langgraph.channels import LastValue, BinaryOperatorAggregate

class State(TypedDict):
    counter: int
    logs: list[str]

# Alternative way to define state
from langgraph.graph import StateGraph

channels = {
    "counter": BinaryOperatorAggregate(int, operator.add, default=lambda: 0),
    "logs": BinaryOperatorAggregate(list, operator.add, default=lambda: [])
}

def increment(state: dict) -> dict:
    return {"counter": 1, "logs": ["incremented"]}

graph = (
    StateGraph(State, channels=channels)
    .add_node("increment", increment)
    .add_edge(START, "increment")
    .add_edge("increment", END)
    .compile()
)

Partial State Updates

class State(TypedDict):
    field1: str
    field2: str
    field3: str

def update_field1(state: State) -> dict:
    # Only update field1, others unchanged
    return {"field1": "updated"}

def update_field2(state: State) -> dict:
    # Only update field2
    return {"field2": "also updated"}

graph = (
    StateGraph(State)
    .add_node("node1", update_field1)
    .add_node("node2", update_field2)
    .add_edge(START, "node1")
    .add_edge("node1", "node2")
    .add_edge("node2", END)
    .compile()
)

result = graph.invoke({
    "field1": "original1",
    "field2": "original2",
    "field3": "original3"
})
# field1: "updated", field2: "also updated", field3: "original3"

Boundaries

What You CAN Configure

✅ Define custom state schemas ✅ Add reducers to fields ✅ Create custom reducer functions ✅ Use built-in channels ✅ Bypass reducers with Overwrite ✅ Partial state updates ✅ Nested state structures

What You CANNOT Configure

❌ Change state after graph compilation ❌ Access state outside node functions ❌ Modify state directly (must return updates) ❌ Share state between separate graphs

Gotchas

1. Forgot Reducer for List

# ❌ WRONG - List will be overwritten
class State(TypedDict):
    items: list  # No reducer!

# Node 1 returns: {"items": ["A"]}
# Node 2 returns: {"items": ["B"]}
# Final state: {"items": ["B"]}  # A is lost!

# ✅ CORRECT
from typing import Annotated
import operator

class State(TypedDict):
    items: Annotated[list, operator.add]
# Final state: {"items": ["A", "B"]}

2. State Must Return Dict

# ❌ WRONG - Returning entire state object
def my_node(state: State) -> State:
    state["field"] = "updated"
    return state  # Don't do this!

# ✅ CORRECT - Return dict with updates
def my_node(state: State) -> dict:
    return {"field": "updated"}

3. Default Values

# ❌ RISKY - No default, may cause errors
class State(TypedDict):
    count: int  # What if not initialized?

def increment(state: State) -> dict:
    return {"count": state["count"] + 1}  # KeyError!

# ✅ BETTER - Use .get() with default
def increment(state: State) -> dict:
    return {"count": state.get("count", 0) + 1}

4. Reducer Type Mismatch

# ❌ WRONG - Reducer expects list, but receives string
class State(TypedDict):
    items: Annotated[list, operator.add]

def bad_update(state: State) -> dict:
    return {"items": "not a list"}  # Type error!

# ✅ CORRECT
def good_update(state: State) -> dict:
    return {"items": ["item"]}

Links

Install via CLI
npx skills add https://github.com/christian-bromann/langchain-skills --skill langgraph-state
Repository Details
star Stars 3
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator
christian-bromann
christian-bromann Explore all skills →