Skip to main content
← Back to list
01Issue
FeatureClosedSwamp CLIPublic
AssigneesNone

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) or daysSince(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.
02Bog Flow
OPENTRIAGEDIN PROGRESSCLOSED

Closed

8/19/2026, 4:56:50 PM

No activity in this phase yet.

03Sludge Pulse
Editable. Press Enter to edit.

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:

  1. run.startedAt is 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.
  2. The failure for now is Unknown variable: now, which does not hint that run.startedAt is what you want. A pointer in that message would have saved this ticket.

Apologies for the noise.

Sign in to post a ripple.