This page explains how LangGraph decides which node runs in the next superstep. The Pregel and checkpointers pages cover the scheduler model and checkpoint storage around that decision; this page covers the bookkeeping that turns stored state into the next frontier of work. See 01-anatomy-of-an-invoke.md , 03-your-state-compiles-to-channels.md , and 04-control-flow-is-channels-too.md for the adjacent pieces that feed this decision.

The scheduler does not keep a runtime queue for this decision and it does not walk edges from a live graph to discover work. It reads two persisted maps in the checkpoint, channel_versions[channel_name] and versions_seen[node_name][channel_name], and it treats a node as due when one of its trigger channels is available and the current channel version is newer than the version recorded for that channel in that node’s versions_seen entry.

The checkpoint is the scheduler state

These maps answer one question: did any watched channel move since the node last ran? channel_versions records the current version for each channel, and versions_seen records the last version each node has already consumed.

prepare_next_tasks in libs/langgraph/langgraph/pregel/_algo.py builds the next task set, and _triggers performs the version comparison for each candidate node. apply_writes returns the updated channel set, and prepare_next_tasks uses that set with trigger_to_nodes before comparison starts.

Where the check happens

The loop follows a simple sequence: run, consume, write, bump, compare again. A task runs in the current superstep, apply_writes records that its trigger channels now count as seen, BaseChannel.consume() lets a channel clear or compact state, BaseChannel.update() applies the new writes, and BaseChannel.finish() closes out the frontier when nothing else remains to trigger.

apply_writes in libs/langgraph/langgraph/pregel/_algo.py updates versions_seen first, then it advances channel_versions as channels accept writes. prepare_next_tasks reads the saved checkpoint on the next superstep and asks the same version question again against the new frontier.

Example: two nodes over three supersteps

The tokens in this table are opaque order markers. The scheduler compares them; it does not parse them as timestamps or counters.

SuperstepNode that runschannel_versionsversions_seen[A][start]versions_seen[B][work]
0Astart=α, work=∅, done=∅α
1Bstart=α, work=β, done=∅αβ
2nonestart=α, work=β, done=γαβ

In this graph, A reacts to start and produces work, then B reacts to work and produces done. Each node only shows the trigger channel it watches. After B runs, no trigger channel advances again, so the frontier goes empty and the run stops.

Why versions exist instead of a dirty flag

Versions make replay and crash recovery deterministic. The next frontier comes back from the checkpoint, so no in-memory queue needs to survive a crash or a restart. A node that runs without its trigger channel advancing does not run again, because versions_seen catches up to the current channel version and the comparison turns false on the next pass.

Failure modes

recursion_limit works as a superstep budget, not as call stack depth. When the loop spends that budget, LangGraph raises GraphRecursionError; the user-facing troubleshooting page for GRAPH_RECURSION_LIMIT covers the error message and the usual response.

The other stop condition is quiescence. When the frontier goes empty, prepare_next_tasks returns nothing, tick() marks the run done, and END in libs/langgraph/langgraph/constants.py names that terminal path. END does not introduce a separate scheduler rule; it just names the stop that already happens when nothing remains due.

Version flow

The same two maps drive every pass through the loop: planning compares them, execution produces writes, and apply_writes moves both maps forward for the next comparison. See 05-why-checkpoints-look-like-that.md and 06-replay-resume-and-idempotency.md for the checkpoint shape and replay contract behind that flow.

flowchart LR
  CV[channel versions] --> Plan[prepare next tasks]
  VS[versions seen] --> Plan
  Plan --> Run[execute due nodes]
  Run --> Writes[writes]
  Writes --> Apply[apply writes]
  Apply --> Bump[channel versions advance]
  Apply --> Seen[versions seen catch up]
  Bump --> Plan
  Seen --> Plan

Where to look in the code

  • libs/langgraph/langgraph/pregel/_algo.pyprepare_next_tasks, _triggers, apply_writes
  • libs/langgraph/langgraph/pregel/_loop.pytick, after_tick, _put_checkpoint
  • libs/checkpoint/langgraph/checkpoint/base/__init__.pyCheckpoint, channel_versions, versions_seen, BaseCheckpointSaver.get_next_version
  • libs/langgraph/langgraph/channels/base.pyconsume, update, finish
  • libs/langgraph/langgraph/pregel/_checkpoint.pycreate_checkpoint, channels_from_checkpoint