# Syntax

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

Source: https://www.sendlang.com/docs/sendql/syntax
Section: 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](/docs/reference/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:

**Rejected — the parser refuses this:**

```sendql
attr.plan == "pro"
```

## Precedence and grouping

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

**Syntax form, not a complete program:**

```sendql
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
```

**Rejected — the parser refuses this:**

```sendql
exists(within within 7d)
```

The first `within` is being used as an event name, and `within` is a
[reserved word](/docs/reference/reserved-words). 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](/docs/sendflow/formatting)
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
```
