Reference
Diagnostics
How to read an error, and the complete catalogue of what both languages can tell you.
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, 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.
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 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.
Structural workflow errors
The full table, with what each one is protecting you from, is on SendFlow’s lints page. 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 |
`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 |