summary
in the workflow expression evaluator, ordering comparisons (<, >, <=, >=) between non-numeric strings silently return False. dates, version tags, and names never compare correctly.
root cause
_safe_compare in src/specify_cli/workflows/expressions.py coerces both operands to a number before comparing:
try:
if isinstance(left, str):
left = float(left) if "." in left else int(left)
if isinstance(right, str):
right = float(right) if "." in right else int(right)
except (ValueError, TypeError):
return False
any non-numeric string fails int()/float(), hits the except, and returns False for the whole comparison. so a legitimate string ordering is lost.
reproduction
from specify_cli.workflows.expressions import evaluate_expression
from specify_cli.workflows.base import StepContext
ctx = StepContext(inputs={"d": "2026-01-01"})
# want True (jan is before feb); actual: False
evaluate_expression("{{ inputs.d < '2026-02-01' }}", ctx)
ctx = StepContext(inputs={"name": "beta"})
# want True; actual: False
evaluate_expression("{{ inputs.name > 'alpha' }}", ctx)
==/!= are unaffected (they don't go through _safe_compare); only ordering ops.
impact
any workflow when: / gate condition that orders strings — comparing an iso date, a semver-ish tag, or a name — evaluates as False regardless of the real order, so steps are skipped (or run) incorrectly with no error.
proposed fix
coerce to a number only when both operands look numeric; otherwise compare the original values. two strings then order lexicographically like python, two numeric strings still compare as numbers ("10" > "9"), and a number vs a non-numeric string stays incomparable (False).
happy to open a PR (have one ready with a regression test).
summary
in the workflow expression evaluator, ordering comparisons (
<,>,<=,>=) between non-numeric strings silently returnFalse. dates, version tags, and names never compare correctly.root cause
_safe_compareinsrc/specify_cli/workflows/expressions.pycoerces both operands to a number before comparing:any non-numeric string fails
int()/float(), hits theexcept, and returnsFalsefor the whole comparison. so a legitimate string ordering is lost.reproduction
==/!=are unaffected (they don't go through_safe_compare); only ordering ops.impact
any workflow
when:/ gate condition that orders strings — comparing an iso date, a semver-ish tag, or a name — evaluates asFalseregardless of the real order, so steps are skipped (or run) incorrectly with no error.proposed fix
coerce to a number only when both operands look numeric; otherwise compare the original values. two strings then order lexicographically like python, two numeric strings still compare as numbers (
"10" > "9"), and a number vs a non-numeric string stays incomparable (False).happy to open a PR (have one ready with a regression test).