State
State is the dict that carries data through a run. It's how a node knows what earlier nodes did.
The basics
The state dict:
- Starts as whatever you pass to
execute()— orinitial_state, if you set one. - Lives for the whole run.
- Is readable by any node that asks for it.
- Grows as nodes return values.
Seeding state
Set an initial state when you create the graph:
from graphai import Graph
graph = Graph(initial_state={
"history": [],
"context": "initial context",
"metadata": {
"user_id": "user123",
"session_start": 1625097600,
},
})Leave it out and the state starts empty.
The state methods
current = graph.get_state()
graph.set_state({"new_state": "value"}) # replace
graph.update_state({"additional": "data"}) # merge
graph.reset_state() # clearReading state in a node
Declare a state parameter and GraphAI passes it in:
from graphai import node
@node
async def stateful_node(input: dict, state: dict):
history = state.get("history", [])
context = state.get("context", "")
processed = process_with_context(input["data"], context)
return {"result": processed}Changing state
Two ways, and both persist.
Return it. Any key in the returned dict is merged into the state. This is the clearest way to hand data to the next node:
@node
async def update_history(input: dict, state: dict):
history = state.get("history", [])
return {
"result": process(input["query"]),
"history": history + [input["query"]],
}Mutate it. The state a node receives is the graph's actual state object, so in-place changes stick:
@node
async def append_history(input: dict, state: dict):
state.setdefault("history", []).append(input["query"])
return {}Returning keys is usually the better habit — it's explicit, and it's easy to see what a node contributes. Mutation is handy for accumulating into an existing list or dict.
State vs. input
There's no separate "input" mechanism. The dict you pass to execute() is the initial state, and each node receives whichever keys it declares as parameters. By convention the request goes under an input key, which is why so many nodes declare input: dict — but that's just a key like any other.
@node
async def process_with_both(input: dict, state: dict):
query = input["query"] # state["input"]["query"]
history = state.get("history", []) # the whole state
result = process_with_history(query, history)
return {"result": result, "history": history + [query]}Persisting across runs
State lives as long as the graph object. To carry it between runs, save and restore it:
result = await graph.execute(input={"input": input_data})
saved = graph.get_state()
store_state(saved)
# later
graph.set_state(load_state())Scope
State belongs to a graph instance. Two graphs never share it:
graph1 = Graph(initial_state={"id": "graph1"})
graph2 = Graph(initial_state={"id": "graph2"})
graph1.update_state({"value": 1})
graph2.update_state({"value": 2})Good habits
- Keep it serializable — dicts, lists, strings, numbers — so you can save it.
- Be selective. Only put things in state that later nodes actually need.
- Write down the shape. A documented state structure saves everyone time.
- Watch the size. Don't let it grow without bound in long-running apps.