LangGraph: State Machines for Complex Agents

By Carlos Montiel | Enterprise AI Specialist
Leer en español →
Published: 2026-07-28 | By: Carlos Montiel | Reading time: ~4 minutes

Real agents aren't linear sequences of steps: they branch, repeat actions, and need to remember context. LangGraph models this explicitly as a state machine.

The mental model: state graph, not chain

LangGraph represents an agent as a directed graph where nodes are functions (or LCEL chains) that receive and modify a shared state, and edges determine which node runs next. Unlike an LCEL chain, edges can be conditional and can form cycles: the graph can return to an already-visited node as many times as the logic requires.

State is the central element. It's typically defined with `TypedDict`, and each node receives the full state, returning only the keys it modifies; LangGraph takes care of merging that update with the existing state.

Defining the state schema

from typing import TypedDict, Annotated, Sequence from langchain_core.messages import BaseMessage import operator class ConversationState(TypedDict): messages: Annotated[Sequence[BaseMessage], operator.add] steps_executed: int finished: bool

Using `Annotated[..., operator.add]` is the least intuitive part for anyone coming from classic imperative programming: it tells LangGraph that when a node returns `{"messages": [new_message]}`, it should concatenate that list onto the existing state instead of overwriting it. Without that reducer, every node update would replace the entire history.

Building the graph

from langgraph.graph import StateGraph, START, END def analyze_node(state: ConversationState): last = state["messages"][-1] return {"steps_executed": state["steps_executed"] + 1} def respond_node(state: ConversationState): response = llm.invoke(state["messages"]) return {"messages": [response]} def route(state: ConversationState): if state["steps_executed"] > 5: return "end" return "respond" builder = StateGraph(ConversationState) builder.add_node("analyze", analyze_node) builder.add_node("respond", respond_node) builder.add_edge(START, "analyze") builder.add_conditional_edges("analyze", route, {"respond": "respond", "end": END}) builder.add_edge("respond", "analyze") compiled_graph = builder.compile()

Note the explicit cycle: `respond` goes back to `analyze`, which can route back to `respond` again. This is exactly what a classic `AgentExecutor` did opaquely under the hood; LangGraph makes it visible and editable.

Running and inspecting the graph

result = compiled_graph.invoke({ "messages": [HumanMessage(content="How do I renew my software license?")], "steps_executed": 0, "finished": False, }) for event in compiled_graph.stream({"messages": [...], "steps_executed": 0}): print(event) # prints the state after each node executes

The `.stream()` method is particularly useful for debugging: it shows the full state after each node, making it visible exactly where in the cycle the agent made a wrong decision — something that's much harder to trace in a manual implementation with a `while` loop and boolean flags.

Checkpointers: persisting state across invocations

A `StateGraph` compiled with a checkpointer keeps state across calls, identified by a `thread_id`, which is the foundation of conversational memory (covered in detail in the next article in this series):

from langgraph.checkpoint.memory import MemorySaver memory = MemorySaver() app = builder.compile(checkpointer=memory) config = {"configurable": {"thread_id": "user-conversation-882"}} app.invoke({"messages": [HumanMessage(content="Hello")]}, config=config) app.invoke({"messages": [HumanMessage(content="What about what I asked you before?")]}, config=config)

When this complexity is justified

A state graph is the right tool when the agent's behavior depends on accumulated history, not just the last message: a support agent that needs to remember how many times it already tried to resolve an issue before escalating to a human, a research pipeline that iterates over searches until it gathers enough evidence, or any flow where "how many times have we already been here" changes the next decision. If your flow doesn't have this characteristic, a simple LCEL chain is still the more maintainable choice.

Carlos Montiel
Enterprise AI Solutions Architect
Specialist in LLMs, Agents, and Orchestration
guatemalia.com/en/#contact · info@guatemalia.com

Need to implement AI at your company?

Carlos Montiel is an enterprise AI solutions architect. He implements LLMs, Agents, RAG, and orchestrators for companies across Guatemala and Latin America. Reach out for a consultation.

Contact Carlos Montiel

info@guatemalia.com