# SendQL

> A query language for defining who a message is for. One query is one boolean expression.

Source: https://www.sendlang.com/docs/sendql
Section: SendQL

A SendQL query is **one boolean expression**. There is no `SELECT`, no `FROM`,
no statement separator, no trailing semicolon. You write the condition and
nothing else, because there is only ever one thing being selected: contacts.

```sendql
attr.plan = "pro" and attr.country in ["US", "CA", "MX"] and not suppressed
```

Read it aloud and you have its meaning. That is the entire design goal.

## The five kinds of term

Every SendQL query is built from terms of exactly five kinds, combined with
`and`, `or`, `not` and parentheses.

**Attribute terms** ask about the contact's own data.

```sendql
attr.score >= 10 and attr.email ends with "@altacoda.io" and has attr.company_name
```

**Event terms** ask about the raw event and activity stream — how many, whether
any, the sum of, the most recent.

```sendql
count(open within 30d) >= 3 and exists(click where url contains "/pricing" within 7d)
```

**Age terms** ask how long ago a datetime attribute was.

```sendql
now - attr.signup_date between 3d and 14d
```

**Consent terms** ask what the contact has agreed to receive.

```sendql
subscribed to "product-updates" and not suppressed
```

**Membership terms** ask whether they are in a list or another segment.

```sendql
in list "beta-testers" and not in segment "power-users"
```

Each has its own page. That is the whole language.

## Events are first class

This is the part that is genuinely different from most segment builders.

Most engines let you filter on *precomputed rollups*: a `total_opens` counter, a
`last_seen` timestamp — fields somebody decided to materialise in advance. If the
rollup you need was not precomputed, you cannot ask the question.

SendQL queries the **stream** directly. `count`, `exists`, `sum`, `avg`, `min`,
`max`, `last` and `first` all operate over raw events and activities, each of
which can carry a `where` filter over its properties and a time window bound to
it:

```sendql
sum(amount of activity.order where status = "paid" within 90d) > 500
```

Nobody had to precompute "revenue from paid orders in the last ninety days".
There is no rollup here at all — the term *is* the question.

## Precedence

`or` binds loosest, then `and`, then `not`, then a primary. So:

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

```sendql
a or b and c        // parses as: a or (b and c)
```

Parentheses override, and are worth using even when they are technically
redundant:

```sendql
(attr.plan = "pro" or attr.plan = "team") and not suppressed
```

Without the parentheses that query means something quite different, and quietly
mails your suppressed pro users.

## What a query does not have

No sorting. No limits. No projection — you never choose which fields come back,
because a segment is a set of contacts, not a table. No joins you write
yourself; the event terms are the join. No functions beyond the eight aggregates,
no arithmetic beyond `now - <duration>`, and no way to call your own code.

The language is small on purpose. Everything it *can* express, it can also
type-check, explain in an error message, and compile to a query that runs over
millions of contacts without you thinking about it.

## Next

- [Syntax](/docs/sendql/syntax) — the tokens, literals and operators
- [Attributes](/docs/sendql/attributes) — matching on contact data
- [Events and activities](/docs/sendql/events) — querying the stream
- [The absence contract](/docs/sendql/absence) — the one semantic that surprises people
