Batching, personalization and routing
Send ordered batches, personalize messages and configure fallback between named provider accounts.
Use sendAll for independent messages, sendPersonalized for one template and an audience, or named account routing to choose where a message is submitted.
Send a batch
sendAll accepts an array of { message, options? } envelopes and a bounded concurrency setting:
import { Effect } from "effect";
import { Email } from "@corven/email-sdk";
export const sendNotifications = Effect.gen(function* sendNotifications() {
const email = yield* Email;
return yield* email.sendAll(
[
{
message: {
body: { text: "A fictional notification." },
from: "sender@example.test",
subject: "Notification",
to: "recipient@example.test",
},
},
],
{ concurrency: 2 },
);
});
Provide an Email Layer when running this program, as shown in sending or the offline quickstart.
Each request has its own key and delivery policy. There is no batch-wide key for unrelated messages. Results retain input order even when messages finish in a different order; expected item failures appear as Effect Results.
Defects and interruption cancel active work and propagate. Already accepted messages cannot be undone. A batch is neither an atomic transaction nor an implicit provider-native bulk request.
Personalize a message
sendPersonalized takes a template without recipient fields and an explicit audience. Each audience member supplies a mailbox, template variables and an optional idempotency key. Shared options specify concurrency and context, not one key for the whole audience.
With the default Individual mode, the SDK expands the template into per-recipient messages. Use renderEmailPersonalizedMessage to preview expansion without a sender. HTML expansion escapes text markup, but does not validate URL or CSS contexts or sanitize arbitrary HTML. Missing variables and invalid expanded messages remain failures.
Use native personalization
Opt-in SendGrid and Mailgun Layers can submit a valid subset of the audience in one native request. Recipients then share request-level acknowledgment, deadline and correlation.
Check the configured personalization mode. Common defaults, transformation wrappers and ordinary routing reject NativeAudience Layers; they do not silently expand them into individual sends. A shared method name does not imply independent wire requests or per-recipient acknowledgment.
Route explicit accounts
Register a Layer for each named account, then select a route through EmailRouter:
import { Effect, Layer } from "effect";
import type { Redacted } from "effect";
import { EmailRouter, emailRouterLayer } from "@corven/email-sdk";
import { postmarkEmailLayer } from "@corven/email-sdk/postmark";
import { resendEmailLayer } from "@corven/email-sdk/resend";
import { emailHttpLayer } from "./sending.js";
export const sendRoutedNotification = (
resendKey: Redacted.Redacted,
postmarkToken: Redacted.Redacted,
) => {
const routes = emailRouterLayer(
[
{ layer: resendEmailLayer({ apiKey: resendKey }), name: "primary" },
{ layer: postmarkEmailLayer({ serverToken: postmarkToken }), name: "backup" },
],
{ fallbacks: ["backup"], primary: "primary" },
).pipe(Layer.provide(emailHttpLayer));
return Effect.gen(function* sendRoutedNotification() {
const router = yield* EmailRouter;
const email = yield* router.select();
return yield* email.send({
body: { text: "A fictional routed notification." },
from: "sender@example.test",
subject: "Notification",
to: "recipient@example.test",
});
}).pipe(Effect.provide(routes));
};
The example uses the HTTP Layer from sending and deliberately omits an idempotency key: every candidate, including Postmark, must support the requested key.
Before transport, the router preflights the effective message, options and policy for every selected candidate. An unsupported field or key on a backup fails locally instead of being dropped.
Understand when fallback stops
Only an eligible definite rejection advances to a fallback account. Unknown outcomes, conflicts, terminal receipts, defects and interruption stop the chain. Partial, suppressed and simulated receipts are terminal outcomes, not reasons to try a backup.
Routed receipts identify the account selected. One routing deadline covers the whole chain rather than resetting for each account.
Persist route selection for recovery across later executions. If a backup previously accepted the email, restarting at the primary can duplicate it. Native replay guarantees remain local to a provider account; they do not transfer across routes. See receipts, errors and retries.