Browse the docs

SendQL

Syntax

Tokens, literals, operators, comments and precedence — the lexical surface of SendQL.

Literals

KindWrittenNotes
String"pro"Double quotes only. Escape an inner quote as \".
Number42, 3.5
Booleantrue, false
Date2026-01-01ISO, YYYY-MM-DD. No time component.
Duration30d, 6mo, 12hAmount + 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

OperatorApplies to
= !=Any type — but both sides must be the same type
< <= > >=number, datetime, duration, and string (lexicographic)
contains, starts with, ends withstring only
in [...]Any type; every element must match the left-hand side
and, or, notBooleans — that is, whole terms

There is no ==. Writing it is the single most common syntax error, and it says so:

SendQLRejected
attr.plan == "pro"

Precedence and grouping

From loosest to tightest: or, and, not, primary.

SendQLSyntax
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:

SendQL
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:

SendQL
// 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 attribute
  • activity.<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.

SendQL
attr.count = 5        // fine — attribute names are qualified
SendQLRejected
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:

SendQL
attr.plan in ["pro", "team"]
  and count(open within 60d) = 0
  and last(activity.order) < now - 90d