---
title: Batching, personalization and routing
description: 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:

```ts
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](/sending) or the offline [quickstart](/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`:

```ts
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](/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](/reliability).
