SendFlow
Lints
Every structural and semantic check the analyzer runs on a workflow, and what each one is protecting you from.
sendflow-lint runs two kinds of check. Structural lints need no knowledge of
your account and always run. Registry checks need to know what attributes,
events, topics, templates and lists exist, and are skipped if you do not supply a
registry.
Errors fail the build. Warnings do not — they are things that are probably wrong, and occasionally exactly right.
Structural — errors
These are all caught with no registry at all.
| Message | What it protects you from |
|---|---|
workflow has no enter setting — it can never start | A campaign that can never enrol anyone |
duplicate enter setting — a workflow has one trigger | Two beginnings |
duplicate exit name "x" (first declared at …) | Two goals reporting into one conversion number |
duplicate send window setting | Ambiguous send hours |
duplicate reentry setting | |
duplicate frequency cap setting | |
duplicate enroll setting | |
workflow version must be at least v1 | v0 |
"x" is a reserved word and cannot be used as an event name | enter on event count |
frequency cap must be at least 1, got 0 | A cap that blocks everything |
hold out must be between 1% and 99%, got 100% | A holdout that holds out everyone |
split weights must sum to 100%, got 60% | Contacts silently falling out of a split |
split arm weight must be at least 1% | A dead arm |
repeat up to 0 — the bound must be at least 1 | A loop that never runs |
duration must be at least 1d | wait 0d, every 0h |
enroll existing on a date-relative trigger requires a since <duration> bound | Enrolling every contact who has ever had an anchor date |
The split one is worth dwelling on, because it is the check that pays for itself:
workflow "t" v1 {
enter on segment "trial-started"
split {
30%: { send "a" via topic "marketing" }
30%: { send "b" via topic "marketing" }
}
}
Forty percent of the contacts in that split go nowhere. In a canvas you would find out from the send volumes a week later. Here it does not compile.
Structural — warnings
| Message | Why |
|---|---|
unreachable: this statement follows an exit | Dead code after a terminal exit |
empty block | An if arm that does nothing |
empty split arm | |
empty repeat body | |
empty timeout block — omit it to continue on timeout | The empty arm and the absent arm mean the same thing; say the shorter one |
split with a single arm — did you mean hold out? | A one-armed split is a holdout spelled the long way |
send window opens and closes at the same time | 9am-9am — almost always a typo |
wait up to 400d is a very long wait window | Anything over a year |
control flow nested more than 5 levels deep — consider flattening | A workflow nobody can read |
Registry checks
These need -registry. Without it they are silently skipped, which is why a
clean sendflow-lint with no registry means considerably less than it looks.
| Message | Trigger |
|---|---|
unknown event "x" | An enter on event that is not one of the eight |
unknown attribute "x" | Any attr.x that does not exist |
unknown template "x" | A send of a template you do not have |
unknown topic "x" | A via topic that does not exist |
unknown list "x" | An add to list that does not exist |
a date-relative trigger requires a datetime attribute; attr.plan is string | enter on 3d before attr.plan |
wait until requires a datetime attribute; attr.plan is string | wait until attr.plan |
cannot set number attribute attr.score to a string value | set attr.score = "high" |
cannot set attr.subscription to "gold"; allowed values are … | An enum attribute set outside its set |
Plus every diagnostic SendQL itself produces, for every embedded predicate —
unknown attributes, type mismatches, reserved words, and the absence lint. They
report at their real line and column in the .flow file. See
SendQL’s type rules.
The absence warning, in a workflow
The one you will see most often, and the one most worth not ignoring:
if attr.plan != "trial" { ... }
warning: predicate on 'attr.plan' excludes contacts where it is unknown — add 'or attr.plan is unknown' to include them, or 'has attr.plan' to silence this
In a branch, an excluded contact does not vanish — they take the else arm. That
is how the wrong cohort ends up receiving the wrong email, and it is why this is
worth a warning even though it is frequently harmless. See
the absence contract.
Reading a diagnostic
flows/winback.flow:8:3: warning: unreachable: this statement follows an exit
│ │ │
│ │ └─ severity: error or warning
│ └──── column
└────── line
Errors and warnings both carry a precise position, because parsing and analysis both track one. The CLI prints a caret under the offending token.
Exit codes: 0 clean, 1 a parse failure or any error, 2 a usage or I/O
problem. Warnings alone do not fail.