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.

Who receives the email?
Section titled “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?
Section titled “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?
Section titled “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 and needs the address on a verified domain. Send-only needs 4 DNS records and leaves your inbound mail alone, which the Workflows overview covers. Omit from to send from the mailbox of the grant whose calendar the event is on.
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>" }'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": "[email protected]", "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?
Section titled “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.
{{#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:
{{#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? covers the guard rules in full.
The complete template
Section titled “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>
<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}}<{{email}}>{{#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>© 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 publishes a sample payload to test with.
What’s next
Section titled “What’s next”- Tell Events API attendees the meeting is recorded when a Notetaker joins the meeting
- Customize Scheduler email for your app for the Scheduler equivalent and its different payload
- Why isn’t my workflow sending email? for the guard rules and render testing
- Events notifications for the full
event.createdpayload