SendQL
Syntax
Tokens, literals, operators, comments and precedence — the lexical surface of SendQL.
Literals
| Kind | Written | Notes |
|---|---|---|
| String | "pro" | Double quotes only. Escape an inner quote as \". |
| Number | 42, 3.5 | |
| Boolean | true, false | |
| Date | 2026-01-01 | ISO, YYYY-MM-DD. No time component. |
| Duration | 30d, 6mo, 12h | Amount + unit suffix, no space. See durations. |
Single quotes are not string delimiters. There are no backticks, no heredocs and
no null literal — absence is expressed with is unknown, not with a value.
A duration is lexed before a number, which is why 30d is one token and not
30 followed by d. It is also why a duration cannot have a space in it:
30 d is two tokens and a parse error.
Operators
| Operator | Applies to |
|---|---|
= != | Any type — but both sides must be the same type |
< <= > >= | number, datetime, duration, and string (lexicographic) |
contains, starts with, ends with | string only |
in [...] | Any type; every element must match the left-hand side |
and, or, not | Booleans — that is, whole terms |
There is no ==. Writing it is the single most common syntax error, and it says
so:
attr.plan == "pro"
Precedence and grouping
From loosest to tightest: or, and, not, primary.
a or b and c // a or (b and c)
not a and b // (not a) and b
Parentheses override, and can nest anywhere a term can go — including inside an
event’s where clause:
exists(click where (url contains "/pricing" or url contains "/upgrade") and not user_agent contains "bot")
Comments
// to the end of the line. Comments are a real token in the language, not a
convention, so a saved segment can carry a note explaining a gnarly predicate:
// Trial users who engaged at least once but never bought.
attr.plan = "trial"
and count(open within 14d) >= 1
and not exists(activity.order)
Whitespace and newlines are insignificant. Break a long query across lines wherever it reads best; the parser does not care and there is no continuation character.
Qualified names
Two qualifiers exist, and both are followed by a .:
attr.<name>— a contact attributeactivity.<name>— a custom activity
Everything else that names something is either a bare identifier (the eight
built-in events, and property names inside a where) or a quoted string
(topics, lists, segments).
That split has one consequence worth internalising: a qualified or quoted name can be a reserved word; a bare one cannot.
attr.count = 5 // fine — attribute names are qualified
exists(within within 7d)
The first within is being used as an event name, and within is a
reserved word. Qualifying it would be fine;
bare, it is ambiguous with the window keyword, so it is rejected.
Whitespace, formatting and style
SendQL has no formatter of its own — a predicate is normally one line, and when it is embedded in a workflow, SendFlow’s formatter prints it on one line for you.
For a standalone segment definition long enough to need wrapping, the convention
that reads best is to break before each and / or and indent the continuation:
attr.plan in ["pro", "team"]
and count(open within 60d) = 0
and last(activity.order) < now - 90d