Expression Engines Overview
Expressions allow workflow designers to reference upstream data and compute values inside node configuration fields. The expression engine resolves these references before the node executor runs — executors always receive concrete values, never raw expression strings.
What Are Expressions?
When a user configures a node in the designer, they can type a static value (e.g. alice@example.com) or an expression (e.g. @{output:form-1.email} or ${{ $output['form-1'].email }}). The expression engine resolves these at execution time against the current execution context.
Where Expressions Appear
- Node configuration fields in Atlas Forms (any field with
supportsExpressions: true) - Condition expressions in If-Condition and Switch nodes
- Loop item collection references in Loop nodes
- Variable assignment values in Variable Assignment nodes
Expression Syntax
Flow Studio uses a directive-based syntax identified by a leading @{ sigil. The expression engine parses and evaluates the directive body:
| Syntax | Resolves |
|---|---|
@{output:{nodeId}.{field}} | Output field from a completed node |
@{input:{field}} | Input data arriving at the current node |
@{var:{name}} | Workflow variable |
@{context:tenantId} | Tenant ID from execution context |
@{context:executionId} | Current execution ID |
@{js: ... } | Arbitrary JavaScript expression (Jint engine) |
Engine Selection
The system uses a single primary expression evaluator: IExpressionEvaluator (default: ExpressionEvaluator backed by Jint). Custom evaluators can be registered for specialized use cases. The config expression resolver (IConfigExpressionResolver) iterates every config field and resolves it using the registered evaluator.
Resolution Lifecycle
- Node reaches the EntryValidate stage in the pipeline
ExpressionContextFactory.Create()builds anEvaluationContextfrom the execution stateIConfigExpressionResolver.ResolveAsync(config, context)resolves all expression fields- The resolved config is stored in
elementContext.ResolvedConfig BaseNodeExecutor.LoadConfigAsync()reads fromResolvedConfigExecuteInternalAsyncreceives fully resolved values via the settings object
ExecuteInternalAsync is called, all expression strings have been replaced with their computed values. Your settings class reads concrete values — it never needs to evaluate expressions itself.