StateSnapshot: the state snapshot at each superstep boundary
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_channelsreads the channel values in between (read_channels:1258). The extraStateSnapshotlayer 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
CheckpointTupleonly stores the values written.StateSnapshotcallsprepare_next_tasksat construction time (prepare_next_tasks:1178-1195) to computenext=(node_name, ...), directly answering "what happens next". - Task list visibility: Each
StateSnapshot.tasksis a tuple ofPregelTaskto execute this step (tasks:658), includingwrites/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_historyreturns the snapshot list in reverse chronological order, and any one can be re-pointed to with itsconfigfor time travel. - Pending writes applied transparently:
get_statetemporarily applies anypending_writesthat have not yet landed on a checkpoint before reading the channels (apply pending writes:1239-1249). So when you callget_stateright 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; thestatefield holds a subgraphStateSnapshotor theRunnableConfiglocating it when recursing._prepare_state_snapshot:1145-1162— Fallback when saved is empty: returns an empty snapshot withvalues={}/next=()/tasks=().step computation + prepare_next_tasks:1167-1195— Derives the current step number fromsaved.metadata["step"]and callsprepare_next_tasksto compute the task dict for this step.task_states recursion:1199-1227— For every task whosetask.nameis insubgraphs, assembles thecheckpoint_nsand recursively calls the subgraph'sget_state.apply pending writes:1229-1255— Temporarily appliesNULL_TASK_IDwrites and regular pending writes to the channels, then computestasks_with_writes.assemble StateSnapshot:1257-1266— Combines channel values, the next-node tuple, config, metadata, ts, parent_config, tasks, and interrupts into the finalStateSnapshot.get_state:1392-1434— Sync entry: resolves the checkpointer, routes bycheckpoint_nsto a subgraph, and decides whether toapply_pending_writes.get_state_history:1480— Iterates all historical checkpoints under the samethread_id, producing aStateSnapshotfor each._aprepare_state_snapshot:1268— Async version; logic mirrors the sync one, exceptchannels_from_checkpointbecomesachannels_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.
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:
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]),
)Boundaries and failure modes
- No checkpoint returns an empty snapshot rather than raising (
empty saved:1152-1162), withvalues={}/next=(). The caller must inspect whethersnapshot.nextis an empty tuple to distinguish "graph has not run yet" from "graph finished". - Calling
get_statewithout a checkpointer configured raisesValueError(no checkpointer:1401-1402):No checkpointer set— a pure in-memory graph has no historical snapshots. checkpoint_nswith no matching subgraph raises (subgraph not found:1415-1416):Subgraph {recast} not found. Common when the namespace was assembled with the wrongNS_SEP(|) orNS_END(:).apply_pending_writesis only enabled when no explicitcheckpoint_idis 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
nextfield filters out tasks that already have writes (next filter:1259). Tasks with non-emptyt.writesare not counted as "next" — their inputs have already been consumed this step, sonextreflects "tasks that have not run yet". tasksincludesinterrupts(interrupts aggregation:1265). TheInterruptobjects from all tasks are flattened into a tuple, andCommand(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.