Browse the docs

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.

MessageWhat it protects you from
workflow has no enter setting — it can never startA campaign that can never enrol anyone
duplicate enter setting — a workflow has one triggerTwo beginnings
duplicate exit name "x" (first declared at …)Two goals reporting into one conversion number
duplicate send window settingAmbiguous send hours
duplicate reentry setting
duplicate frequency cap setting
duplicate enroll setting
workflow version must be at least v1v0
"x" is a reserved word and cannot be used as an event nameenter on event count
frequency cap must be at least 1, got 0A 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 1A loop that never runs
duration must be at least 1dwait 0d, every 0h
enroll existing on a date-relative trigger requires a since <duration> boundEnrolling 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:

SendFlowRejected
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

MessageWhy
unreachable: this statement follows an exitDead code after a terminal exit
empty blockAn if arm that does nothing
empty split arm
empty repeat body
empty timeout block — omit it to continue on timeoutThe 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 time9am-9am — almost always a typo
wait up to 400d is a very long wait windowAnything over a year
control flow nested more than 5 levels deep — consider flatteningA 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.

MessageTrigger
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 stringenter on 3d before attr.plan
wait until requires a datetime attribute; attr.plan is stringwait until attr.plan
cannot set number attribute attr.score to a string valueset 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:

SendFlowSyntax
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.