# Diagnostics

> How to read an error, and the complete catalogue of what both languages can tell you.

Source: https://www.sendlang.com/docs/reference/errors
Section: Reference

## How to read one

Every diagnostic carries a precise position, because both the parser and the
analyzer track one.

```
flows/winback.flow:8:3: error: unknown template "welcom"
                  │ │  │
                  │ │  └─ severity
                  │ └──── column
                  └────── line
```

**Errors** fail the build. **Warnings** do not — they mark things that are
probably wrong and occasionally exactly right. The most important lint in the
language, [the absence lint](/docs/sendql/absence), is a warning for precisely
that reason.

There are no numeric error codes. The message *is* the identifier, and the
messages are written to be read.

## Parse errors

A parse failure is grammar-only and stops at the first problem. It has no idea
what your attributes are — it only knows the program is malformed.

**Rejected — the parser refuses this:**

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

> `1:12: unexpected token "=" (expected Value)`

Common ones:

| You wrote | Why it fails |
|---|---|
| `attr.plan == "pro"` | There is no `==`. Use `=`. |
| `attr plan = "pro"` | Missing the `.` after `attr`. |
| `count(open within 30d)` | `count` always needs a comparison. |
| `exists()` | An event source is required. |
| `subscribed "news"` | Missing `to`. |
| `attr.country in []` | An empty set matches nothing; say that another way. |
| `sum(of activity.order) > 1` | The aggregate field is missing. |
| `attr.plan = "pro" and` | Trailing operator. |
| `(attr.plan = "pro"` | Unbalanced parenthesis. |

## Errors that hand you the fix

A few mistakes are common enough that the parser recognises them specifically and
names the replacement, rather than failing obscurely on whatever token happened
to break first.

| You wrote | What you get |
|---|---|
| `wait 2 days` | `durations are written in the compact form: use 2d, not 2 days` |
| `every 24 hours` | `… use 24h, not 24 hours …` |
| `wait 2 dayz` | `expected a duration such as 7d, 24h or 2w — "dayz" is not a unit` |
| `via topic onboarding` | `topic names are quoted: use via topic "onboarding", not via topic onboarding` |

These are the three shapes people reach for by instinct: a duration written out
in words, and a topic written without quotes. Both are wrong, and both say so
plainly.

## Name resolution

The analyzer knows what exists in your account. These are the errors that catch
typos.

| Message | Means |
|---|---|
| `unknown attribute "plna"` | No such attribute |
| `unknown event "order"` | Not one of the eight events — did you mean `activity.order`? |
| `event "open" has no property "colour"` | The property is not on that event's schema |
| `event "open" has no field "colour"` | Same, for an aggregate field |
| `activity "purchase" has no promoted property "amount"; promote it to filter on it` | The activity property must be promoted before you can filter on it |
| `unknown template "welcom"` | No such template |
| `unknown topic "marketng"` | No such topic |
| `unknown list "enagaged"` | No such list |

The `unknown event` one is the most common error in the language, and almost
always means the same thing: you wrote a product signal as if it were an email
event. Activities need the `activity.` qualifier.

## Type errors

| Message | Example |
|---|---|
| `operator > is not valid between string and number` | `attr.plan > 5` |
| `operator = requires matching types, got number and string` | `attr.score = "10"` |
| `string-match operators (contains/starts with/ends with) require a string operand, got number` | `attr.score contains "1"` |
| `in-list element is string but the attribute is number; all elements must match` | `attr.score in [1, "two"]` |
| `"gold" is not an allowed value for attr.subscription; allowed values are …` | An enum set outside its set |
| ``age expression `now - attr.plan` requires a datetime attribute, got string`` | Age on a non-datetime |
| `count(...) must be compared to a number, got string` | `count(open) >= "3"` |
| `last(...) must be compared to a time, got number` | `last(open) < 5` |
| `aggregate sum requires a numeric field, but open.template is string` | Summing a string |
| `between window is reversed: 2026-02-01 is after 2026-01-01` | Backwards window |
| `between window is reversed: 14d is longer than 3d` | Backwards age bounds |
| `cannot set number attribute attr.score to a string value` | `set attr.score = "high"` |
| `a date-relative trigger requires a datetime attribute; attr.plan is string` | `enter on 3d before attr.plan` |

See [SendQL's type rules](/docs/sendql/types) for the rule behind each.

## Reserved words

| Message | Example |
|---|---|
| `"within" is a reserved word and cannot be used as an event name` | `exists(within within 7d)` |
| `"within" is a reserved word and cannot be used as a property name` | `exists(click where within = true)` |
| `"count" is a reserved word and cannot be used as an aggregate field name` | `sum(count of activity.order) > 1` |

Only **bare** names are restricted. `attr.count` and `via topic "timeout"` are
both fine. See [reserved words](/docs/reference/reserved-words).

## Structural workflow errors

The full table, with what each one is protecting you from, is on
[SendFlow's lints page](/docs/sendflow/lints). The greatest hits:

| Message | Example |
|---|---|
| `workflow has no enter setting — it can never start` | No trigger |
| `split weights must sum to 100%, got 60%` | `30%` + `30%` |
| `hold out must be between 1% and 99%, got 100%` | |
| `repeat up to 0 — the bound must be at least 1` | |
| `duration must be at least 1d` | `wait 0d` |
| `duplicate exit name "converted"` | |
| `enroll existing on a date-relative trigger requires a since <duration> bound` | An unbounded backfill |

## Warnings

| Message | Meaning |
|---|---|
| `predicate on 'attr.x' excludes contacts where it is unknown` | [The absence contract](/docs/sendql/absence) |
| ``` `last(open)` excludes contacts with no such event ``` | Same, for recency — use `count(open) = 0` for the empty case |
| `unreachable: this statement follows an exit` | Dead code |
| `empty block` / `empty split arm` / `empty repeat body` | Something that does nothing |
| `empty timeout block — omit it to continue on timeout` | |
| `split with a single arm — did you mean hold out?` | |
| `send window opens and closes at the same time` | `9am-9am` |
| `wait up to 400d is a very long wait window` | Over a year |
| `control flow nested more than 5 levels deep — consider flattening` | |

## Exit codes

Both CLIs agree:

| Code | Means |
|---|---|
| `0` | Clean — or warnings only |
| `1` | A parse failure, or any error-severity diagnostic. For `sendflow-fmt -l` / `-d`, a file that would change. |
| `2` | A usage or I/O problem — a missing file, an unreadable registry |
