Execution Memory & Scopes
The mechanism that turns one node's Output Data into the next node's Input Data — a stack of scopes, not a single flat bag.
The Scope Stack
ExecutionMemory holds a Stack<ExecutionScope>. The bottom of the stack is always the global scope (created once per workflow execution, optionally pre-loaded with thread input data). Every node execution pushes its own scope on entry and pops it on exit:
// ExecutionMemory.cs:275-297 (EnterNodeScope)
var nodeScope = new ExecutionScope(
scopeId: $"node-{nodeKey}",
type: VariableScopeType.Node,
parentScope: CurrentScope, // chains to whatever scope was active
description: $"Node execution scope: {nodeName}",
nodeKey: nodeKey
);
_scopeStack.Push(nodeScope);
| Member | What it does |
|---|---|
CurrentScope | The top of the stack — throws if the stack is ever empty (should never happen in normal execution). |
ScopeDepth | 0 at global scope only, 1+ once nodes/loops have pushed scopes. |
GetVariable(key) / SetVariable(key, value) | Read/write on CurrentScope — a null value passed to SetVariable removes the variable rather than storing null. |
SetLocalVariable(key, value) | Same as SetVariable but throws on a null value, and is explicitly for shadowing a parent-scope variable within just this scope. |
GetAllVisibleVariables() | Walks the scope chain from CurrentScope up through every parent, merging into one dictionary — this is exactly what becomes the next node's InputData. |
LoopStack | A separate stack tracking (LoopNodeKey, IterationCount) — consulted for loop-iteration metadata in execution history, independent of the variable scope stack. |
Why a Stack, Not a Flat Dictionary?
Scoping lets nested execution (loops, sub-workflows) shadow variables locally without corrupting the parent's view. A variable set inside a loop iteration's scope is visible to nodes inside that iteration, but disappears once the iteration's scope is popped — the parent scope's own copy of that variable name (if any) is unaffected.
Two Separate Storage Mechanisms — Don't Confuse Them
- Flat scope variables (
SetVariable/GetVariable/GetAllVisibleVariables) — one entry per field name, written byProcessElementExecutionSequencer.cs:299-302from each node's OutputData. This is what InputData is built from. - Node branch storage (
StoreNodeResultBranches/GetNodeResultBranches) — one entry per node key, holding a fullNodeResultBranchesobject. This exists purely so{{node-X.output.field}}expressions can look up a specific earlier node's result by name — it does not feed into InputData at all.
Node Scope Lifecycle, in Order
EnterNodeScope(nodeKey, nodeName, inputData)
Pushes a new scope chained to whatever was current, optionally pre-loaded with input parameters.
Node executes
Reads via InputData (already resolved before entry), writes its own OutputData.
ExitNodeScope()
Pops the node's scope and returns the variables it exported — you cannot exit past the global scope (throws if only 1 scope remains).
Sequencer writes fields to the (now-restored) parent scope
After ExitNodeScope(), CurrentScope is the parent again — each OutputData field gets written there via SetVariable, becoming visible to sibling/downstream nodes.