Portal Community

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);
MemberWhat it does
CurrentScopeThe top of the stack — throws if the stack is ever empty (should never happen in normal execution).
ScopeDepth0 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.
LoopStackA 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 variables vs. branch storage are two different systems
  • Flat scope variables (SetVariable/GetVariable/GetAllVisibleVariables) — one entry per field name, written by ProcessElementExecutionSequencer.cs:299-302 from each node's OutputData. This is what InputData is built from.
  • Node branch storage (StoreNodeResultBranches/GetNodeResultBranches) — one entry per node key, holding a full NodeResultBranches object. 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

1

EnterNodeScope(nodeKey, nodeName, inputData)

Pushes a new scope chained to whatever was current, optionally pre-loaded with input parameters.

2

Node executes

Reads via InputData (already resolved before entry), writes its own OutputData.

3

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).

4

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.