Browse the docs

SendQL

Time

Durations, windows, `now`, and the age of a datetime attribute.

Time shows up in SendQL in three places, and they are easy to keep apart once you have seen all three side by side.

SendQLSyntax
now - attr.signup_date > 7d          // age:     how long ago was this attribute?
count(open within 30d) >= 3          // window:  bound an event source in time
last(activity.login) < now - 14d     // recency: when did this last happen?

Durations

A duration is an amount and a unit suffix, with no space between them:

SendQLSyntax
30d    6mo    12h    2w    1y    90s

The units are s, m, h, d, w, mo, y. There is one form and only one form — 30 days is not a duration, 30 d is not a duration, and -1d is not a duration. Durations are unsigned; direction comes from the operator around them.

The unit you must be careful with is m, which is minutes. Months are mo.

SendQLSyntax
5m     // five minutes
5mo    // five months

Months and years are nominal: 30 days and 365 days exactly. They are not calendar-aware, and this is deliberate. A calendar-exact month would make a literal’s meaning depend on when it happened to be evaluated, which would make it impossible to check a window statically or to fold a schedule at compile time. 6mo means 180 days, on every day of the year.

The full table is on the durations reference.

now

now is the current instant, and it is only useful in two shapes:

SendQLSyntax
now              // the instant itself
now - 14d        // an instant, fourteen days ago

You can subtract a duration from now, and that is the whole of the arithmetic in SendQL. You cannot add to it, multiply it, or subtract two attributes from each other. A date literal works anywhere now does:

SendQL
last(open) > 2026-01-01

Windows

A window bounds an event source. It comes last inside the parentheses, after any where clause.

within <duration> — relative to now, looking backwards:

SendQL
count(click within 7d) >= 1

between <time> and <time> — an absolute or relative span:

SendQL
count(open between 2026-01-01 and 2026-02-01) > 0
SendQL
count(open between now - 30d and now - 7d) >= 2

The second one is the interesting shape: “opened at least twice, between a month ago and a week ago” — a question about a slice of the past that within cannot express.

Write the bounds backwards and you get an error rather than a query that matches nobody:

SendQLRejected
count(open between 2026-02-01 and 2026-01-01) > 0

Omit the window and the source means ever.

Age

now - attr.<datetime> is the age of a datetime attribute. It produces a duration, which you compare against a duration:

SendQL
now - attr.signup_date > 7d
SendQL
now - attr.trial_ends_at between 3d and 14d

Read now - attr.signup_date > 7d as “signed up more than seven days ago”. Bigger age means longer ago, which is the intuitive direction — but note that it is the opposite direction from a recency term, where < means “longer ago”. That is not an inconsistency: an age is a duration and gets bigger with time, while last(...) is an instant and gets smaller the further back it was.

If you only remember one thing:

Shape> means< means
now - attr.x (a duration)longer agomore recently
last(source) (an instant)more recentlylonger ago

The attribute must be a datetime. An age term on a string is an error, and the between bounds may not be reversed.

Age is absence-sensitive

Every age term silently excludes contacts where the attribute is unknown, and SendQL will warn you about it every time:

SendQL
now - attr.signup_date > 7d

warning: predicate on 'attr.signup_date' excludes contacts where it is unknown

If that is what you wanted, silence it by saying so:

SendQL
has attr.signup_date and now - attr.signup_date > 7d

If it was not, include the absent case explicitly:

SendQL
now - attr.signup_date > 7d or attr.signup_date is unknown

See the absence contract for why this is a warning rather than a default.