Problem
The workflow expression evaluator currently supports only 5 filters: default, join, map, contains, and from_json. For a system marketed as "Jinja2-like," common string and collection operations are missing, forcing workflow authors to shell out for basic transformations.
Missing filters that workflow authors frequently need:
- String operations:
upper, lower, trim (whitespace), split (string to list)
- Collection operations:
length, first, last, sort, unique
- Serialization:
to_json (the reverse of existing from_json)
Example of current limitation:
# Cannot do this today — no `length` filter:
- id: check
type: shell
config:
command: "echo 'Found {{ items | length }} files'"
# Cannot do this today — no `split` filter:
- id: list
type: shell
config:
command: "echo '{{ file_list | split(\",\") | length }} files found'"
Proposed Solution
Add a batch of new filters to the expression evaluator, following the existing registration pattern.
String Filters
| Filter |
Input |
Output |
Example |
upper |
"hello" |
"HELLO" |
{{ name | upper }} |
lower |
"HELLO" |
"hello" |
{{ name | lower }} |
trim |
" hi " |
"hi" |
{{ name | trim }} |
split |
"a,b,c" |
["a","b","c"] |
{{ csv | split(",") }} |
Collection Filters
| Filter |
Input |
Output |
Example |
length |
["a","b"] |
2 |
{{ items | length }} |
first |
["a","b","c"] |
"a" |
{{ items | first }} |
last |
["a","b","c"] |
"c" |
{{ items | last }} |
sort |
[3,1,2] |
[1,2,3] |
{{ items | sort }} |
unique |
["a","a","b"] |
["a","b"] |
{{ items | unique }} |
Serialization Filter
| Filter |
Input |
Output |
Example |
to_json |
{"key":"val"} |
"{\"key\":\"val\"}" |
{{ data | to_json }} |
Design Notes
- Follows existing pattern: Each filter is a standalone function registered in
_REGISTERED_FILTERS and dispatched in _apply_filter() (expressions.py lines 20-26, 419-484)
- Safe by design: All operations are pure transformations — no arbitrary code execution, no side effects, consistent with the sandboxed evaluator model
length enables conditionals: Unlocks {% if items | length > 0 %} patterns that currently require shell workarounds
split bridges shell output: Shell steps return newline/comma-separated strings; split converts them to lists for downstream processing
to_json completes the round-trip: from_json exists; to_json is its natural pair for passing structured data back to shell commands
Affected Files
src/specify_cli/workflows/expressions.py — filter registration and implementation (lines 20-26, 419-484)
tests/test_workflows.py — unit tests for each new filter
Questions
- Are there additional filters you would want in this batch, or should we start with a smaller set?
- Should
length work on both lists and strings (Jinja2 behavior), or only collections?
- Any naming preferences (e.g.,
len vs length, flatten vs unique)?
Happy to implement if this aligns with your plans for the expression engine.
Problem
The workflow expression evaluator currently supports only 5 filters:
default,join,map,contains, andfrom_json. For a system marketed as "Jinja2-like," common string and collection operations are missing, forcing workflow authors to shell out for basic transformations.Missing filters that workflow authors frequently need:
upper,lower,trim(whitespace),split(string to list)length,first,last,sort,uniqueto_json(the reverse of existingfrom_json)Example of current limitation:
Proposed Solution
Add a batch of new filters to the expression evaluator, following the existing registration pattern.
String Filters
upper"hello""HELLO"{{ name | upper }}lower"HELLO""hello"{{ name | lower }}trim" hi ""hi"{{ name | trim }}split"a,b,c"["a","b","c"]{{ csv | split(",") }}Collection Filters
length["a","b"]2{{ items | length }}first["a","b","c"]"a"{{ items | first }}last["a","b","c"]"c"{{ items | last }}sort[3,1,2][1,2,3]{{ items | sort }}unique["a","a","b"]["a","b"]{{ items | unique }}Serialization Filter
to_json{"key":"val"}"{\"key\":\"val\"}"{{ data | to_json }}Design Notes
_REGISTERED_FILTERSand dispatched in_apply_filter()(expressions.py lines 20-26, 419-484)lengthenables conditionals: Unlocks{% if items | length > 0 %}patterns that currently require shell workaroundssplitbridges shell output: Shell steps return newline/comma-separated strings;splitconverts them to lists for downstream processingto_jsoncompletes the round-trip:from_jsonexists;to_jsonis its natural pair for passing structured data back to shell commandsAffected Files
src/specify_cli/workflows/expressions.py— filter registration and implementation (lines 20-26, 419-484)tests/test_workflows.py— unit tests for each new filterQuestions
lengthwork on both lists and strings (Jinja2 behavior), or only collections?lenvslength,flattenvsunique)?Happy to implement if this aligns with your plans for the expression engine.