Reference
Durations
One form, seven units, and two of them are nominal.
A duration is an amount and a unit suffix, with no space between them.
30d 12h 2w 6mo 1y 90s 5m
It is written identically everywhere — in a workflow’s schedule, in a SendQL window, in an age comparison. There is one form and there are no alternatives.
wait 2d // a schedule
frequency cap 4 per 7d // a schedule
repeat up to 3 every 24h { ... } // a schedule
if count(open within 7d) >= 1 { ... } // inside a predicate
if now - attr.signup_date > 30d { ... } // inside a predicate
The units
| Unit | Means | Exact length |
|---|---|---|
s | seconds | 1 second |
m | minutes | 60 seconds |
h | hours | 60 minutes |
d | days | 24 hours |
w | weeks | 7 days |
mo | months | 30 days — nominal |
y | years | 365 days — nominal |
m is minutes. Months are mo.
This is the one genuinely dangerous ambiguity in the language, and it is worth saying loudly:
5m // five minutes
5mo // five months
A wait 3m that was meant to be wait 3mo does not fail — it succeeds, three
minutes later. The lexer checks for the mo suffix first and falls back to the
single trailing character, so both spellings are valid and only one of them is
what you meant. There is no way for the language to catch this for you. Read your
diffs.
Months and years are nominal
1mo is exactly 30 days. 1y is exactly 365 days. Neither is calendar-aware:
1mo from the 31st of January is not the 28th of February, it is the 2nd of
March.
This is deliberate, and the reason is worth understanding because it explains a
lot about the rest of the design. A calendar-exact month would make a literal’s
meaning depend on when it was evaluated — 1mo would be 28 days in February
and 31 in March. That would make it impossible to check a between window for
reversal statically, and impossible for the engine to fold a schedule at compile
time.
A duration has to be a fixed quantity for the language to be able to reason about it at all. So it is one.
If you need calendar-exact behaviour — “the first of every month” — that is a date on the contact, and it belongs in a date-relative trigger, not in a duration.
What is not a duration
| Not this | Why |
|---|---|
30 days | The word form is not a duration. Write 30d. |
30 d | A duration is one token; no space. |
-1d | Durations are unsigned. Direction comes from the operator. |
2.5d | The amount is an integer. Write 60h. |
0d | Valid inside a predicate, but a schedule duration must be at least 1. |
The word form is the one people reach for first, and it fails with an error that hands you the fix rather than pointing at a token:
workflow "t" v1 {
enter on segment "trial-started"
wait 2 days
}
durations are written in the compact form: use 2d, not 2 days (a workflow and a SendQL condition spell a duration the same way now)
The compact form is the only form, everywhere. A workflow’s schedule and a condition inside that workflow spell a duration exactly the same way, so a single line never carries two spellings of one idea:
wait up to 7d until count(activity.login within 7d) >= 2
Get the unit wrong and it tells you that too:
workflow "t" v1 {
enter on segment "trial-started"
wait 2 dayz
}
expected a duration such as 7d, 24h or 2w — "dayz" is not a unit
Rounding and precision
0d is rejected as a schedule — wait 0d, every 0h and frequency cap 4 per 0d are all errors, because each describes something that would either never
happen or happen infinitely. Inside a predicate, a zero duration is merely
pointless rather than wrong, and is left alone.
A schedule duration over a year draws a warning (wait up to 400d is a very long wait window), on the theory that it is more often a typo than an intention.