Skip to main content
State is the core idea behind Graph<S>. Every node reads the current state and emits an update; the engine applies the update and moves on. How updates apply is up to you — concatenate lists, deep-merge JSON, replace whole values. That decision is encoded in your GraphState impl, either by hand or via a derive.

The shape

apply is the heart. The engine calls it once per superstep with the merged update from all nodes that ran in that step.

Two ways to define state

Most explicit. Every field decision is in front of you.

Reducer reference

Why typed state matters

In Python, graph state is a dict. You learn at runtime whether a key exists, whether the type is right, whether the reducer is hooked up. In Rust:
  • Missing fields don’t compile. The Update struct must include every field your apply mutates.
  • Reducer mismatches don’t compile. A reducer for Vec<Message> won’t accept String.
  • The state shape is shared with consumers. Observers, time-travel debuggers, checkpointers — they all see the same S.

Default values

State and Update need Default (for the engine’s bootstrap). Either derive it (#[derive(Default)]) or implement it manually for non-trivial cases:
The default is what you get from Graph::invoke(State::default(), cfg) and also what every checkpoint resumes from when there’s no prior state.

Custom reducers

For fields that don’t fit the four built-ins, write apply yourself. Either drop the macro or mix manual + macro per-field — the macro emits standard Rust, so you can expand and tweak.

How it works

  • Updates are merged within a superstep, then applied once. If two nodes ran in the same superstep, both their updates contribute.
  • apply is sync. Don’t block in there — it runs on the engine’s hot path.
  • Snapshots are taken after apply. Checkpoints, observers, and stream events all see the post-apply state.

See also

Graphs and state

The wider mental model.

Control flow

Goto, edges, fan-out.

Checkpointing

Persist state, time-travel, resume.