Data Bags
A data bag is a named container of key-value pairs that exists during workflow execution. Understanding which bags exist, when they are populated, and who can read or write them is fundamental to building correct node executors and expressions.
The Bag Hierarchy
Data bags exist at two scopes. Thread-scope bags live for the entire thread run and are accessible across all nodes. Element-scope bags are created for a single node's execution and discarded when that node completes.
Thread-Scope Bags
All thread-scope bags live on ExecutionMemory — the central object that
travels with the thread for its entire lifetime and is serialized when the thread is
durably suspended.
InputData
| Property | Value |
|---|---|
| Type | IReadOnlyDictionary<string, object> |
| Scope | Thread lifetime |
| Mutable | No |
| Expression | @{input:key} |
The original trigger data that started the workflow. Set once when the thread begins and never changed. Contains whatever the triggering event provided: form submission fields, webhook payload, API request body, or scheduled trigger parameters.
At startup, InputData is also loaded into the global variable scope, so
values are accessible via @{var:key} in addition to @{input:key}.
Variables
| Property | Value |
|---|---|
| Type | Scoped via VariableScope chain |
| Scope | Thread lifetime (global) or current block (loop/function) |
| Mutable | Yes |
| Expression | @{var:key} |
Variables are the workflow's writable memory. A scope stack implements a chain-of-responsibility lookup:
- Global scope — always present. Variables set here persist for the entire thread.
- Loop scope — created when entering a loop node. Variables are local to the iteration and do not pollute the parent scope.
- Function scope — created when entering a sub-workflow node.
Reading a variable walks up the scope chain until it finds the key, or returns null. Two write methods have different semantics:
SetVariable(key, value)— writes to the current scope, but propagates to the parent scope if the key already exists there.SetLocalVariable(key, value)— forces the write into the current scope only, never propagating to parent scopes.
When a loop or function scope exits, all variables local to that scope are discarded.
Variables in parent scopes that were modified via SetVariable retain their values.
NodeOutputs
| Property | Value |
|---|---|
| Type | Dictionary<string, object> |
| Scope | Thread lifetime |
| Mutable | Yes — each node appends its entry |
| Expression | @{output:nodeKey.field} |
Every time a node completes, its output data is stored here keyed by
ProcessElementKey. This is what makes @{output:nodeKey.field}
work — any downstream node can reference any previously executed node's output by its key.
The key is the unique identifier assigned in the workflow editor (visible in the
node's property panel).
NodeResultBranches
| Property | Value |
|---|---|
| Type | Dictionary<string, INodeResultBranches> |
| Scope | Thread lifetime |
| Mutable | Yes — only stored when executor sets result.OutputBranches |
A typed companion to NodeOutputs for nodes that produce structured branch
outputs — for example, an HTTP request node that returns different typed shapes on
success versus error. Most nodes do not use this bag.
Cache
| Property | Value |
|---|---|
| Type | Dictionary<string, object> |
| Scope | Thread lifetime — but NOT serialized on pause/resume |
| Mutable | Yes |
Transient scratch space. Use for values that are expensive to recompute but do not need
to survive a pause. The cache is cleared when the thread suspends. If a value must be
available after resumption, store it in Variables or NodeOutputs
instead.
Element-Scope Bags
Element-scope bags live on ProcessElementExecutionContext and exist only
for a single node's execution. They are created by the element layer before dispatch
and discarded after the node completes (with the exception of OutputData,
which is promoted to the thread-scope NodeOutputs).
ResolvedConfig
| Property | Value |
|---|---|
| Type | IReadOnlyDictionary<string, object> |
| When available | After Tier 1 config resolution completes |
| Mutable | Yes — Tier 2 writes resolved expression values back into it |
The result of the 3-layer config merge (Extension defaults → Connector config →
ProcessElement config), with all applicable expression fields resolved. By the time
OnEntryValidate fires in the executor, this bag contains final, usable
values. Executor settings classes read from it via
LoadAndValidateConfigAsync<TSettings>, which deserializes the
dictionary into a typed settings object.
InputData (element-level)
| Property | Value |
|---|---|
| Type | Dictionary<string, object> |
| When available | Populated by the orchestrator before element dispatch |
| Mutable | Yes — the node can read and write it |
| Expression | @{input:key} in Tier 2 expressions |
The node's input bag — data from upstream nodes mapped into this node's input schema.
This is what @{input:key} directives in Tier 2 expressions resolve against.
It is separate from the thread-level ExecutionMemory.InputData, which holds
the original trigger data.
ExecutionMemory.InputData) holds the original trigger payload and is
immutable. The element-level bag (ProcessElementExecutionContext.InputData)
holds outputs from upstream nodes mapped into this node's input, and is mutable.
@{input:key} in Tier 2 expressions resolves against the element-level bag.
OutputData (element-level)
| Property | Value |
|---|---|
| Type | Dictionary<string, object> |
| When available | After the executor completes |
| Promoted to | ExecutionMemory.NodeOutputs[elementKey] |
The node's output. After the executor returns, this dictionary is stored in the thread's
NodeOutputs keyed by the element key, making it available to downstream
nodes via @{output:elementKey.field}.
LocalMemory
| Property | Value |
|---|---|
| Type | Dictionary<string, object> |
| Scope | Single node execution only — not visible to other nodes |
| Mutable | Yes |
Private scratch space for a node's internal state. Used to pass values between lifecycle
stages within the same execution — for example, a resource handle opened in
OnEntry and released in OnExit.
HIL Bags
PreHilData and Hil are specialized bags used by the HIL
(Human-in-the-Loop) suspension flow:
PreHilData— snapshot of the data as it was before the human reviewed it. Preserved for audit purposes.Hil— the data the human provided during the HIL interaction. Merged into the workflow on resume.
For the full HIL suspension flow, see HIL Labels.
Quick Reference: Which Expression to Use
| What you want | Expression | Resolves from | When |
|---|---|---|---|
| Original trigger data | @{input:key} | Thread-level InputData | Tier 1 or Tier 2 |
| Upstream node output | @{output:nodeKey.field} | NodeOutputs | Tier 2 only |
| Workflow variable | @{var:key} | Variable scope chain | Tier 2 only |
| Environment variable | @{env:KEY} | Process environment | Tier 1 only |
| Vault secret | @{secret:Key} | Secret store | Tier 1 only |
| Execution context | @{context:tenantId} | Execution context | Tier 1 or Tier 2 |