Web開発
SendLang: Eメール自動化のためのDSL
SendLang: A DSL for Email Automation (sendlang.com)
要約
SendLangは、Eメールのライフサイクル管理を自動化するための2つの言語、SendQLとSendFlowを提供します。SendQLは対象者を定義し、SendFlowは時間経過に伴うアクションを定義します。これらの言語はプレーンテキストファイルとしてバージョン管理システムに保存でき、コードと同様のレビュープロセスを経ることができます。また、コーディングエージェントによる生成も可能で、タイプチェッカーによる検証を経てから実行されます。
全文翻訳
Reference
The full syntax for both languages
A language layer for lifecycle email
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.
Read the docs
Try it in SendOps
Written by people and by coding agents — a published grammar, a type checker to iterate against, and a diff you approve.
See how.
SendFlow
trial-onboarding.flow
workflow "Trial onboarding" v1 {
enter on segment "trial-started"
exit "converted"
when attr.plan != "trial"
send "welcome" via topic "onboarding"
wait 2d
if not exists(open where template = "welcome") {
send "welcome-reminder" via topic "onboarding"
wait 2d
}
send "activation-tips" via topic "onboarding"
wait up to 7d until count(activity.login within 7d) >= 2 {
timeout: send "need-a-hand" via topic "onboarding"
}
}
Already in production at
Cloud 66
SendOps
Adze
Fortworx
Markbase
SendLang
One family, two languages
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
the who
A segment-definition query language.
Every query is a single boolean expression describing which contacts a segment matches.
SendQL
segment.sendql
attr.plan = "pro" and attr.country in ["US", "CA", "MX"] and not suppressed
SendFlow
the what
A drip-workflow language stored in .flow files.
It reads top to bottom and embeds SendQL for every condition.
SendFlow
onboarding.flow
workflow "Nudge" v1 {
enter on segment "trial-started"
wait 2d
if not exists(open within 2d) {
send "nudge" via topic "onboarding"
}
}
Why a language
Your lifecycle email, in version control
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−1·workflow "Winback" v1 {
· enter on segment "inactive-90d"
· exit "reactivated" when count(open within 14d) >= 1
·
· send "we-miss-you" via topic "marketing"
− wait 3d
+
wait up to 5d until exists(click within 5d) {
+
timeout: send "last-call" via topic "marketing"
+
}
·
}
It lives in version control
Every 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 sends
A 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 it
This 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
Hand the campaign to an agent
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.”
SendFlow
trial-ending.flow
workflow "Trial ending" v1 {
enter on 3d before attr.trial_ends_at
exit "upgraded" when attr.plan != "trial"
send "trial-ending" via topic "onboarding"
wait up to 3d until exists(click where template = "trial-ending" within 3d) {
timeout: send "upgrade-offer" via topic "marketing"
}
}
ci
$sendflow-fmt -l flows/$sendflow-lint -registry registry.json flows/*.flow
$flows/trial-ending.flow: ok
The docs are machine-readable
Every 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 guess
The 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 loop
Every 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 approver
No 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.
Writing SendLang with a coding agent
SendQL
Describe exactly who you mean
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.
Attributes and lists
Match on contact data with operators, in [...] sets, contains / starts with / ends with, and list or segment membership.
Events as a first-class source
Query the raw event stream directly — count, exists, sum, avg, last, first — not just precomputed engagement rollups.
Consent and suppression
Express deliverability rules inline: subscribed to "topic", opted out of "topic", unsubscribed from all, not suppressed.
Windows and recency
Bind a time window to any event source with within 30d or between two dates, and reason about recency with now - 14d.
SendQL
segments.sendql
// A high-intent trial user, and safe to email.
attr.plan = "trial"
// Signed up 3-14 days ago and now
and attr.signup_date between 3d and 14d
// Engaged with the mail we already sent
and count(open within 14d) >= 1
and exists(click where url contains "/pricing" within 7d)
// Uses the product, but has not bought
and count(activity.login within 7d) >= 2
and not exists(activity.order)
// Opted in, and not suppressed
and subscribed to "product-updates" and not suppressed
SendFlow
cart-abandonment.flow
workflow "Cart abandonment" v1 {
enter on activity.cart_updated where not exists(activity.order within 1h)
exit "purchased" when exists(activity.order)
reentry per occurrence
wait 1h
hold out 10%
send "cart-reminder" via topic "marketing"
repeat up to 2 every 24h until exists(activity.order) {
send "cart-nudge" via topic "marketing"
}
}
The same file, rendered as a canvas
Enter
cart_updated
Wait
1 hour
Send
cart-reminder
repeat up to 2 · every 24h · until order
Send
cart-nudge
Exit
purchased
SendFlow
Structure, no goto
A workflow reads top to bottom and maps losslessly to a flowchart — sequence, waits, branches, weighted splits, and one bounded repeat.
Never a goto, never an unbounded loop.
One trigger, many exits
Enter on a segment, an activity, an email event, or a duration before a contact's own date.
Named exits report conversions the moment they happen.
Timed steps
wait 2d, wait until a date or attribute, or wait up to 7d until a condition — with an optional timeout arm.
Branch, split, hold out
Fall-through if / else if / else, weighted split { 30%: ... 70%: ... } that must sum to 100, and hold out 10%.
Bounded loops only
The single loop is repeat up to N every duration until a condition.
No goto, no unbounded loops, ever.
How they fit
SendFlow speaks SendQL
The predicate you write to define a segment is the very same syntax that drives every condition inside a workflow.
A SendQL segment
SendQL
engaged.sendql
count(open within 14d) >= 1
The same predicate, inside a workflow
SendFlow
winback.flow
workflow "Winback" v1 {
enter on segment "inactive-90d"
exit "reactivated" when count(open within 14d) >= 1
send "we-miss-you" via topic "marketing"
wait up to 5d until exists(click within 5d) {
timeout: send "last-call" via topic "marketing"
}
}
A strict superset
One shared token stream
One type-checker and diagnostics
Design
Built like languages, not config
Both languages are versioned by a profile number, split cleanly from their execution engines, and specified down to a machine-checked grammar.
Structured by design
Sequence, branch, bounded repeat, timed waits, named exits — a control-flow subset that maps losslessly to a flowchart.
Events as first class
Selection over the raw event stream sits alongside contact attri