Relationships
#1720 CEL expressions have timestamp/duration but no current-time value, so no guard can express "older than N"
Opened by sntxrr · 8/19/2026
Problem
timestamp(), duration() and timestamp arithmetic all work in workflow
expressions — but the CEL environment injects no current-time value, so none
of it can be used against now. Every time-relative predicate is therefore
impossible to write: "older than N days", "has not run since", "proof has
lapsed", "last success was too long ago".
This is the whole class of predicates a scheduled validation workflow needs.
Reproduction
swamp 20260818.022435.0-sha.4ed5cc7b, macOS arm64. A workflow step with a
when: input, allowFailure: false.
Timestamp/duration arithmetic works. This evaluates to true and the step
proceeds to act on it:
when: >-
${{ timestamp("2026-01-01T00:00:00Z")
< timestamp("2026-08-19T00:00:00Z") - duration("720h") }}Nothing supplies the current time. All four spellings fail at evaluation:
now -> Invalid expression: Unknown variable: now
now() -> Invalid expression: found no matching overload for 'now()'
timestamp() -> Invalid expression: found no matching overload for 'timestamp()'
time.now() -> Invalid expression: found no matching overload for 'dyn.now()'So the literal form above is the only way to compare against a point in time, and a literal cannot mean "now".
The evaluator is @marcbachmann/cel-js@7.6.1. Current time in CEL is
conventionally supplied by the host as a bound variable rather than a builtin,
so this looks like an omission in the environment swamp constructs rather than a
limitation of the library.
Concretely what it blocks
A restic fleet-validation workflow that needs to alert when a repository's last
proven restore is older than 30 days. The data is right there —
data.latest("restic-<repo>", "validation-restore").attributes.ranAt returns the
ISO timestamp — and it cannot be aged.
The established pattern for alerting in this repo's other scheduled workflows is
an apprise notify step gated on a when: predicate over model data. That pattern
works for every boolean the model computes at write time (reachable, stale,
exitCode != 0) and is unreachable for anything that depends on how much time
has passed since the write.
The workaround is to push the comparison out of CEL: either have a model method
precompute a stale boolean at read time and write it as a resource field, or
move the predicate out of the workflow entirely into a wrapper script. Both mean
a workflow cannot express a condition its own data fully determines.
Proposed solution
Bind a current-time value into the expression environment — now as a
google.protobuf.Timestamp variable would match how CEL hosts usually do this
and would make the existing timestamp/duration support usable:
when: ${{ timestamp(data.latest("m", "r").attributes.ranAt) < now - duration("720h") }}Evaluated once per expression evaluation, so a guard and a later step in the same run see a consistent value.
Alternatives considered
now()as a function — equivalent; a variable reads better and is the more common host convention.- A helper such as
age(ts)ordaysSince(ts)returning a duration/double. Friendlier for the common case, but narrower than exposing the clock. - Precompute in the model — what I will do meanwhile. It duplicates comparison logic into every extension that needs it, and the resource then carries a boolean that was only true at write time.
Closed
No activity in this phase yet.
sntxrr commented 8/19/2026, 4:56:50 PM
Withdrawing this — I was wrong, and the premise is incorrect
run.startedAt is bound into the expression context and does exactly what I
claimed was impossible. I filed this after testing four spellings of "now"
(now, now(), timestamp(), time.now()), all of which fail, and concluded
no current-time value existed. I did not check the run context object.
/manual/reference/cel-expressions documents it, and it works. Verified against
a real run in both directions:
when: >-
${{ timestamp(data.latest("m", "validation-restore").attributes.ranAt)
< timestamp(run.startedAt) - duration("720h") }}- threshold
720h, record ~1 day old → evaluated false, step succeeded, nothing sent - threshold
1s, same record → evaluated true, step proceeded to notify
So "older than N days" is expressible today, and the workarounds I described (precompute a boolean in a model method, or move the predicate into a wrapper script) are unnecessary. Closing.
Two small things a maintainer may or may not think worth acting on separately — I am not re-filing them, since neither is a defect:
run.startedAtis the workflow's start time, not the current moment. For a long-running workflow the skew equals its duration. Irrelevant at a 30-day threshold; worth knowing for short ones.- The failure for
nowisUnknown variable: now, which does not hint thatrun.startedAtis what you want. A pointer in that message would have saved this ticket.
Apologies for the noise.
Sign in to post a ripple.