---
title: Personalize an email
description: Render recipient variables locally or deliberately choose a native grouped audience.
---

Use `sendPersonalized` for an explicit template and audience. Templates omit recipient fields; audience entries supply `to`, variables and optional keys.

```ts
import { Effect } from "effect";
import { Email, renderEmailPersonalizedMessage } from "@corven/email-sdk";
import { recordingEmailLayer } from "@corven/email-sdk/testing";

const template = {
  body: { html: "<p>Hello %recipient.name%</p>", text: "Hello %recipient.name%" },
  from: { address: "sender@example.test", name: "Synthetic Sender" },
  subject: "Hello %recipient.name%",
} as const;

export const personalizedPreview = renderEmailPersonalizedMessage(template, {
  to: "first@example.test",
  variables: { name: "Fictional & synthetic" },
});

export const recordedPersonalization = Effect.gen(function* recordPersonalizedNotifications() {
  return yield* (yield* Email).sendPersonalized(
    {
      message: template,
      recipients: [
        {
          idempotencyKey: "synthetic/first",
          to: "first@example.test",
          variables: { name: "First" },
        },
        {
          idempotencyKey: "synthetic/second",
          to: "second@example.test",
          variables: { name: "Second" },
        },
      ],
    },
    { concurrency: 2 },
  );
}).pipe(Effect.provide(recordingEmailLayer));
```

The preview helper needs no sender. The recording program expands two individual messages without contacting a provider.

## Template expansion

Variables use `%recipient.name%` syntax. Missing variables and invalid expanded messages remain failures. HTML expansion escapes text markup, but does not validate URL/CSS contexts or sanitize arbitrary HTML. Keep templates trusted and validate links in application code.

Every independent recipient operation has its own key. Shared options carry concurrency and execution context, not one key for the audience.

## Native grouped requests

Use `sendgridPersonalizedEmailLayer` or `mailgunPersonalizedEmailLayer` only when the provider's grouped operation matches your intent:

```ts
import { Effect, Layer } from "effect";
import type { Redacted } from "effect";
import { Email } from "@corven/email-sdk";
import { sendgridPersonalizedEmailLayer } from "@corven/email-sdk/sendgrid";
import { mailgunPersonalizedEmailLayer } from "@corven/email-sdk/mailgun";
import { emailHttpLayer } from "./sending.js";

export const sendgridNativeAccount = (apiKey: Redacted.Redacted) =>
  sendgridPersonalizedEmailLayer({ apiKey, region: "Europe" }).pipe(Layer.provide(emailHttpLayer));

export const mailgunNativeAccount = (apiKey: Redacted.Redacted, domain: string) =>
  mailgunPersonalizedEmailLayer({ apiKey, domain, region: "Europe" }).pipe(
    Layer.provide(emailHttpLayer),
  );

export const submitNativeAudience = Effect.gen(function* submitNativeAudience() {
  const email = yield* Email;
  return yield* email.sendPersonalized({
    message: {
      body: { html: "<p>Hello %recipient.name%</p>", text: "Hello %recipient.name%" },
      from: "sender@example.test",
      subject: "Hello %recipient.name%",
    },
    recipients: [
      { to: "first@example.test", variables: { name: "First" } },
      { to: "second@example.test", variables: { name: "Second" } },
    ],
  });
});
```

Provide one of these account Layers to `submitNativeAudience`. Inspect `personalization: "NativeAudience"` on the acquired service. A valid subset can be submitted as one request with shared acknowledgment, deadline and correlation.

Common defaults, transformations and ordinary routing refuse native-audience Layers. They do not silently rerender or split the request. A per-audience Result is not proof of a separate wire send or independent recipient acceptance.

See [sending modes](/concepts/sending-modes) and [batching and routing](/batching-routing) for ordering and failure semantics.
