5.7 KiB
Expression Engine for Workflow Templates
Date: 2026-02-28
Scope: crates/common/src/workflow/expression/, crates/executor/src/workflow/context.rs
Summary
Implemented a complete expression evaluation engine for workflow templates, replacing the previous simple dot-path variable lookup and string-based condition evaluation with a full recursive-descent parser and evaluator operating over JSON values.
Motivation
Workflows previously had no way to perform calculations, comparisons, or transformations within template expressions ({{ }}). The evaluate_condition() method could only check if a rendered string was truthy — it couldn't evaluate expressions like result().code == 200 and succeeded() or length(items) > 3. Publish directives and task inputs had no way to compute derived values.
Architecture
The engine follows a classic three-phase interpreter design, located in crates/common/src/workflow/expression/:
- Lexer (
tokenizer.rs) — converts expression strings into tokens - Parser (
parser.rs) — recursive-descent parser producing an AST - Evaluator (
evaluator.rs) — walks the AST against anEvalContexttrait to produceJsonValueresults
The EvalContext trait decouples the expression engine from the WorkflowContext, allowing it to be used in other contexts (e.g., rule condition evaluation, template resolver) in the future.
Operator Precedence (lowest to highest)
orandnot(unary)==,!=,<,>,<=,>=,in+,-*,/,%- Unary
- - Postfix:
.field,[index],(args)
Supported Operators
Arithmetic
+— number addition, string concatenation, array concatenation-,*,/,%— standard numeric operations- Unary
-— negation - Integer division returns float when not evenly divisible (e.g.,
10 / 4→2.5,10 / 5→2)
Comparison
==,!=— deep equality (recursive for objects/arrays, cross-type for int/float)>,<,>=,<=— ordering for numbers, strings, and lists- No implicit type coercion:
"3" == 3→false
Boolean
and,or— short-circuit evaluationnot— logical negation
Membership & Access
.— object property access[n]— array index (supports negative indexing), object bracket access, string character accessin— membership test (item in array, key in object, substring in string)
Built-in Functions
Type Conversion
string(v), number(v), int(v), bool(v)
Introspection
type_of(v), length(v), keys(obj), values(obj)
Math
abs(n), floor(n), ceil(n), round(n), min(a,b), max(a,b), sum(arr)
String
lower(s), upper(s), trim(s), split(s, sep), join(arr, sep), replace(s, old, new), starts_with(s, prefix), ends_with(s, suffix), match(pattern, s) (regex)
Collection
contains(haystack, needle), reversed(v) (arrays and strings), sort(arr), unique(arr), flat(arr), zip(a, b), range(n) / range(start, end), slice(v, start, end), index_of(haystack, needle), count(haystack, needle), merge(obj_a, obj_b), chunks(arr, size)
Workflow-specific (via EvalContext)
result(), succeeded(), failed(), timed_out()
Design Decisions
- No implicit type coercion: Arithmetic between strings and numbers is an error. Only int/float cross-comparison is allowed (e.g.,
3 == 3.0→true). - Python-like truthiness:
null,false,0,"",[],{}are falsy; everything else is truthy. - Integer preservation: Operations on two integers produce integers; operations involving any float produce floats. Integer division that isn't evenly divisible promotes to float.
- Array literals: Supported in expressions:
[1, 2, 3](with optional trailing comma). - Negative array indexing:
arr[-1]returns the last element.
Integration with WorkflowContext
WorkflowContextimplements theEvalContexttrait, bridging variable resolution (parameters,item,index,system,task,variables, and direct variable names) and workflow functions (result(),succeeded(),failed(),timed_out())evaluate_expression()now delegates entirely to the expression engineevaluate_condition()tries the expression engine first (for bare expressions likex > 5), with fallback to template rendering for backward compatibility with{{ }}wrapper syntax- All existing tests pass without modification — backward compatibility is fully preserved
Test Coverage
- 63 tests in
crates/common/src/workflow/expression/covering tokenizer, parser, and evaluator - 28 tests in
crates/executor/src/workflow/context.rs(17 existing + 11 new) covering integration with WorkflowContext - New tests cover: condition comparisons, boolean operators,
inoperator, function calls in conditions,length()in conditions, arithmetic in templates, string concatenation, nested function calls, bracket access, and type conversion
Files Changed
- Created:
crates/common/src/workflow/expression/mod.rs— module entry point and integration tests - Created:
crates/common/src/workflow/expression/ast.rs— AST node types - Created:
crates/common/src/workflow/expression/tokenizer.rs— lexer - Created:
crates/common/src/workflow/expression/parser.rs— recursive-descent parser - Created:
crates/common/src/workflow/expression/evaluator.rs— AST evaluator with all built-in functions - Modified:
crates/common/src/workflow/mod.rs— addedexpressionmodule - Modified:
crates/executor/src/workflow/context.rs— integrated expression engine, replaced old evaluate_expression/evaluate_condition, implemented EvalContext trait, removed unused get_nested_value, added new tests