You need: email composed, pithy email provision run, and a domain onboarded onto Cloudflare Email Service.
Enqueue
The job model is the row this writes, sending email is what happens to it next, the template model is what gets rendered, and bounces and suppression is why a send might never leave.
const { id, subject } = await enqueueEmail(db, {
to: "ada@example.com",
template: "welcome",
payload: { name: "Ada" },
locale: c.var.locale?.catalogLocale ?? null,
});Every email becomes a row first. A request handler only ever enqueues; the actual send always runs inside a Workflow.
The payload is validated against the template’s schema here, so a bad call fails while the caller is still there to hear about it — not half way through a send, days later, in a Workflow nobody is watching.
The rendered subject comes back
So nobody renders it twice.
A surface with an administrative trail has to record what it queued, and restating the kit’s English by hand agrees with the row only until a locale is passed — at which point the template renders the reader’s catalog and the mirror keeps saying English.
Pass the locale
null means nobody chose — render the kit’s English. It is a different fact from en, which asserts a choice.
The email is written in the language the job recorded, not in the language of whoever triggered it. A receipt for a Spanish-speaking customer, generated by an English-speaking admin’s action, is still in Spanish.
Suppression is checked at enqueue too
A blocked recipient never becomes a queued send. The row is born suppressed, no Workflow starts, and the reason comes back on the result.
The send path is still the authority — whether an address is blocked is a question about the instant of sending, and a scheduled job is enqueued days before that.
The enqueue check exists so the caller learns. Otherwise a three-person account whose addresses have all hard-bounced is three ordinary skips in a send log nobody reads, instead of one notice that it reached nobody.
The kind comes from the template
Never from your call site.
An unsubscribe blocks elective mail only; a bounce or a complaint blocks everything — which is what stops somebody who unsubscribed from a newsletter also losing their sign-in links.
A magic link sent as elective is a magic link an unrelated unsubscribe silently swallows, which is why the kind is declared on the template and there is no argument for it.
What retries
| Retryable | Rate limited · a transient delivery failure or upstream 5xx · a transient D1 fault |
| Terminal | The job row is gone · the template is not found · the payload will not render |
A suppressed recipient is neither. It is recorded and the send returns — somebody who asked not to be mailed is an outcome, not a failure.
Retry one by hand
POST /email/jobs/<id>/retry control-plane · email:jobs:retryThe payload is spent on delivery
For a transactional template, the stored variables are emptied when the message goes out, with a timestamp recording that it happened.
A magic-link token, an invitation code, a receipt’s line items — none of that needs to outlive the send, and a send log holding them is a send log worth stealing.