Skip to content

StateSnapshot: the state snapshot at each superstep boundary

源码版本1.2.9

Responsibilities

StateSnapshot is the immutable snapshot LangGraph exposes at every superstep boundary — "what the entire graph looks like the moment this step finished". It packs five things into a single NamedTuple: the current values of each channel (values), which nodes will run next (next), the config used this run, the corresponding checkpoint metadata (metadata + created_at), and the parent checkpoint's config (StateSnapshot definition:643-661). When you call graph.get_state(config), graph.aget_state(config), or iterate graph.get_state_history(config), this is the type you get back.

Architecturally it sits "above checkpoints, below the user view". The checkpoint (CheckpointTuple) is the engine's internal serialization format — it stores low-level fields like channel_values / channel_versions / pending_writes and does not tell you what the business state looks like. StateSnapshot translates that layer into "a dict the business can read directly, plus the next steps to run, plus the task list" (_prepare_state_snapshot:1145-1162). Subgraph state uses it too — task.state is populated with a subgraph StateSnapshot when subgraphs=True is recursed (PregelTask.state:200-201).

StateSnapshot itself is an immutable NamedTuple; to "change state" you must write new values via graph.update_state(config, values). The engine turns that into a pending write with NULL_TASK_ID, lands it onto the next checkpoint, and then produces a fresh StateSnapshot.

Design motivation

Why not expose CheckpointTuple directly?

  • Decouple the internal representation: For efficient serialization, the checkpoint format stores flat dicts with versioned fields like channel_versions; what the user cares about is "my business state dict". read_channels reads the channel values in between (read_channels:1258). The extra StateSnapshot layer lets the checkpoint format evolve freely without breaking the public API.
  • Carry "what's next" semantics: The engine knows which nodes will run next, but CheckpointTuple only stores the values written. StateSnapshot calls prepare_next_tasks at construction time (prepare_next_tasks:1178-1195) to compute next=(node_name, ...), directly answering "what happens next".
  • Task list visibility: Each StateSnapshot.tasks is a tuple of PregelTask to execute this step (tasks:658), including writes / interrupts / subgraph state — the key to deciding "which node are we stuck on, and what input is it waiting for" in human-in-the-loop scenarios.
  • Immutable = comparable: Historical snapshots are threaded into a linked list via parent_config (parent_config:656). get_state_history returns the snapshot list in reverse chronological order, and any one can be re-pointed to with its config for time travel.
  • Pending writes applied transparently: get_state temporarily applies any pending_writes that have not yet landed on a checkpoint before reading the channels (apply pending writes:1239-1249). So when you call get_state right after a node has written but before the checkpoint has landed, you see the latest value, not the previous step's stale value.

Key files

  • StateSnapshot NamedTuple:643-661 — 7 fields: values / next / config / metadata / created_at / parent_config / tasks / interrupts.
  • PregelTask:200-217 — The task object; the state field holds a subgraph StateSnapshot or the RunnableConfig locating it when recursing.
  • _prepare_state_snapshot:1145-1162 — Fallback when saved is empty: returns an empty snapshot with values={} / next=() / tasks=().
  • step computation + prepare_next_tasks:1167-1195 — Derives the current step number from saved.metadata["step"] and calls prepare_next_tasks to compute the task dict for this step.
  • task_states recursion:1199-1227 — For every task whose task.name is in subgraphs, assembles the checkpoint_ns and recursively calls the subgraph's get_state.
  • apply pending writes:1229-1255 — Temporarily applies NULL_TASK_ID writes and regular pending writes to the channels, then computes tasks_with_writes.
  • assemble StateSnapshot:1257-1266 — Combines channel values, the next-node tuple, config, metadata, ts, parent_config, tasks, and interrupts into the final StateSnapshot.
  • get_state:1392-1434 — Sync entry: resolves the checkpointer, routes by checkpoint_ns to a subgraph, and decides whether to apply_pending_writes.
  • get_state_history:1480 — Iterates all historical checkpoints under the same thread_id, producing a StateSnapshot for each.
  • _aprepare_state_snapshot:1268 — Async version; logic mirrors the sync one, except channels_from_checkpoint becomes achannels_from_checkpoint.

Data flow

Here is the key section where _prepare_state_snapshot translates a checkpoint into a StateSnapshot: derive the step number from saved.metadata, call prepare_next_tasks to get this step's tasks, then handle subgraph recursion.

python
step = saved.metadata.get("step", -1) + 1
stop = step + 2
channels, managed = channels_from_checkpoint(
    self.channels,
    saved.checkpoint,
    saver=self.checkpointer
    if isinstance(self.checkpointer, BaseCheckpointSaver) else None,
    config=saved.config,
)
next_tasks = prepare_next_tasks(
    saved.checkpoint,
    saved.pending_writes or [],
    self.nodes,
    channels,
    managed,
    saved.config,
    step,
    stop,
    for_execution=True,
    store=self.store,
    checkpointer=(self.checkpointer
                  if isinstance(self.checkpointer, BaseCheckpointSaver) else None),
    manager=None,
)

(prepare_next_tasks call:1167-1195)

Finally, the read channel values and the task names filtered to those without pending writes are assembled into the immutable snapshot:

python
return StateSnapshot(
    read_channels(channels, self.stream_channels_asis),
    tuple(t.name for t in next_tasks.values() if not t.writes),
    patch_checkpoint_map(saved.config, saved.metadata),
    saved.metadata,
    saved.checkpoint["ts"],
    patch_checkpoint_map(saved.parent_config, saved.metadata),
    tasks_with_writes,
    tuple([i for task in tasks_with_writes for i in task.interrupts]),
)

(assemble:1257-1266)

Boundaries and failure modes

  • No checkpoint returns an empty snapshot rather than raising (empty saved:1152-1162), with values={} / next=(). The caller must inspect whether snapshot.next is an empty tuple to distinguish "graph has not run yet" from "graph finished".
  • Calling get_state without a checkpointer configured raises ValueError (no checkpointer:1401-1402): No checkpointer set — a pure in-memory graph has no historical snapshots.
  • checkpoint_ns with no matching subgraph raises (subgraph not found:1415-1416): Subgraph {recast} not found. Common when the namespace was assembled with the wrong NS_SEP (|) or NS_END (:).
  • apply_pending_writes is only enabled when no explicit checkpoint_id is given (apply condition:1433). That means historical snapshots show the values that were on disk at the time, uncontaminated by later pending writes; only the "latest" snapshot temporarily applies pending.
  • The next field filters out tasks that already have writes (next filter:1259). Tasks with non-empty t.writes are not counted as "next" — their inputs have already been consumed this step, so next reflects "tasks that have not run yet".
  • tasks includes interrupts (interrupts aggregation:1265). The Interrupt objects from all tasks are flattened into a tuple, and Command(resume=...) backfills them in this order.

Summary

StateSnapshot is the boundary type LangGraph uses to translate the internal checkpoint format into a business-readable view — it stores no data, only assembles the checkpoint plus pending writes with "what's next" semantics into an immutable snapshot. Every breakpoint-resume, time-travel, and human-in-the-loop API is built on top of it.

It pairs with RunnableConfig: the config is the coordinate "how to find this snapshot", and StateSnapshot is "what you see once found". Continue with StateGraph to see how the shape of the state being snapshot is defined, or with the Pregel engine to see how _prepare_state_snapshot gets called from get_state.

See official docs: LangGraph docs · README.