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.
attr.score > -5There are no negative numbers.
send window 13pm-6pm …invalid clock time "13pm" — hours run 1 to 12.
A setting after a statementReported as the setting’s keyword in the wrong place: "enter" is a reserved word and cannot be used as a name here. Move it above the first statement.

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
call webhook mobile_pushwebhook keys are quoted: use call webhook "mobile_push", not call webhook mobile_push

These are the shapes people reach for by instinct: a duration written out in words, and a name written without quotes. Both are wrong, and both say so plainly. The two quoting hints exist for the same reason — without them the parser reports the token that started the construct it could not finish, which is via or call, the one word on the line that is perfectly correct.

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
activity "purchase" has no promoted property "amount"; promote it to aggregate on itThe same, for sum/avg/min/max
unknown segment "trial-startd"enter on segment "…" names no segment (checked when you save in SendOps)
template "base" is a layout, not an email template — a layout cannot be sent on its ownsend "…" names a layout (checked when you save in SendOps)
in list "beta-tester": no List has this key or id in this organizationA saved segment’s in list / in segment names nothing; the segment is saved as invalid
unknown template "welcom"No such template
unknown topic "marketng"No such topic
unknown list "enagaged"No such list
unknown or unadopted sender identity "no-reply@acme.com"send … from "…" names an identity the account has not verified and adopted
unknown or unadopted channel "promo"send … via channel "…" names a channel the account has not adopted
unknown webhook endpoint "mobile-push-v2"call webhook "…" names an endpoint the account has not created with the Workflows purpose
invalid reply-to address "support at acme": must be an email address, optionally as `Name <addr>` send … reply-to "…" is not a well-formed address — shape only; it is never checked against the account’s identities
invalid webhook label "Cart Reminder": use lowercase letters, digits, - and _, starting with a letter or digit, up to 64 characterscall webhook … as "…" is not a machine-readable key — shape only, like reply-to; the label resolves against nothing

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
aggregate sum requires a numeric property, but activity.order.status is stringThe same, on an activity
"refunded" is not an allowed value for activity.order.status; allowed values are …An enum activity property compared outside its set
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 (the unit follows yours: every 0h → at least 1h)
invalid reply-to address "…": must not be empty / must be at most 320 characters / must not contain line breaksOther shapes a reply-to cannot take
frequency cap window 2y is longer than the 400d send-log retention horizon …A cap window over 400d (checked when you save in SendOps)
duplicate exit name "converted"
send window opens and closes at the same time, so it never opens — …9am-9am: the closing time is exclusive, so no moment is inside the window and nothing would ever send
enroll existing on a date-relative trigger requires a since <duration> boundAn unbounded backfill
invalid webhook label "…"A call webhook … as "…" label with spaces or capitals

Warnings

MessageMeaning
predicate on `attr.x` excludes contacts where it is unknown — …The absence contract
age term on `attr.x` excludes contacts where it is unknown — …The same, for now - attr.x
`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
empty failed block — omit it to continue when the call failsThe same, for a call webhook arm
duplicate webhook call "x" …Two calls one receiver cannot tell apart — label them
split with a single arm — did you mean hold out?
wait up to 400d is a very long wait windowOver a year
control flow nested more than 5 levels deep — consider flattening

Limits

SendOps refuses a predicate that would compile into a pathologically expensive query. The limits are generous for anything a person writes by hand:

LimitMessage
200 terms in one predicatepredicate: too complex: more than 200 term nodes
20 levels of nestingpredicate: too deeply nested: more than 20 levels
100 elements in one in [...]predicate: list literal has more than 100 elements
8 event or activity termspredicate: more than 8 event references
8 in list / in segment termspredicate: more than 8 list/segment membership references

If you hit one, the predicate almost always wants splitting into saved segments composed with in segment.

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