Skip to main content
The graph executor turns a stored run into useful work. Your graph still makes the decisions. The runtime records what happened and runs each node safely.

How a dispatch runs

  1. The executor loads the run definition and frozen plan from storage.
  2. It checks that the supplied graph matches the stored definition.
  3. It folds the run’s graph: events into graph state.
  4. The graph returns its next command.
  5. The executor records the whole dispatch decision before any node starts.
  6. It records node-attempt-started immediately before each node starts.
  7. It runs each node through the safe node-attempt lifecycle.
  8. It records node-completed, node-paused, or node-failed, then asks the graph again.
Node behaviour is keyed by node id. A data-only node gets a data function. An engine-backed node gets a prompt builder. The builder receives that dispatch’s typed input, so two positions can produce different prompts. The node then uses the lane in the stored plan. Engine bindings join that exact plan target to the full engine identity, including its adapter version and executable path when present. The host chooses the order once. The executor tries the effective target and at most one live fallback from that order. Only a failure that means the lane is dead, such as bad credentials or a missing model, can move to the fallback. A rate limit, timeout, policy pause, or denied action does not run again elsewhere. Each dead-lane event records both the selected identity and the identity the engine reported. Stored facts and later routing use the same provider and model key. Later dispatches skip either identity. If every declared target is dead, no engine runs. The executor records ENGINE_UNAVAILABLE as a normal node failure so the graph can retry, fail, or recover in its usual way. An action-policy wait records node-paused with its reason and request. A denied action records node-failed with DENIED. An aborted attempt records node-failed with ABORTED. These events stay distinct when the graph folds the stream. If a result or pause request is too large for the event stream, the executor records a small node-failed event with RESULT_TOO_LARGE so the dispatch does not stay in flight. If a process stops after node-attempt-started and the start record has retrySafe: false, resume does not run node code. It records node-paused with this request shape:
attemptId is the attempt identity from that start record. The pause reason is that the previous process stopped after node code started, so the outcome is uncertain. An empty decision is valid only when an attempt is already in flight. The executor rejects a second dispatch record for the same position.

Resume after a process stops

A fresh executor returns waiting when a dispatch has no result. Call resume with one exact position from that result:
Call resume only after the earlier executor process stops. The executor does not lock the run between processes. Resume rebuilds the dispatch input from its event prefix and the graph decision. It does not add a second dispatch event.
  • Node code did not start. If no start record exists, the executor runs the attempt at its recorded position. It does not write node-resumed.
  • A retry is safe. Set retrySafe: true in the node binding. The start record saves this rule before node code starts. Resume records node-resumed for that in-flight position, then runs the same attempt again.
  • The result is not known. If the saved rule is false, resume records a typed pause with a reconcile-attempt request and does not call node code.
  • The node is paused. Resume records node-resumed for the exact position and offers the same attempt again.
node-resumed is recorded for a paused node and for an in-flight attempt that already has a start record with retrySafe: true. The saved retrySafe value controls crash recovery. A changed value in the live binding does not change an earlier start record.

Two nodes in flight

A graph can dispatch more than one node at a time. If the process stops with two dispatches still open, a fresh executor returns waiting with both positions. resume takes one position. After that attempt settles, if the sibling is still unfinished, the result is waiting again with the sibling’s position:
At this stage, the executor checks the stored graph definition and plan. It does not check general code or node-body fingerprints. If the graph declares memory as required, createGraphExecutor also requires a live Memory object. The executor passes that same object into the node attempt, and a data-only node receives it in its context. The runtime check covers TypeScript and plain JavaScript callers. A data-only node can call another executor and return the child result. This is how parent and child graphs compose without adding a second execution system.

Turn-taking example

The outside example alternates a writer and critic for three rounds. Each critic prompt includes that dispatch’s position summary. The example stores the run, starts the public executor, and uses a real dead-primary path through two MockEngine instances. The first critic attempt records the primary as dead. The next two critic attempts go straight to the fallback. The critic permits a crash retry.
The command reports six dispatches, one primary call, three fallback calls, and the final result. The same source compiles and runs from the packed package with TypeScript 6 and TypeScript 7.