SendLang: A DSL for Email Automation

ksajadi1 pts0 comments

SendLang — a language layer for lifecycle emailTry it in SendOps

ReferenceThe full syntax for both languages<br>A language layer for lifecycle email<br>SendLang is the home of two small, focused languages. SendQL describes who a message is for. SendFlow describes what happens to them over time — and speaks SendQL natively.<br>Read the docs Try it in SendOps<br>Written by people and by coding agents — a published grammar, a type checker to iterate against, and a diff you approve. See how.

SendFlowtrial-onboarding.flow

workflow "Trial onboarding" v1 {<br>enter on segment "trial-started"<br>exit "converted" when attr.plan != "trial"

send "welcome" via topic "onboarding"<br>wait 2d<br>if not exists(open where template = "welcome") {<br>send "welcome-reminder" via topic "onboarding"<br>wait 2d<br>send "activation-tips" via topic "onboarding"<br>wait up to 7d until count(activity.login within 7d) >= 2 {<br>timeout: send "need-a-hand" via topic "onboarding"

Already in production at<br>Cloud 66<br>SendOps<br>Adze<br>Fortworx<br>Markbase

SendLang<br>One family, two languages<br>SendQL is the noun. SendFlow is the verb. Together they turn contact data and event streams into campaigns you can read in a diff.

SendQL<br>the who<br>A segment-definition query language. Every query is a single boolean expression describing which contacts a segment matches.

SendQLsegment.sendql

attr.plan = "pro"<br>and attr.country in ["US", "CA", "MX"]<br>and not suppressed

SendFlow<br>the what<br>A drip-workflow language stored in .flow files. It reads top to bottom and embeds SendQL for every condition.

SendFlowonboarding.flow

workflow "Nudge" v1 {<br>enter on segment "trial-started"

wait 2d<br>if not exists(open within 2d) {<br>send "nudge" via topic "onboarding"

Why a language<br>Your lifecycle email, in version control<br>Every other tool locks your cohorts and lifecycle campaigns inside a visual editor. Make them a language instead, and they become text you can diff, review, and hand to a coding agent.

winback.flow+3&minus;1·workflow "Winback" v1 {<br>· enter on segment "inactive-90d"<br>· exit "reactivated" when count(open within 14d) >= 1<br>·​<br>· send "we-miss-you" via topic "marketing"<br>− wait 3d<br>+ wait up to 5d until exists(click within 5d) {<br>+ timeout: send "last-call" via topic "marketing"<br>+ }<br>·}

It lives in version controlEvery segment and workflow is a plain text file. Diff it, review it in a pull request, roll it back. Your targeting and your drip logic earn the same rigor as the rest of your code.

It fails before it sendsA misspelled attribute is a type error with a line and a column — not a campaign that quietly sends to nobody for a week.

A coding agent can write itThis is the big one. A canvas is a dead end for an agent; a language is the loop it already works in. Describe the outcome, let it draft the file, merge it once it passes review.

Coding agents<br>Hand the campaign to an agent<br>A canvas is a dead end for a coding agent — it cannot read it, change it, or tell you what it changed. A language is the loop an agent already works in: read the file, write the file, run the checker, open a pull request. That is not a side effect of the design. It is a reason for it.

“Three days before a trial ends, email them. If they have not clicked within three days, send the upgrade offer.”

SendFlowtrial-ending.flow

workflow "Trial ending" v1 {<br>enter on 3d before attr.trial_ends_at<br>exit "upgraded" when attr.plan != "trial"

send "trial-ending" via topic "onboarding"<br>wait up to 3d until exists(click where template = "trial-ending" within 3d) {<br>timeout: send "upgrade-offer" via topic "marketing"

ci$sendflow-fmt -l flows/<br>$sendflow-lint -registry registry.json flows/*.flow<br>$flows/trial-ending.flow: ok

The docs are machine-readableEvery page has a Markdown twin, and llms.txt / llms-full.txt hand an agent the whole of both languages in a single fetch. Nothing to scrape, nothing to guess at.

A grammar, not a guessThe normative EBNF and the reserved-word list are emitted by the parsers themselves, so the spec an agent reads cannot drift from the code that judges what it writes.

The type checker is the feedback loopEvery parse and type error carries a line, a column, and a message that names the fix. The agent iterates against the checker until the run is clean — and nothing sends until it is.

You are still the approverNo goto, no unbounded loops, nothing to evaluate. The worst an agent can hand you is a workflow that terminates — and you read it as a diff before it ever reaches a contact.

The shortest useful thing you can do: put /llms-full.txt in your agent's context. That is both languages, complete, in one fetch.<br>Writing SendLang with a coding agent

SendQL<br>Describe exactly who you mean<br>SendQL reads events as a first-class source alongside contact attributes — the gap left by engines that see only precomputed rollups, never the raw stream.<br>Attributes and listsMatch on contact data with operators, in [...] sets, contains / starts with / ends with, and list or segment membership.

Events as a first-class sourceQuery...

agent trial send topic language sendql

Related Articles