Evaluation Pipeline
Expression evaluation happens in two distinct tiers, at two different points during element execution. Understanding why two tiers exist — and exactly when each runs — is essential for correctly declaring field policies and for knowing which directives are available at each point.
Why Two Tiers?
Some expressions can be resolved as soon as the node is about to execute, before the upstream outputs are known. Environment variables and secrets fall into this category — they are static configuration data that does not depend on what any upstream node produced.
Other expressions — like @{input:employeeId} or
@{output:fetchUser.email} — cannot be resolved until the orchestrator
has assembled the input bag from upstream outputs. Trying to resolve them before
that point would always produce an empty result.
The two-tier design resolves each field exactly when its source data is available, avoiding both premature resolution (empty results) and deferred resolution (unnecessary overhead).
The Full Pipeline
Tier 1 — AtConfigLoad
Tier 1 runs during PrepareExecutionAsync, immediately after the
3-layer config merge completes. Only fields whose NodeFieldDescriptor
has EvaluationStage == AtConfigLoad are processed here.
What is available at Tier 1
| Directive | Available | Notes |
|---|---|---|
@{env:KEY} | Yes | Process environment variables — always available |
@{secret:Key} | Yes | Vault secrets — resolved via async secret store |
@{context:field} | Yes | Execution context (tenantId, processId, threadId) |
@{input:key} | Partial | Thread-level InputData (trigger payload) is available; element-level InputData is not yet populated |
@{output:nodeKey.field} | No | NodeOutputs are not yet available at Tier 1 |
@{var:key} | No | Variable scope is not yet wired to element context |
Tier 1 is synchronous with config loading. Its results are written back into the
merged config dictionary in-place. If no fields have AtConfigLoad stage,
the expression pipeline is skipped entirely for Tier 1 with zero overhead.
Tier 2 — AtInputReady
Tier 2 runs after the orchestrator has populated the element's InputData
bag with mapped outputs from upstream nodes. Only fields with
EvaluationStage == AtInputReady are processed here.
What is available at Tier 2
| Directive | Available | Notes |
|---|---|---|
@{env:KEY} | Yes | Also available in Tier 1, so typically declared AtConfigLoad instead |
@{secret:Key} | Yes | Also available in Tier 1, so typically declared AtConfigLoad instead |
@{context:field} | Yes | Same as Tier 1 |
@{input:key} | Yes | Element-level InputData — upstream outputs mapped into this node's input |
@{output:nodeKey.field} | Yes | Full NodeOutputs from all previously completed nodes |
@{var:key} | Yes | Variable scope chain (global + any current loop/function scope) |
Tier 2 results are also written back into the merged config dictionary. Like Tier 1,
if no fields require AtInputReady evaluation, the entire tier is skipped.
EvaluationStage Enum
| Value | When it runs | Use for |
|---|---|---|
AtConfigLoad | After 3-layer merge, before input mapping | Static configuration: env vars, secrets, execution context |
AtInputReady | After input bag is populated from upstream outputs | Runtime data: input values, upstream outputs, variables |
Never | — | Field is never expression-evaluated (raw string only) |
What Happens When No Manifest Exists
If a node executor does not implement INodeFieldManifestSource, all
fields default to ExpressionPolicy.Default:
EvaluationStage = AtConfigLoadEvaluatorKind = Template
This means only @{env:}, @{secret:}, and
@{context:} directives will resolve. @{input:},
@{output:}, and @{var:} directives in fields without
an AtInputReady declaration will not resolve and will remain as
literal text in the config.
Executors that need runtime data in their config fields must declare
a field manifest with AtInputReady stage for those fields.
Error Handling
Expression evaluation errors are handled at the field level. A single field failing to resolve does not abort the entire pipeline. The behavior depends on the field's policy:
- Required fields — if resolution fails and the field is marked required, a validation error is raised and the executor is not dispatched. The element returns a failed result.
- Optional fields — if resolution fails, the field is set to null. Execution continues. The executor must handle null values for optional fields.
- JavaScript evaluator errors — if a Jint script throws, the error is caught, logged, and the field is set to null (unless required, in which case it fails).
AtInputReady fields (for example, a simple
utility node that only uses @{env:}), Tier 2 is entirely skipped without
scanning the config dictionary.