What is a state graph?
A state graph is a graph-based model for AI agent workflows where nodes represent discrete processing steps (an LLM call, a tool execution, a validation step) and edges define how typed state moves between them. State flows into a node, is transformed, and the updated state continues to the next node as determined by conditional routing logic.
State graphs make agent control flow explicit and inspectable — unlike free-form "while" loops or declarative role-based systems, every transition is visible, testable, and replayable.
Why graphs for agents?
Agent logic has three structural needs that graphs address well:
- Cycles — the Reason-Act loop needs to return to the "Reason" node after every tool call
- Conditional branching — route to different nodes based on what the LLM decided to do
- State management — pass structured data through the workflow without global variables
A state graph encodes all three: cycles via back-edges, branching via conditional edges, state via typed state objects passed from node to node.
Anatomy of a state graph
┌──────────────┐
│ AGENT node │◄─────────────┐
│ (LLM call) │ │
└──────┬───────┘ │
│ │
conditional edge │
(has tool calls?) │
│ │
┌──────▼───────┐ │
│ TOOLS node ├──────────────┘
│ (run tools) │ back-edge
└──────────────┘
│
END (when no tool calls)
Three components:
- Nodes — functions that take state and return updated state
- Edges — direct connections between nodes
- Conditional edges — routing functions that inspect state and return the name of the next node
Building a state graph with AgentFlow
from agentflow.core.graph import Agent, StateGraph, ToolNode
from agentflow.core.state import AgentState, Message
from agentflow.utils import END
# Define tools
def search(query: str) -> str:
"""Search the web."""
return f"Results for: {query}"
def calculate(expr: str) -> str:
"""Evaluate a math expression."""
return str(eval(expr, {"__builtins__": {}}, {}))
# Create nodes
tool_node = ToolNode([search, calculate])
agent_node = Agent(
model="google/gemini-2.5-flash",
system_prompt=[{"role": "system", "content": "Helpful assistant. Use tools when needed."}],
tool_node="TOOLS",
)
# Define routing logic
def route(state: AgentState) -> str:
last = state.context[-1] if state.context else None
if last and getattr(last, "tools_calls", None) and last.role == "assistant":
return "TOOLS"
return END
# Build the graph
graph = StateGraph(AgentState)
graph.add_node("AGENT", agent_node)
graph.add_node("TOOLS", tool_node)
graph.add_conditional_edges("AGENT", route, {"TOOLS": "TOOLS", END: END})
graph.add_edge("TOOLS", "AGENT") # back-edge creates the cycle
graph.set_entry_point("AGENT")
app = graph.compile()
The add_edge("TOOLS", "AGENT") line creates the ReAct cycle. After every tool call, control returns to the agent for another reasoning step.
State
State is a typed Python class that carries all data through the graph:
from agentflow.core.state import AgentState, Message
# AgentState is the built-in state class
# It includes: messages (user/assistant history) + context (LLM messages)
# You can extend it with custom fields:
from agentflow.core.state import AgentState
from pydantic import Field
class MyState(AgentState):
user_id: str = ""
search_results: list[str] = Field(default_factory=list)
Each node receives the full state, makes changes, and returns the updated state. Nodes cannot have side effects on state outside of their return value — this makes the graph deterministic and replayable.
State graphs vs alternatives
| Approach | Control flow | Debuggability | Flexibility |
|---|---|---|---|
| State graph (AgentFlow, LangGraph) | Explicit nodes + edges | High — trace every transition | High |
| Role-based (CrewAI) | Declared crews + processes | Medium — process hides routing | Medium |
| Conversation-driven (AutoGen) | LLM selects next speaker | Low — routing is opaque | High |
| Raw while loop | Custom Python | Low — custom code | High |
State graphs trade some flexibility for debuggability: you can always see exactly which node ran, what state it received, and what state it produced.