Skip to content

Graph Runner

The Graph Runner is the execution engine. You hand it a graph and an initial workflow state, and it runs the graph node by node, merging each result back into state through the reducers and routing along the edges until it reaches an end node. Everything else in the orchestrator is either an input to a run agents, tools, memory or a place a run writes to, such as persistence, event log, streams.

Each run is a loop over nodes. The runner executes the start node, drives it to an action, and dispatches that action through the reducers to produce the next state. It then evaluates the current node’s outgoing edges against that new state to pick the next node, and repeats. The loop ends when it reaches a declared end node, exhausts its maximum iterations, or a failure halts it.

State is the only thing that moves between nodes, so the runner can persist a complete snapshot after every step. And every state transition is recorded in the event log, so a crashed run can be rebuilt by replaying those actions through the same reducers. The durable execution section covers both.

A run is single-writer: one runner owns one run at a time. In production you don’t construct runners by hand for each request. A workflow worker pulls jobs from a queue and runs each one for its whole lifetime, with run fencing to keep two workers off the same run.

Two entry points execute a run. Both drive the same loop; they differ only in what you observe.

Executes to completion and resolves with the final state. Use it for fire-and-forget execution and worker processes.

const finalState = await runner.run();

Yields a stream event at each step: token deltas, node transitions, memory diffs, and a terminal event carrying the full final state.

for await (const event of runner.stream()) {
if (event.type === 'agent:token_delta') process.stdout.write(event.token);
if (event.type === 'workflow:complete') console.log(event.state.status);
}

See Streaming for the full event catalogue and SSE forwarding. GraphRunner also extends EventEmitter, so non-streaming consumers can attach listeners for the same events.

Two methods stop an in-flight run, and they mean different things.

Aborts immediately. It signals the shared abort controller to cancel any in-flight LLM call, then transitions the run to cancelled. Use it when the result is no longer wanted.

runner.cancel();

Stops gracefully. The current node finishes, its state is persisted, and the run pauses in a resumable state, emitting a workflow:paused event. Use it for deploys and scaling down, so a long run can resume later from its last checkpoint rather than restart.

const resultPromise = runner.run();
runner.shutdown();
const pausedState = await resultPromise;

Wire an EventLogWriter and the runner records every action as it runs, so a crashed run can be rebuilt exactly.

const runner = new GraphRunner(graph, state, {
eventLog: myEventLog,
persistState: async (s) => persistence.saveWorkflowSnapshot(s),
});

The GraphRunner.recover rebuilds a ready-to-continue runner from a run’s event log. It loads the latest checkpoint, replays only the events after it through the same reducers, and returns a runner you can call .run() on to continue. Replay makes no LLM calls: the stored Action objects already hold every agent output, so replay is deterministic and reconstructs identical state, including approval deadlines.

const runner = await GraphRunner.recover(graph, runId, eventLog, {
persistState: async (s) => persistence.saveWorkflowSnapshot(s),
});
const finalState = await runner.run(); // continues from where it left off

compactEvents() checkpoints the current state and deletes the events behind it, returning the number removed. This keeps a long run’s log bounded. It also runs automatically every compactionInterval events (default 1000) when an event log is wired. See Persistence for the recovery and compaction flow, and Error Handling for the corruption guarantees.

The runner writes state through callbacks rather than a storage object, so it stays free of any database dependency. Pass persistState to persist a full snapshot after every step, and optionally persistDelta to send compact patches for the steps in between. See Persistence for the wiring, failure escalation, the event-log write barrier, and differential persistence.

Create a runner for one run. It resumes from a checkpoint when state.visited_nodes is non-empty, so the same constructor starts a fresh run or continues a recovered one.

GraphRunner(graph: Graph, initialState: WorkflowState, options?: GraphRunnerOptions)

The options are GraphRunnerOptions. The Configuration Reference carries the full table with types and defaults.

Execute the graph to completion and resolve with the final state. Consumes stream() internally and preserves the original error types.

run(): Promise<WorkflowState>

Execute the graph, yielding a StreamEvent at each step. Pass an AbortSignal to cancel mid-run. The terminal workflow:complete event carries the full final state.

stream(options?: { signal?: AbortSignal }): AsyncGenerator<StreamEvent>

Static. Rebuild a ready-to-continue runner from a run’s event log by deterministic replay, making no LLM calls. Throws if no events exist for the run.

GraphRunner.recover(
graph: Graph,
runId: string,
eventLog: EventLogWriter,
options?: Omit<GraphRunnerOptions, 'eventLog'>,
): Promise<GraphRunner>

Checkpoint the current state, delete the events at or before it, and return the number removed.

compactEvents(): Promise<number>

Abort the run immediately: cancel in-flight LLM calls and transition to cancelled.

cancel(): void

Request graceful shutdown. The current node finishes, state is persisted, and the run pauses in a resumable state, emitting workflow:paused.

shutdown(): void

Return a read-only view of the current state. Useful for inspecting a runner before or after a run, such as reconciling a recovered runner against the latest snapshot.

getState(): Readonly<WorkflowState>

Constructor options. Every field is optional; the defaults give an in-memory, single-process run with no persistence. Types and defaults for each field are in the Configuration Reference.

Field Purpose
persistState Persist a full state snapshot after each step. Wire to a PersistenceProvider.
persistDelta Persist compact StatePatch diffs between full snapshots.
deltaTrackerOptions Tune the delta tracker: fullSnapshotInterval, maxPatchBytes.
eventLog EventLogWriter for durable, replayable execution. Defaults to an in-memory no-op.
compactionInterval Events between automatic event-log compactions. Default 1000; 0 disables.
loadGraph Load subgraph definitions by ID for subgraph nodes.
tools Provide tools. See Tools & MCP.
modelResolver Budget-aware model selection for agents with a model_preference.
contextCompressor Compress memory before prompt injection (Context Engine).
memoryRetriever Inject facts from the memory graph for nodes declaring a memory_query.
memoryWriter Persist facts from reflection nodes. Required when the graph has one.
factSanitizer / factSanitizerFailMode Pre-write hook on reflection facts, and its fail-closed behavior.
fitnessFunction Deterministic fitness evaluator for evolution nodes.
rateLimiter Awaited before every LLM call to pace a run inside a provider budget.
securityPolicy Taint-aware policy consulted before each node runs.
middleware beforeNodeExecute / afterReduce hooks (Middleware).
onToken Callback for each token delta from agent nodes.
autoRollback Run saga compensations on failure, transitioning to cancelled instead of failed.
allowImplicitCompletion Restore the legacy behavior of completing silently at a dead-end node instead of failing with NoMatchingEdgeError.