The canvas projects IWorkflowBase. Execution projects IRunExecutionData. Those structures share node names and connections, but they answer different questions: one describes the workflow definition, the other records a specific run. The editor draws the definition, while the engine and stored execution records describe what happened. See Anatomy of an execution
for the data shape behind that split.
Wires imply order, position decides it
The canvas suggests that wire direction alone determines branch order. The engine does not follow that mental model. In useRunWorkflow.ts, sortNodesByYPosition() sorts the start nodes by canvas Y before the payload leaves the editor, and WorkflowExecute.addNodeToBeExecuted() uses the workflow’s execution order setting to decide whether it unshifts or pushes the next node onto the stack. That makes branch order sensitive to the payload, not just to the wires on the screen.
The result is easy to miss in executionOrder: 'v1': moving a node vertically can change which branch runs first even when the wires stay the same. See How the engine decides what runs next
and the official execution order
docs for the wider rule set.
One box, many runs
The canvas shows one node box, but the runtime stores many task records for that node. runData[nodeName] is an array, and each entry captures one run at a specific execution index. getNextExecutionIndex() derives the next index from the stored run data, and moveNodeMetadata() merges per-run metadata back into the matching array slot. The box stays singular; the execution history does not.
The editor can still summarize that history as counts because it reads stored execution data, not the canvas. useRunWorkflow.ts seeds local execution state with createRunExecutionData({ resultData: { runData, pinData } }), and workflows.store.ts later loads saved execution data through fetchExecutionDataById(). For the item level view of the same structure, see Items, runs, and pairedItem
.
One run, many items — usually
Most nodes process a set of items in a single run. The engine does not treat each item as a separate run by default. WorkflowExecute.handleExecuteOnce() narrows executeOnce nodes to the first item, and prepareConnectionInputData() chooses the input shape that the node actually receives. The Code node reference documents the related run-once modes, which change how the node groups or reuses items. See the official Code node reference
.
The important point stays simple: the canvas shows a node, but the runtime decides item granularity. A node can consume many items, only one item, or a shaped subset of the input, depending on the node implementation and its execution mode.
The loop that isn’t
The canvas can suggest a plain loop. SplitInBatches does not behave like a language loop. findStartNodes() treats n8n-nodes-base.splitInBatches specially by checking the last run’s done output, and recreateNodeExecutionStack() rebuilds waiting execution state from stored IRunData and pinned data. That makes the next pass depend on saved execution state, not on a loop construct in the canvas.
This is why SplitInBatches feels stateful across successive runs. The visual pattern points to repetition, but the engine advances by resuming stored state. See Items, runs, and pairedItem
for the item history that makes that resumption possible.
Disabled is not deleted
A disabled node still exists in the workflow definition. The engine only changes how it treats that node. In a full run, WorkflowExecute.handleDisabledNode() passes through the first main input so downstream data can keep flowing. In a partial run, filterDisabledNodes() removes disabled nodes from the graph and reconnects the main connections before findSubgraph() and recreateNodeExecutionStack() rebuild the execution path.
That split matters because the same disabled node can behave like a passthrough in a full run and like a removed vertex in a partial run. See Partial executions and dirty nodes and the official dirty nodes docs.
Pinned nodes never run
Pins look like saved node output, and the main execution loop in processRunExecutionData() substitutes pinData for execution data when a pin exists for that node, skipping runNode() entirely. In that path the node’s code does not run; the editor can still seed local playback with pinData through useRunWorkflow.ts, and workflows.store.ts later reads stored execution data back for display. The official execution types
docs explain why that replay behavior stays separate from production runs.
“Execute this node” is not “execute one node”
A targeted run does more than start one box. consolidateRunDataAndStartNodes() walks backward from the destination, keeps the reusable run data, and marks the nodes that must run again. On the backend, runPartialWorkflow2() then filters disabled nodes, finds the subgraph, cleans the run data, finds start nodes, and recreates the execution stack before execution begins. See Partial executions and dirty nodes
for the dirty chain model behind that behavior.
The practical rule stays narrow: the editor decides which chain needs replay, and the backend rebuilds that chain from stored data. The target node only looks like the whole story.
Branches can die silently
The canvas does not show a special state for a branch that receives no items. The engine simply stops enqueueing downstream work when the input stays empty. incomingConnectionIsEmpty() checks whether a connection has any data, and addNodeToBeExecuted() skips the downstream node when the output array is empty. The branch ends because no data arrives, not because the canvas raises a separate skipped marker.
That silence often looks like a missing wire event from the outside. In reality, the runtime just has nothing to pass forward.
The canvas does not run anything
The editor does not compute node status, item counts, or errors on its own. pushConnection.store.ts receives websocket or event source messages and dispatches them to handlers, and useRunWorkflow.ts turns the returned execution data into local editor state. When the editor needs a finished execution, workflows.store.ts loads the stored execution record and the UI replays that data back onto the canvas.
What appears on the canvas comes from resultData and execution data, not from a second computation path in the editor.
Summary
| Mismatch | Rule of thumb |
|---|---|
| Wires imply order, position decides it | Read branch order from the engine stack and the sorted start-node payload, not from wire layout alone. |
| One box, many runs | Treat runData[nodeName] as a run history, one array entry per run. |
| One run, many items — usually | Expect item arrays, but remember executeOnce and node run-once modes can narrow them. |
| The loop that isn’t | Treat SplitInBatches as stateful handoff across runs, not as a language loop. |
| Disabled is not deleted | Expect full runs to pass data through and partial runs to strip disabled nodes. |
| Pinned nodes never run | Treat pins as editor replay data, not as production input. |
| “Execute this node” is not “execute one node” | Expect the editor and backend to rebuild the dirty chain and reuse stored run data. |
| Branches can die silently | A branch ends when it receives no items; the canvas does not invent a special skipped state. |
| The canvas does not run anything | Treat the editor as a renderer for server execution data, not as the executor. |
Where to look in the code
packages/core/src/execution-engine/workflow-execute.ts— full and partial execution, disabled node passthrough, item shaping, and run data bookkeeping.packages/core/src/execution-engine/partial-execution-utils/filter-disabled-nodes.ts,find-start-nodes.ts,recreate-node-execution-stack.ts,run-data-utils.ts— partial-run graph cleanup, start-node discovery, stack rebuilding, and execution index tracking.packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts— editor-side payload assembly, start-node sorting, and local execution playback.packages/frontend/editor-ui/src/app/stores/workflows.store.ts— run requests and stored execution fetches.packages/frontend/editor-ui/src/app/stores/pushConnection.store.ts— push-message transport for execution updates.packages/core/src/execution-engine/partial-execution-utils/directed-graph.ts— graph rewiring used by partial execution.