# Tell Events API attendees a meeting is recorded

Source: https://developer.nylas.com/docs/cookbook/workflows/recording-notice-events/

When your product creates meetings through the Events API and sends a Notetaker to them, the attendees have no idea the call is being captured. The calendar invite says nothing about it. An `event.created` workflow sends the disclosure with the invitation.

This is the Events API counterpart to [the Scheduler version](/docs/cookbook/workflows/recording-notice-scheduler/). The logic is the same and the template variables aren't.

![A meeting notice email stating the meeting will be recorded and transcribed, with the start time, join link, and attendee list](/_images/workflows/recording-notice-event.png)

## Why fire this from event.created?

Because it's the only trigger in the chain that knows who is attending. The Notetaker triggers carry an `event` object containing just `ical_uid`, `event_id`, and `master_event_id`, with no participants and no title, so a template fired from `notetaker.created` has nobody to address.

`event.created` gives you `participants`, `title`, `when`, `organizer`, and `conferencing` at the payload root, and Nylas populates `recipient.email` per attendee. That's everything a disclosure needs.

`event.created` fires whether or not a Notetaker was sent to that meeting, and workflows don't filter, so scope the workflow to the accounts where recording is always on. That's covered next.

## How do I only send this for recorded meetings?

Use a grant-level workflow on the grants where every meeting is recorded, rather than an application-level workflow across everything. An application-level `event.created` workflow fires for every event on every calendar the application can see, including meetings your users create by hand, so a recording notice on all of them would be wrong as well as noisy.

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

If recording is a per-meeting choice rather than a per-account one, a workflow is the wrong tool. Consume the [`event.created` webhook](/docs/reference/notifications/events/) instead, check your own record of whether a Notetaker was invited, and send through [transactional send](/docs/v3/getting-started/transactional-send/) when it was. That costs you a handler and gives you a check a workflow can't make, because the event payload doesn't say whether a Notetaker was invited.

Remember that grant-level workflows don't appear in the Dashboard. Track which grants you have provisioned in your own system, because there is no interface that will tell you later.

## What should the email say?

Name what's captured, and say who to talk to. The template states audio, video, and a written transcript, because a Notetaker takes all three and attendees reading "recorded" tend to assume audio only.

Attendees can't remove the Notetaker themselves, so point them at the organizer. The organizer's address is in the payload as `organizer.email`, which makes that instruction concrete rather than generic.

Send it with `delay: 0`. A disclosure that arrives after the meeting has started is worthless, and events are often created minutes before they begin.

## How does the template handle the payload?

Three things need care, and all three are guard problems rather than content problems.

**The time depends on the kind of event.** 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, `when.start_timezone` comes from the event itself. Guard it anyway, because `formatDate` throws on a missing argument rather than defaulting.

```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 join link is three levels deep.** `conferencing` is absent entirely on meetings without a video call, so each level needs its own guard.

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

**Participants may have no name.** Iterate with `{{#each}}` and guard the name inside the loop, so an attendee who was invited by address alone still appears.

```handlebars
{{#each participants}}{{#if name}}{{name}} {{/if}}&lt;{{email}}&gt;{{/each}}
```

## The complete template

This renders the email above for timed, all-day, and multi-day events, including events with no conferencing, no timezone, and unnamed participants.

```html [recordingNoticeEvents-event.created]

<html>

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>This meeting will be recorded</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;">This meeting will be recorded</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}}Your upcoming meeting{{/if}} will be recorded and transcribed. A notetaker joins the call and captures audio, video, and a written transcript.</p><p>If you would rather it was not recorded, tell the organizer before the meeting starts.</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">Meeting</td><td class="detail-value">{{#if title}}{{title}}{{/if}}</td></tr>
        <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">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;">View the meeting</a></div>
      <p style="font-size: 14px; color: #666666; text-align: center;">Recording begins when the meeting starts and stops when it ends.</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 with `POST /v3/templates/render` against a real payload before enabling the workflow. A compliance email that silently fails to send is worse than one that looks plain.

## What's next

- [Tell Scheduler attendees the meeting is recorded](/docs/cookbook/workflows/recording-notice-scheduler/) for bookings made through Scheduler
- [Email attendees when a meeting is booked](/docs/cookbook/workflows/event-booked-email/) for the plain confirmation on the same trigger
- [Send the recording when Notetaker media is ready](/docs/cookbook/workflows/notetaker-media-ready/) delivers the output afterwards
- [Notetaker](/docs/v3/notetaker/) for how Notetaker joins meetings and what it captures