Skip to main content
View as Markdown

Date Actions

The date action family does timezone- and locale-aware date work inside an automation: render an instant, read one back out of a string, shift it, measure between two, and snap to a calendar boundary. Eight operators, no more — the omissions are deliberate and are listed at the end of this page.

app.yaml
- name: dueLabel
  type: date
  operator: format
  props:
    input: '{{trigger.data.dueAt}}'
    pattern: "EEEE d MMMM yyyy 'at' HH:mm"
    timezone: Europe/Paris
    locale: fr-FR

The Operators

Operator Required props Optional props Output
format input, pattern timezone, locale { formatted }
parse input, pattern timezone { instant, valid }
add input + ≥1 duration part timezone { instant }
subtract input + ≥1 duration part timezone { instant }
diff from, to, unit timezone { value }
startOf input, unit timezone { instant }
endOf input, unit timezone { instant }
now (none) pattern, timezone, locale { instant }, plus formatted with a pattern

instant is always an ISO 8601 string, never a Date: a step output is JSON-persisted to run history, re-read by templates and returned in webhook bodies, and a Date survives none of those hops with its type intact.

timezone is an IANA identifier (Europe/Paris), defaulting to UTC. An instant carries no zone of its own — timezone is what the instant is read in. A fixed offset (+02:00) is accepted but cannot express DST, so prefer a named zone anywhere that observes it. locale is a BCP 47 tag (fr-FR, default en-US) and affects only the month and weekday name tokens; a purely numeric pattern ignores it.

The Pattern Token Set Is Closed

pattern is written in Unicode LDML tokens, and the vocabulary is closed:

Token Meaning Parseable
yyyy Calendar year, 4 digits (2026) yes
MM Month, 2 digits (01–12) yes
dd Day of month, 2 digits (01–31) yes
HH Hour, 2 digits, 24-hour (00–23) yes
mm Minute, 2 digits (00–59) yes
ss Second, 2 digits (00–59) yes
MMMM Month name, full, localised (March) no
MMM Month name, short, localised (Mar) no
EEEE Weekday name, full, localised (Saturday) no
EEE Weekday name, short, localised (Sat) no
YYYY Legacy alias of yyyy yes
DD Legacy alias of dd yes

The four name tokens are marked not parseable. parse therefore takes no locale, and a pattern containing MMMM or EEEE is rejected on the parse side — mars is ambiguous across locales and abbreviation styles, and accepting it would be guesswork rather than parsing.

parse Reports Validity as Data

app.yaml
- name: readDueDate
  type: date
  operator: parse
  props:
    input: '{{trigger.data.dueDate}}'
    pattern: dd/MM/yyyy
    timezone: Europe/Paris

Output is { instant, valid }. A string that does not match the pattern succeeds the step with valid: false and a null instant, rather than failing it. That is deliberate, and two behaviours depend on it:

  • Retry. A failed step is retried per its retry config. Retrying a deterministic verdict burns the budget and delays the run for a result that cannot change.
  • Control flow. A failed step stops the branch unless continueOnError is set, so a downstream filter or path meant to route the invalid rows would never run. Returning data keeps the decision where an author can act on it.

A malformed pattern is the opposite case — an author error, not data — and does fail the step.

app.yaml
- name: routeInvalid
  type: filter
  operator: continue
  props:
    condition:
      conditions:
        - field: '{{readDueDate.valid}}'
          operator: equals
          value: true
    onFalse: skip

Arithmetic: Calendar Units vs Elapsed Time

add and subtract take at least one duration component. The counts are plural (days: 7) while unit names elsewhere are singular — quantities versus names, the same split Temporal and java.time draw. Declaring none is a configuration error.

Component Resolved as
years Calendar — DST-aware, wall clock preserved
months Calendar — 31 January + 1 month is 28 February
weeks Calendar — DST-aware, wall clock preserved
days Calendar — a day across a DST change is 23 or 25 real hours
hours Elapsed — an hour is always 60 minutes
minutes Elapsed — unaffected by DST
seconds Elapsed — unaffected by DST

Larger units apply before smaller ones, so month-end clamping happens first: 31 January + { months: 1, hours: 6 } clamps to 28 February and then adds six hours.

app.yaml
- name: reminderAt
  type: date
  operator: subtract
  props:
    input: '{{trigger.data.dueAt}}'
    days: 2
    hours: 3
    timezone: Europe/Paris

diff — Signed and Truncated Toward Zero

app.yaml
- name: daysLate
  type: date
  operator: diff
  props:
    from: '{{trigger.data.dueAt}}'
    to: '{{now.instant}}'
    unit: day
    timezone: Europe/Paris

unit is singular: year, month, week, day, hour, minute, second or millisecond. The result is signed — negative when to precedes from — and truncated toward zero, so a 47-hour gap is one day, not two. Calendar units are counted against the zone, elapsed ones are not.

Because diff already answers with a sign, there are no isBefore / isAfter / isBetween operators; compare the result in a filter or path condition instead.

startOf / endOf — Calendar Boundaries

app.yaml
- name: monthStart
  type: date
  operator: startOf
  props:
    input: '{{trigger.data.occurredAt}}'
    unit: month
    timezone: Europe/Paris

unit is one of year, month, week, day, hour, minute or second — the same singular vocabulary as diff, minus millisecond, which no boundary snaps to. The boundary is computed in timezone, so the start of a day in Europe/Paris is 22:00 or 23:00 UTC the evening before, depending on the season.

now

app.yaml
- name: stamp
  type: date
  operator: now
  props:
    pattern: yyyy-MM-dd
    timezone: Europe/Paris

Returns { instant } — a UTC instant — and adds formatted when a pattern is supplied. It is the one operator with no required props.

What Is Deliberately Absent

The eight operators were chosen against a capability domain rather than against a date library's API surface, so several familiar names are missing on purpose:

  • isBefore / isAfter / isBetween — belong in a filter or path condition; diff already returns a signed answer.
  • toTimezone — an instant carries no zone, so the name teaches a wrong model. format's timezone covers it.
  • dayOfWeek / isWeekday / isWeekendformat with EEEE, plus a filter. Three operators for one token is the per-library sprawl this family exists to avoid.
  • timestamp / fromTimestamp — epoch tokens are a token-set question, not an operator question.
  • Business-day and holiday arithmetic — needs a calendar the platform does not have, and a wrong answer here is worse than no answer.

Calling Date Operators From Code

Every operator is reachable from a code action as context.actions.date.<operator>(props). That path does not decode props against the schema, so the handler re-applies the guards itself — an invalid timezone or an unrecognised token fails there exactly as it would at boot.

  • Actions Overview — the action model and every family.
  • Data & Statefilter/continue, where a parse verdict is routed.
  • Flow Controlpath/branch for date-driven routing.
  • Triggers — the cron trigger, which shares this timezone vocabulary.
  • Code Actions — the context.actions.date call path.

Last updated August 28, 2026

This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.

Built with Sovrium