StateGraph

In shortHow StateGraph models an agent as nodes, edges and shared state, how compile() produces a runnable graph, and when to use invoke or stream.

  • 4 min read
  • 8 sections
  • Updated
  • v0.9.2
  • Markdown

A StateGraph describes an agent as a set of nodes (Python functions, agents or tool nodes) joined by edges, all reading and writing one shared state object. You build it, call compile() to get a runnable CompiledGraph, then run it with invoke or stream.

What are the parts of a graph?

Part What it is How you add it
Node A function, Agent or ToolNode that receives state and returns an update add_node(name, func)
Static edge Always go from node A to node B add_edge(a, b)
Conditional edge A function inspects state and picks the next node add_conditional_edges(a, fn, path_map)
START Virtual node where a run begins add_edge(START, "first") or set_entry_point("first")
END Virtual node that finishes the run add_edge("last", END)
State An AgentState (or subclass) shared by all nodes StateGraph(MyState())

START and END live in tenxgraph.utils.constants. Nodes you register may return a Message, a list of messages, a plain string (stored as an assistant message), an AgentState, a dict or a Command.

How do I build a graph?

This graph routes a support ticket. It uses no model, so you can run it as is:

triage.py
from tenxgraph.core import StateGraph
from tenxgraph.core.state import AgentState, Message
from tenxgraph.storage.checkpointer import InMemoryCheckpointer
from tenxgraph.utils.constants import END


def intake(state: AgentState, config: dict) -> str:
    return "Ticket received."


def urgent(state: AgentState, config: dict) -> str:
    return "Paging the on-call engineer."


def backlog(state: AgentState, config: dict) -> str:
    return "Filed in the backlog."


def route(state: AgentState) -> str:
    user_messages = [m for m in state.context if m.role == "user"]
    return "urgent" if "outage" in user_messages[-1].text().lower() else "backlog"


graph = StateGraph()
graph.add_node("intake", intake)
graph.add_node("urgent", urgent)
graph.add_node("backlog", backlog)

graph.set_entry_point("intake")  
graph.add_conditional_edges("intake", route, {"urgent": "urgent", "backlog": "backlog"})  
graph.add_edge("urgent", END)
graph.add_edge("backlog", END)

app = graph.compile(checkpointer=InMemoryCheckpointer())

result = app.invoke(
    {"messages": [Message.text_message("Checkout outage in EU")]},
    config={"thread_id": "ticket-1"},
)
print(result["messages"][-1].text())

set_entry_point("intake") adds the edge from START. The routing function returns a key, and the path_map turns that key into a node name. If you omit path_map, the function must return the node name itself.

How does state work?

Every run starts from an AgentState. Its main field is context, the list of messages, and it uses a reducer (add_messages) so new messages are appended rather than replacing the list. AgentState also carries context_summary and internal execution_meta that the runtime uses for steps, interrupts and resume.

Add your own fields by subclassing, then pass an instance to the graph:

state.py
from tenxgraph.core.state import AgentState


class TicketState(AgentState):
    customer_tier: str = "free"
Python
graph = StateGraph(TicketState())

StateGraph(state=None, ...) creates a plain AgentState when you pass nothing. It also accepts context_manager, publisher, id_generator and container (an InjectQ container) for trimming context, emitting events, generating ids and dependency injection.

What does compile() do?

compile() checks the graph and returns a CompiledGraph. It fails early on two mistakes: no entry point (GraphError, code GRAPH_002) and nodes that no edge touches (orphans). It also rejects an edge that targets a node you never added.

Python
app = graph.compile(
    checkpointer=checkpointer,      # persist state per thread
    store=store,                    # long-term memory store
    interrupt_before=["urgent"],    # pause before a node
    interrupt_after=None,           # pause after a node
)

The remaining arguments are media_store, callback_manager and shutdown_timeout (30 seconds by default).

Should I use invoke or stream?

Both take the same input_data, config and response_granularity. Each has an async twin, ainvoke and astream.

Item invoke stream
Returns One dict when the run finishes A generator of StreamChunk objects
Use when A script or job needs the final answer A UI should show tokens and progress as they arrive
Server endpoint POST /v1/graph/invoke POST /v1/graph/stream
In async code await app.ainvoke(...) async for chunk in app.astream(...)

invoke calls asyncio.run internally, so call ainvoke from inside an event loop.

Python
for chunk in app.stream(
    {"messages": [Message.text_message("Checkout outage in EU")]},
    config={"thread_id": "ticket-2"},
):
    print(chunk.event, chunk.message.text() if chunk.message else "")

A chunk has an event (message, state, updates or error) and carries a message or state accordingly.

What goes in the run config?

Key Meaning
thread_id Which conversation to load and save. A random id is generated, with a warning, if you omit it.
user_id Owner of the thread. Defaults to anonymous.
recursion_limit Maximum steps per run. Default 25.

response_granularity is a separate argument: LOW returns only messages, PARTIAL adds context and summary, FULL adds the complete state.

Frequently asked questions

What is the difference between StateGraph and ReactAgent?
ReactAgent is a prebuilt that creates a StateGraph for you, with one agent node, one tool node and a conditional edge between them. Build a StateGraph yourself when you need your own nodes, routing or several agents.
What happens if a graph loops forever?
Each run has a recursion limit, 25 steps by default. When a run exceeds it, execution stops with a GraphRecursionError. Raise the limit with recursion_limit in the run config, or fix the routing function.
Do I need a checkpointer to use a StateGraph?
No, a graph runs without one. But without a checkpointer nothing is saved between calls, so every invoke starts from the graph's initial state and a thread cannot be resumed.
Last updated for v0.9.2Edit this page on GitHubReport an issue