Expressions (expr)
Expressions live inside {{ ... }} in any string-typed activity input. The
runner evaluates them with expr-lang, which provides a
full expression language — not just variable substitution. Most list
filtering, dedup, pattern matching, and string munging that authors reach
for RunPython for can be done as a single expression.
Syntax shapes
WriteLine(response) # bare variable — passes reference
WriteLine("{{ response.body }}") # property access
WriteLine("{{ items | filter(.active) }}") # full expression
WriteLine("Hi {{ user.name }}, {{ count }} items") # embedded in a string
Rule of thumb: if you're accessing a property, calling a function, doing
arithmetic, or piping — wrap it in {{ }}. Bare names without braces only
work for single-variable references.
Collection operations
| Expression | Result |
|---|---|
filter(items, .active) | items where predicate is true |
map(items, .name) | list of field values |
sortBy(items, .priority) | sorted ascending |
sortBy(items, .priority, "desc") | sorted descending |
groupBy(items, .status) | map[key] → []item |
find(items, .id == 42) | first match or nil |
any(items, .total > 100) | true if any element matches |
all(items, .priority > 0) | true if every element matches |
count(items, .active) | count of matches |
take(items, 5) | first N elements |
reduce(items, #acc + .amount, 0) | fold |
reverse(items) | reversed copy |
Inside predicates, .fieldName is the current element; # is the element
itself (useful for scalars — filter(codes, # startsWith "A")).
Pipes
items | filter(.active) | sortBy(.priority) | map(.name) | take(3)
items | sortBy(.updated_at) | last()
names | filter(# endsWith ".pdf") | strings.Join(",")
Pipe chains read left-to-right; each stage's output feeds the next.
Operators
| Operator | Example | Notes |
|---|---|---|
in | "abc" in names | membership |
contains | s contains "foo" | substring |
startsWith / endsWith | path endsWith ".pdf" | |
matches | s matches "[a-z]+" | regex |
?? | x ?? "default" | nil-coalesce |
?. | user?.email | safe navigation |
.. | 1..5 | range → array |
Data / type helpers
| Expression | Result |
|---|---|
len(items) | length |
items[0], items[-1] | indexing, negative from end |
items[1:3] | slicing |
keys(obj) / values(obj) | object keys/values |
toPairs(obj) | [[key, value], ...] |
fromPairs([[k, v], ...]) | pairs → object |
fromJSON(str) / toJSON(value) | JSON parse / stringify |
date("2026-04-01") | time.Time, sortable |
now() | current time.Time |
{"a": 1, "b": 2} | object literal (top-level only) |
let x = expr; body | local binding |
Registered function packages
strings.* (38 functions)
ToLower, ToUpper, Contains, HasPrefix, HasSuffix, Split, Join,
Replace, ReplaceAll, Trim, TrimPrefix, TrimSuffix, Repeat,
Count, Fields, Cut, CutPrefix, CutSuffix, EqualFold, and more.
Match Go's strings package naming.
Example: {{ strings.Join(map(codes, # + ".pdf"), ",") }}
math.* (~30 functions)
Abs, Max, Min, Round, Floor, Ceil, Pow, Sqrt, Log,
Log10, Log2, trig functions, Pi, E, IsNaN, IsInf.
Example: {{ math.Round(total * 1.2) }}
fuzzy.* (7 functions)
Similarity, IsMatch, Contains, BestMatch, BestMatchWithScore,
FindAllMatches, LookupMatch — for approximate string matching.
Example: {{ fuzzy.BestMatch(query, knownCompanies) }}
collections.* (3 functions)
| Function | Purpose |
|---|---|
collections.Fnmatch(name, pattern) | shell-glob match (*, ?, [...]) |
collections.Unique(arr) | first-seen dedup, any element type |
collections.DistinctBy(arr, "field") | first-seen dedup by field name |
path.* (6 functions)
GetFileName, GetExtension, GetDirectoryName,
GetFileNameWithoutExtension, Combine, GetTempPath. They use the robot's
own separator, so path.Combine("a", "b") is a\b on Windows and a/b
elsewhere — build paths with these rather than by concatenating strings.
Recipes
Each recipe names a task, the before-Python approach, and the expression
replacement. Every expression here is covered by a passing probe in
expression_capabilities_test.go.
Pick the latest record per unique key
Before — Python with dict grouping + sort.
{{ resources | sortBy(.updated_at, "desc") | collections.DistinctBy("name") }}
Deduplicate a list
{{ collections.Unique(codes) }}
{{ collections.DistinctBy(orders, "customer_id") }}
Set difference (what's expected but missing)
{{ filter(expected, !(# in map(available, .name))) }}
Build a CSV string for a names= filter
{{ strings.Join(map(codes, # + ".pdf"), ",") }}
Match a supplier code against a mapping rule table
{{ let c = supplierCode;
let hit = find(sortBy(mappings, .priority), collections.Fnmatch(c, .pattern));
hit == nil ? c + ".pdf" : hit.tech_sheet + ".pdf" }}
Count duplicates by field value
{{ count(toPairs(groupBy(resources, .name)), let p = #; len(p[1]) > 1) }}
Conditional routing in an IfCondition
{{ any(orders, .status == "pending" && .total > 1000) }}
{{ len(codes) - count(codes, !(# + ".pdf" in map(available, .name))) }}
Safe access into nested webhook payloads
{{ webhookBody?.value?[0]?.resourceData?.id ?? "unknown" }}
First element matching a pattern
{{ find(items, .name matches "^invoice_[0-9]+\\.pdf$") }}
Sort & take the top N
{{ items | sortBy(.score, "desc") | take(5) | map(.id) }}
Known limitations (document, don't try)
These hit expr-lang grammar issues, not things we haven't registered.
Workarounds noted.
- Dict literals inside
map()/filter()/count()predicates failmap(codes, {code: #, file: # + ".pdf"})→ parse error. Workaround:map(codes, fromPairs([["code", #], ["file", # + ".pdf"]])), or compute in an outerletbinding. .[index]inside a predicate failscount(pairs, len(.[1]) > 1)→ parse error. Workaround: rebind withlet p = #; p[1].- Array
+concat and string*repeat are not supported Usestrings.Repeat("-", 5); for array concat, usereduceor compute in separate steps.
When RunPython is still the right choice
Use RunPython when you need:
- Multi-page / spatial PDF text extraction (use span geometry, OCR coords)
- Stateful loops that mutate across iterations (expressions are pure)
- External library calls (pandas, numpy, pillow, pymupdf, etc.)
- Custom regex with capture groups (
matchesreturns bool only) - Anything you'd describe as "a small program" rather than "a transform"
Everything else — filtering, dedup, sort, group, set ops, pattern matching, string manipulation — is an expression.