State Design Patterns
How to structure LangGraph state for parallel execution, API safety, and long-running workflows — including schema separation, reducers, the Command API, state explosion mitigation, checkpointing, and debugging with time travel.
Quick Reference
- →Use MessagesState for simple chatbots — only build custom state when you have parallel nodes, private fields, or API-facing schemas
- →Any list field written by parallel nodes needs an Annotated reducer — without one, last-write-wins silently drops data
- →input_schema / output_schema are the only LangGraph-enforced way to separate public from internal state — the _ prefix convention enforces nothing
- →State explosion from unbounded list growth is the #1 production state bug — trim messages before you need to, not after
- →LangGraph has no built-in schema versioning — add new fields with defaults first, then make them required in a separate deploy
- →get_state_history() + invoke with checkpoint_id gives you deterministic replay from any prior checkpoint
- →Command(update={...}, goto='node') combines state mutation and routing in one return — use it instead of conditional edges when routing logic lives inside the node
When State Complexity Is Worth It
Most agents don't need custom state. If your graph has a single entry point, a single exit, and no parallel branches, MessagesState is almost always enough. The question to ask before adding state complexity is: what can't MessagesState do that I actually need?
| Scenario | Use MessagesState | Use custom state |
|---|---|---|
| Chatbot / Q&A | ✓ — messages list is sufficient | — |
| Parallel sub-tasks accumulating results | — | ✓ — reducer needed to merge concurrent writes |
| API-exposed graph with internal bookkeeping | — | ✓ — input/output schema keeps private fields from leaking |
| Long-running workflow with retry tracking | — | ✓ — private retry counter, RemainingSteps |
| Multi-turn workflow with resume-on-failure | Partial — messages checkpoint, but no custom recovery logic | ✓ — full checkpoint + state-based recovery |
from langgraph.graph import MessagesState class State(MessagesState): pass # add fields only when you need them MessagesState gives you messages with the add_messages reducer already wired. Extend it with class State(MessagesState): when you need additional fields.