Browse the docs

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.

SendQLRejected
attr.plan == "pro"

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

Common ones:

You wroteWhy 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) > 1The aggregate field is missing.
attr.plan = "pro" andTrailing 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 wroteWhat you get
wait 2 daysdurations are written in the compact form: use 2d, not 2 days
every 24 hours… use 24h, not 24 hours …
wait 2 dayzexpected a duration such as 7d, 24h or 2w — "dayz" is not a unit
via topic onboardingtopic 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.

MessageMeans
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 itThe 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

MessageExample
operator > is not valid between string and numberattr.plan > 5
operator = requires matching types, got number and stringattr.score = "10"
string-match operators (contains/starts with/ends with) require a string operand, got numberattr.score contains "1"
in-list element is string but the attribute is number; all elements must matchattr.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 stringAge on a non-datetime
count(...) must be compared to a number, got stringcount(open) >= "3"
last(...) must be compared to a time, got numberlast(open) < 5
aggregate sum requires a numeric field, but open.template is stringSumming a string
between window is reversed: 2026-02-01 is after 2026-01-01Backwards window
between window is reversed: 14d is longer than 3dBackwards age bounds
cannot set number attribute attr.score to a string valueset attr.score = "high"
a date-relative trigger requires a datetime attribute; attr.plan is stringenter on 3d before attr.plan

See SendQL’s type rules for the rule behind each.

Reserved words

MessageExample
"within" is a reserved word and cannot be used as an event nameexists(within within 7d)
"within" is a reserved word and cannot be used as a property nameexists(click where within = true)
"count" is a reserved word and cannot be used as an aggregate field namesum(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:

MessageExample
workflow has no enter setting — it can never startNo 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 1dwait 0d
duplicate exit name "converted"
enroll existing on a date-relative trigger requires a since <duration> boundAn unbounded backfill

Warnings

MessageMeaning
predicate on 'attr.x' excludes contacts where it is unknownThe absence contract
`last(open)` excludes contacts with no such eventSame, for recency — use count(open) = 0 for the empty case
unreachable: this statement follows an exitDead code
empty block / empty split arm / empty repeat bodySomething 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 time9am-9am
wait up to 400d is a very long wait windowOver a year
control flow nested more than 5 levels deep — consider flattening

Exit codes

Both CLIs agree:

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