# Email attendees when a meeting is booked

Source: https://developer.nylas.com/docs/cookbook/workflows/event-booked-email/

Not every meeting comes from Scheduler. When your product creates events directly through the Events API, the attendees get whatever Google or Microsoft sends, which carries the organizer's branding rather than yours. An `event.created` workflow puts your own email in front of them.

This recipe covers the payload differences from Scheduler, which matter because the template variables aren't the same.

![A branded meeting confirmation email showing the meeting title, start time in the attendee's timezone, organizer, join link, and attendee list](/_images/workflows/event-booked.png)

## Who receives the email?

Attendees. Nylas sends one message per participant on the event and populates `recipient.email` with that person's address, so the template can greet each attendee by name without you building a recipient list.

The message goes to the addresses in the event's `participants` array. To send the organizer a copy, include them in that array when you create the event.

## How's the payload different from Scheduler?

There's no `booking_info` wrapper. Scheduler nests everything under `booking_info`, while `event.created` puts the event fields at the payload root, and times live under a `when` object rather than as top-level timestamps.

| Purpose | Scheduler | Events API |
| --- | --- | --- |
| Title | `booking_info.title` | `title` |
| Start time | `booking_info.start_time` | `when.start_time` |
| Timezone | `booking_info.guest_timezone` | `when.start_timezone` |
| Attendees | `booking_info.participants` | `participants` |
| Join link | `booking_info.location` | `conferencing.details.url` |
| Organizer | not exposed directly | `organizer.email` |
| Calendar link | `booking_info.event_html_link` | `html_link` |

A Scheduler template won't work here, and pointing an `event.created` workflow at one fails the render with `400` and sends nothing. Write a separate template.

`event.updated` and `event.deleted` carry the same root shape, so the same template structure covers a meeting being moved or cancelled. `event.updated` adds `cancelled_occurrences` for recurring events.

## How do I create the workflow?

Create a template, then an application-level workflow on `event.created`. The example sets `from`, which sends through [transactional send](/docs/v3/getting-started/transactional-send/) and needs the address on a verified domain. Send-only needs 4 DNS records and leaves your inbound mail alone, which the [Workflows overview](/docs/cookbook/workflows/#what-do-i-need-before-i-start) covers. Omit `from` to send from the mailbox of the grant whose calendar the event is on.

```bash
curl -X POST 'https://api.us.nylas.com/v3/templates' \
  -H "Authorization: Bearer $NYLAS_API_KEY" -H 'Content-Type: application/json' \
  -d '{
    "name": "Meeting booked",
    "engine": "handlebars",
    "subject": "Confirmed: {{title}}",
    "body": "<html>…</html>"
  }'
```

```bash
curl -X POST 'https://api.us.nylas.com/v3/workflows' \
  -H "Authorization: Bearer $NYLAS_API_KEY" -H 'Content-Type: application/json' \
  -d '{
    "name": "Meeting booked",
    "trigger_event": "event.created",
    "template_id": "<TEMPLATE_ID>",
    "delay": 0,
    "is_enabled": true,
    "from": { "email": "meetings@yourdomain.com", "name": "Your Company" }
  }'
```

Before enabling it, consider the volume. `event.created` fires for every event created on every calendar the application can see, including events your users create by hand in Google Calendar and events synced from elsewhere. On an application with busy calendars that's a lot of mail from your domain. Scope this to an application where your product is the thing creating events, or use a grant-level workflow to limit it to the customers who asked for it.

## How does the template handle dates and timezones?

The `when` object takes one of three shapes, named by `when.object`:

| `when.object` | Fields | Used for |
| --- | --- | --- |
| `timespan` | `start_time`, `end_time`, `start_timezone`, `end_timezone` | Timed meetings |
| `date` | `date` | A single all-day event |
| `datespan` | `start_date`, `end_date` | A multi-day all-day event |

Only `timespan` carries a Unix timestamp. `formatDate` throws on the date strings in `date` and `datespan`, so branch on `when.object` first and print the date as it arrives. The template prints only the start of a `datespan`, because Google and Microsoft set `end_date` to the day after the event ends.

For timed events, use `when.start_timezone`, which comes from the event itself. This is simpler than the Scheduler case, where the guest timezone is often empty.

```handlebars
{{#if (eq when.object "date")}}
  {{when.date}}, all day
{{else}}{{#if (eq when.object "datespan")}}
  All day, starting {{when.start_date}}
{{else}}
  {{#if when.start_timezone}}
    {{ formatDate when.start_time when.start_timezone "EEEE d LLLL yyyy, h:mm a ZZZZ" "en" }}
  {{else}}
    {{ formatDate when.start_time "UTC" "EEEE d LLLL yyyy, h:mm a ZZZZ" "en" }}
  {{/if}}
{{/if}}{{/if}}
```

The inner guard matters because `formatDate` throws on a missing argument rather than falling back. The `ZZZZ` token prints the zone name, so a recipient reading a time in a zone other than their own can see which one applies.

The join link needs three levels of guard, because `conferencing` is absent entirely on meetings without a video call:

```handlebars
{{#if conferencing}}{{#if conferencing.details}}{{#if conferencing.details.url}}
  <a href="{{conferencing.details.url}}">{{conferencing.details.url}}</a>
{{/if}}{{/if}}{{/if}}
```

Each level is required. The renderer throws when a guard reaches through a parent that doesn't exist, so `{{#if conferencing.details.url}}` on its own fails on a meeting with no conferencing. [Why isn't my workflow sending email?](/docs/cookbook/workflows/workflow-not-sending/) covers the guard rules in full.

## The complete template

This renders the email in the screenshot above for timed, all-day, and multi-day events, including events with no conferencing, no organizer name, or no timezone.

```html [eventBookedTemplate-event.created]

<html>

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Your meeting is confirmed</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
        Helvetica, Arial, sans-serif;
      line-height: 1.6;
      color: #333333;
      margin: 0;
      padding: 0;
      background-color: #f9fbfe;
      font-size: 16px;
    }

    .container {
      max-width: 600px;
      margin: 0 auto;
      padding: 20px;
    }

    .header {
      text-align: center;
      padding: 20px 0;
    }

    .content {
      background-color: #ffffff;
      padding: 30px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    .detail-label {
      color: #666666;
      font-size: 14px;
      padding: 6px 16px 6px 0;
      vertical-align: top;
      white-space: nowrap;
    }

    .detail-value {
      padding: 6px 0;
      vertical-align: top;
    }

    .footer {
      text-align: center;
      padding: 20px 0;
      color: #666666;
      font-size: 14px;
    }

    @media only screen and (max-width: 600px) {
      .container {
        width: 100% !important;
        padding: 10px !important;
      }

      .content {
        padding: 20px !important;
      }
    }
  </style>
</head>

<body>
  <div class="container">
    <div class="header">
      <img src="https://brand.nylas.com/assets/site_images/Nylas-Logo_Horizontal-Blue.png" alt="Nylas Logo"
        style="max-width: 150px;" />
    </div>

    <div class="content">
      <h1 style="margin-top: 0; color: #1A1A1A;">Your meeting is confirmed</h1>

      {{#if recipient}}{{#if recipient.first_name}}<p>Hi {{recipient.first_name}},</p>{{else}}<p>Hi there,</p>{{/if}}{{else}}<p>Hi there,</p>{{/if}}<p>{{#if title}}<strong>{{title}}</strong>{{else}}A meeting{{/if}} is on your calendar.</p>

      <hr style="border: 0; border-top: 1px solid #eeeeee; margin: 20px 0;" />

      <h2 style="color: #4D4D4D; font-weight: 500;">Details</h2>

      <table cellpadding="0" cellspacing="0" border="0" style="width: 100%;">
        <tr><td class="detail-label">When</td><td class="detail-value">{{#if (eq when.object "date")}}{{when.date}}, all day{{else}}{{#if (eq when.object "datespan")}}All day, starting {{when.start_date}}{{else}}{{#if when.start_timezone}}{{ formatDate when.start_time when.start_timezone "EEEE d LLLL yyyy, h:mm a ZZZZ" "en" }}{{else}}{{ formatDate when.start_time "UTC" "EEEE d LLLL yyyy, h:mm a ZZZZ" "en" }}{{/if}}{{/if}}{{/if}}</td></tr>
        <tr><td class="detail-label">Organizer</td><td class="detail-value">{{#if organizer}}{{#if organizer.email}}{{organizer.email}}{{/if}}{{/if}}</td></tr>
        <tr><td class="detail-label">Join</td><td class="detail-value">{{#if conferencing}}{{#if conferencing.details}}{{#if conferencing.details.url}}<a href="{{conferencing.details.url}}" style="color: #0D6EFD;">{{conferencing.details.url}}</a>{{/if}}{{/if}}{{/if}}</td></tr>
        <tr><td class="detail-label">Who</td><td class="detail-value">{{#each participants}}{{#if name}}{{name}} {{/if}}&lt;{{email}}&gt;{{#unless @last}}<br />{{/unless}}{{/each}}</td></tr>
      </table>
      <div style="text-align: center; margin: 30px 0;"><a href="{{#if html_link}}{{html_link}}{{/if}}" style="background-color: #0D6EFD; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Open in your calendar</a></div>
      <p style="font-size: 14px; color: #666666; text-align: center;">Replying to this message won't reach the organizer.</p>
    </div>

    <div class="footer">
      <p>&copy; 2026 <a href="https://www.nylas.com/" style="color: #0D6EFD;">Nylas</a></p>
      <p style="font-size: 12px; color: #999999; margin-top: 4px;">
        {{#if id}}Event: {{id}}{{/if}}
      </p>
    </div>
  </div>
</body>

</html>


```

Render it against a real `event.created` payload with `POST /v3/templates/render` before enabling the workflow. The [Events notification reference](/docs/reference/notifications/events/) publishes a sample payload to test with.

## What's next

- [Tell Events API attendees the meeting is recorded](/docs/cookbook/workflows/recording-notice-events/) when a Notetaker joins the meeting
- [Customize Scheduler email for your app](/docs/cookbook/workflows/scheduler-booking-email/) for the Scheduler equivalent and its different payload
- [Why isn't my workflow sending email?](/docs/cookbook/workflows/workflow-not-sending/) for the guard rules and render testing
- [Events notifications](/docs/reference/notifications/events/) for the full `event.created` payload