Output Branches
A branch-aware copy of Output Data that exists for exactly one reason: so {{node-X.output}} expressions can resolve later in the workflow. It is built twice, independently, by two different pieces of code.
The Shape
INodeResultBranches
├── Success : IReadOnlyList<INodeDataItem>
├── Error : IReadOnlyList<INodeDataItem>?
└── Conditional : IReadOnlyDictionary<string, IReadOnlyList<INodeDataItem>>?
INodeDataItem
├── Data : IReadOnlyDictionary<string, object?> ← a COPY of OutputData's top-level fields
├── SourceItemIndex : int
└── SourceBranchIndex : int
Build Site #1 — the Auto-Fill
Right after an executor's ExecuteInternalAsync returns, if it didn't set OutputBranches itself, the base class builds one automatically so expression resolution always has something to read:
// BaseNodeExecutor.Process.cs:65-73
if (ResultInfo.Result.OutputBranches is null
&& ResultInfo.Result.OutputData is { Count: > 0 })
{
var data = ResultInfo.Result.OutputData
.ToDictionary(k => k.Key, v => (object?)v.Value);
ResultInfo.Result.OutputBranches = new NodeResultBranchesBuilder()
.AddSuccess(data)
.Build();
}
.ToDictionary(...) is a genuine shallow copy — a new outer Dictionary instance, though any nested reference-type values (lists, nested dictionaries) are still shared with the original OutputData.
Build Site #2 — the Sequencer's Own Copy
Separately, after the node fully returns, the orchestration sequencer builds a second, independent NodeResultBranches and stores it keyed by node key — this is the one actually consulted for {{node-X.output.field}} template resolution:
// ProcessElementExecutionSequencer.cs:312-316
// Also store for expression resolution ({{node-X.output.Y}} templates)
var branches = new NodeResultBranchesBuilder()
.AddSuccess(nodeResult.OutputData.ToDictionary(k => k.Key, v => (object?)v.Value))
.Build();
memory.StoreNodeResultBranches(currentElement.NodeKey, branches);
NodeExecutionResult object itself (useful for anything holding that specific result, e.g. the pinned-data short-circuit path). Build #2 is stored in ExecutionMemory keyed by node key, so any later node in the workflow can resolve {{someEarlierNode.output.field}} — not just whatever code happens to be holding that one result object. They're redundant with each other but serve different lookup paths. Neither shares an object reference with the original OutputData dictionary itself (both do a real .ToDictionary(...)), so this redundancy is not, by itself, a correctness bug — just duplicated memory and duplicated JSON if either gets serialized wholesale.
How to Read {{node-X.output}} Cleanly
NodeDataContextAdapter (Services/Expressions/Adapters/NodeDataContextAdapter.cs) is the clean, typed way expressions reach this data — it wraps memory.GetNodeResultBranches(nodeId) behind INodeResultData, exposing CurrentItem / Items / Branches. Prefer resolving a specific field off CurrentItem.Data rather than the whole Branches object — reading the whole object and re-embedding it somewhere is exactly the pattern that caused the pinned-data bug (see Pinned Data).