Skip to content
Skip to main content

Email attendees when a meeting is booked

Last updated:

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

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.

PurposeSchedulerEvents API
Titlebooking_info.titletitle
Start timebooking_info.start_timewhen.start_time
Timezonebooking_info.guest_timezonewhen.start_timezone
Attendeesbooking_info.participantsparticipants
Join linkbooking_info.locationconferencing.details.url
Organizernot exposed directlyorganizer.email
Calendar linkbooking_info.event_html_linkhtml_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.

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.objectFieldsUsed for
timespanstart_time, end_time, start_timezone, end_timezoneTimed meetings
datedateA single all-day event
datespanstart_date, end_dateA 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.

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.

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.