After every meeting, someone has to write up what happened, pull out the action items, and email them to the rest of the team. It takes ten minutes on a good day and rarely happens at all on a busy one. The information ends up scattered across personal notes, or worse, lost entirely.
This tutorial builds a system that handles all of it automatically. Nylas Notetaker joins your meetings, records and transcribes them, and generates a summary with action items. When the recording is ready, your webhook handler picks it up, composes a follow-up email, and sends it to every attendee using the Nylas Email API. No manual note-taking, no forgotten follow-ups.
What you’ll build
Section titled “What you’ll build”The complete pipeline is webhook-driven and works like this:
- Notetaker joins a meeting and records the conversation with transcription, summary, and action item generation enabled.
- Nylas processes the recording after the meeting ends, generating a transcript, summary, and list of action items.
- Nylas fires a
notetaker.mediawebhook when the processed files are available. - Your webhook handler receives the notification, downloads the summary and action items, and fetches the meeting’s attendee list from the calendar event.
- Your handler composes and sends a follow-up email to all attendees with the summary and action items using the Nylas Email API.
The result: every meeting your Notetaker attends automatically produces a follow-up email within minutes of the call ending.
Before you begin
Section titled “Before you begin”Make sure you have the following before starting this tutorial:
- A Nylas account with an active application
- A valid API key from your Nylas Dashboard
- At least one connected grant (an authenticated user account) for the provider you want to work with
- Node.js 18+ or Python 3.8+ installed (depending on which code samples you follow)
You also need:
- A connected grant with calendar access so you can retrieve event attendees
- Notetaker enabled on your Nylas plan (check your Nylas Dashboard to confirm)
- A publicly accessible webhook endpoint that can receive POST requests from Nylas. During development, use VS Code port forwarding or Hookdeck to expose your local server.
Set up webhooks for Notetaker events
Section titled “Set up webhooks for Notetaker events”Your system needs to know when a recording is ready. Subscribe to the notetaker.media trigger so Nylas notifies your endpoint as soon as the transcript, summary, and action items are available. You should also subscribe to notetaker.meeting_state to track when Notetaker joins and leaves meetings.
Create the webhook subscription with a POST /v3/webhooks request:
curl --request POST \ --url 'https://api.us.nylas.com/v3/webhooks/' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --data '{ "trigger_types": [ "notetaker.media", "notetaker.meeting_state" ], "description": "Notetaker follow-up automation", "webhook_url": "https://your-server.com/webhooks/nylas", "notification_email_addresses": [ "your-team@example.com" ] }'Send Notetaker to a meeting
Section titled “Send Notetaker to a meeting”Invite Notetaker to a meeting by making a POST /v3/grants/<NYLAS_GRANT_ID>/notetakers request. Enable summary and action_items so Nylas generates the content your follow-up email needs.
curl --request POST \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/notetakers" \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "meeting_link": "https://meet.google.com/abc-defg-hij", "meeting_settings": { "video_recording": true, "audio_recording": true, "transcription": true, "summary": true, "action_items": true }, "name": "Meeting Notetaker" }'{ "request_id": "5fa64c92-e840-4357-86b9-2aa364d35b88", "data": { "id": "<NOTETAKER_ID>", "name": "Meeting Notetaker", "meeting_link": "https://meet.google.com/abc-defg-hij", "meeting_provider": "Google Meet", "state": "connecting", "meeting_settings": { "video_recording": true, "audio_recording": true, "transcription": true, "summary": true, "action_items": true } }}If you omit join_time, Notetaker attempts to join the meeting immediately. For scheduled meetings, include a Unix timestamp so Notetaker joins at the right time:
{ "join_time": 1732657774, "meeting_link": "https://teams.microsoft.com/l/meetup-join/...", "meeting_settings": { "summary": true, "action_items": true, "transcription": true, "audio_recording": true, "video_recording": true }, "name": "Meeting Notetaker"}You can also customize the AI output by passing instructions. For example, to get action items assigned to specific people:
{ "meeting_settings": { "summary": true, "action_items": true, "action_items_settings": { "custom_instructions": "Assign each action item to the person responsible and include a suggested deadline." }, "summary_settings": { "custom_instructions": "Focus on decisions made and open questions. Keep it under 200 words." } }}Handle the notetaker.media webhook
Section titled “Handle the notetaker.media webhook”When Notetaker finishes processing the recording, Nylas sends a notetaker.media webhook with the state available and URLs for each media file. Here is what that payload looks like:
{ "specversion": "1.0", "type": "notetaker.media", "source": "/nylas/notetaker", "id": "<WEBHOOK_ID>", "time": 1737500935555, "data": { "application_id": "<NYLAS_APPLICATION_ID>", "object": { "id": "<NOTETAKER_ID>", "grant_id": "<NYLAS_GRANT_ID>", "object": "notetaker", "meeting_settings": { "video_recording": true, "audio_recording": true, "transcription": true, "summary": true, "summary_settings": { "custom_instructions": "Focus on action items related to the product launch." }, "action_items": true, "action_items_settings": { "custom_instructions": "Group action items by team member." }, "leave_after_silence_seconds": 300 }, "meeting_provider": "Google Meet", "meeting_link": "https://meet.google.com/abc-defg-hij", "join_time": 1737500936450, "event": { "ical_uid": "<ICAL_UID>", "event_id": "<EVENT_ID>", "master_event_id": "<MASTER_EVENT_ID>" }, "status": "available", "state": "available", "media": { "recording": "<SIGNED_URL>", "recording_duration": "1800", "recording_file_format": "mp4", "thumbnail": "<SIGNED_URL>", "transcript": "<SIGNED_URL>", "summary": "<SIGNED_URL>", "action_items": "<SIGNED_URL>" } } }}The media object contains URLs for the recording, transcript, summary, and action_items. Your handler needs to check that state is available, download the summary and action items, then look up the meeting attendees.
Here is a Node.js Express handler that does all of this:
const express = require("express");const app = express();app.use(express.json());
const NYLAS_API_KEY = process.env.NYLAS_API_KEY;const NYLAS_GRANT_ID = process.env.NYLAS_GRANT_ID;const BASE_URL = "https://api.us.nylas.com/v3";
app.post("/webhooks/nylas", async (req, res) => { const { type, data } = req.body;
// Only process media notifications where files are ready if (type !== "notetaker.media" || data.object.state !== "available") { return res.status(200).send("OK"); }
const { media } = data.object; const notetakerId = data.object.id;
try { // Download the summary and action items const [summaryRes, actionItemsRes] = await Promise.all([ fetch(media.summary), fetch(media.action_items), ]);
const summary = await summaryRes.json(); const actionItems = await actionItemsRes.json();
// Get the Notetaker details to find the linked event const notetakerRes = await fetch( `${BASE_URL}/grants/${NYLAS_GRANT_ID}/notetakers/${notetakerId}`, { headers: { Authorization: `Bearer ${NYLAS_API_KEY}` } }, ); const notetaker = await notetakerRes.json(); const eventId = notetaker.data.event?.event_id; const calendarId = notetaker.data.calendar_id;
if (!eventId || !calendarId) { console.log("No linked calendar event found. Skipping follow-up."); return res.status(200).send("OK"); }
// Fetch the calendar event to get attendees const eventRes = await fetch( `${BASE_URL}/grants/${NYLAS_GRANT_ID}/events/${eventId}?calendar_id=${calendarId}`, { headers: { Authorization: `Bearer ${NYLAS_API_KEY}` } }, ); const event = await eventRes.json(); const attendees = event.data.participants || []; const meetingTitle = event.data.title || "Meeting";
// Send the follow-up email await sendFollowUpEmail(meetingTitle, summary, actionItems, attendees);
res.status(200).send("OK"); } catch (error) { console.error("Error processing notetaker media:", error); res.status(500).send("Error processing webhook"); }});
app.listen(3000, () => console.log("Webhook server running on port 3000"));Compose and send the follow-up email
Section titled “Compose and send the follow-up email”With the summary, action items, and attendee list in hand, build the follow-up email and send it through the Nylas Email API.
Build the email body
Section titled “Build the email body”Format the summary and action items into an HTML email body. Keep the formatting clean since attendees will read this on a variety of email clients.
function buildEmailBody(meetingTitle, summary, actionItems) { const actionItemsHtml = Array.isArray(actionItems) ? actionItems.map((item) => `<li>${item}</li>`).join("\n") : `<li>${actionItems}</li>`;
return ` <html> <body style="font-family: sans-serif; line-height: 1.6; color: #333;"> <p>Hi everyone,</p> <p>Here is a summary from today's meeting: <strong>${meetingTitle}</strong>.</p>
<h2 style="color: #1a73e8;">Meeting summary</h2> <p>${summary}</p>
<h2 style="color: #1a73e8;">Action items</h2> <ul> ${actionItemsHtml} </ul>
<hr style="border: none; border-top: 1px solid #ddd; margin: 24px 0;" /> <p style="color: #888; font-size: 12px;"> This follow-up was generated automatically by Nylas Notetaker. </p> </body> </html> `;}Send the email
Section titled “Send the email”Use the POST /v3/grants/<NYLAS_GRANT_ID>/messages/send endpoint to deliver the follow-up to all attendees.
Here is the curl version:
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "subject": "Follow-up: Weekly Sync - Summary & Action Items", "body": "<html><body><p>Hi everyone,</p><h2>Meeting summary</h2><p>The team discussed Q1 progress and assigned tasks for the upcoming sprint.</p><h2>Action items</h2><ul><li>Finalize the API integration by Friday</li><li>Schedule a design review for next week</li></ul></body></html>", "to": [ {"name": "Jordan Lee", "email": "jordan@example.com"}, {"name": "Alex Chen", "email": "alex@example.com"} ] }'And the complete Node.js function that ties into the webhook handler from the previous section:
async function sendFollowUpEmail( meetingTitle, summary, actionItems, attendees,) { const body = buildEmailBody(meetingTitle, summary, actionItems);
// Format attendees for the Nylas Email API const to = attendees.map((attendee) => ({ name: attendee.name || attendee.email, email: attendee.email, }));
const response = await fetch( `${BASE_URL}/grants/${NYLAS_GRANT_ID}/messages/send`, { method: "POST", headers: { Authorization: `Bearer ${NYLAS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ subject: `Follow-up: ${meetingTitle} - Summary & Action Items`, body: body, to: to, }), }, );
if (!response.ok) { throw new Error(`Failed to send email: ${response.status}`); }
const result = await response.json(); console.log(`Follow-up email sent. Message ID: ${result.data.id}`); return result;}Automate with calendar sync
Section titled “Automate with calendar sync”Manually sending Notetaker to each meeting works, but the real value comes from full automation. Nylas supports calendar sync rules that automatically schedule a Notetaker for meetings that match your criteria.
For example, to have Notetaker auto-join all external meetings with three or more participants:
curl --request PUT \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/calendars/<CALENDAR_ID>' \ --header 'Accept: application/json, application/gzip' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "notetaker": { "meeting_settings": { "summary": true, "action_items": true, "transcription": true, "audio_recording": true, "video_recording": true }, "name": "Meeting Notetaker", "rules": { "event_selection": ["external"], "participant_filter": { "participants_gte": 3 } } } }'With calendar sync enabled, the entire pipeline runs hands-free. Notetaker joins qualifying meetings automatically, and your webhook handler sends the follow-up email when the recording is processed. No manual step required.
Things to know
Section titled “Things to know”A few practical details to keep in mind when building this system:
-
Lobby and waiting rooms require manual admission. Notetaker is treated as a non-signed-in user by meeting platforms. If the meeting has a lobby or waiting room enabled, someone needs to admit the bot. If nobody admits it within 10 minutes, Notetaker times out and reports a
failed_entrystate. For fully automated workflows, configure your meeting provider to allow Notetaker to bypass the lobby. -
Processing takes a few minutes. After Notetaker leaves a meeting, Nylas needs time to process the recording into a transcript, summary, and action items. Expect a delay of a few minutes between the meeting ending and the
notetaker.mediawebhook arriving. Your follow-up emails will not be instant, but they will typically arrive well before anyone would have written them manually. -
Silence detection ends recordings automatically. By default, Notetaker leaves a meeting after 5 minutes of continuous silence. This prevents the bot from lingering in dead calls. You can adjust this threshold with
leave_after_silence_seconds(between 10 and 3600 seconds) in your meeting settings. -
Skip cancelled and declined meetings. Before sending a follow-up, check that the calendar event was not cancelled. If you are using calendar sync, Nylas handles this for you by cancelling the Notetaker when an event is removed. If you are scheduling Notetaker manually, add a check in your webhook handler to verify the event status before sending.
-
Every POST creates a new Notetaker bot. Nylas does not de-duplicate requests. If your code retries a failed
POST /v3/grants/<NYLAS_GRANT_ID>/notetakersrequest, you could end up with multiple bots in the same meeting. Use idempotency checks on your side to avoid duplicates. -
Media URLs expire after 60 minutes. The URLs in the
notetaker.mediawebhook payload are temporary. Download the summary and action items immediately when you receive the webhook. If you need to re-access files later, use the Download Notetaker Media endpoint. -
Nylas stores media files for 14 days. After 14 days, recordings, transcripts, summaries, and action items are permanently deleted. If you need to retain them longer, download and store the files in your own infrastructure.
What’s next
Section titled “What’s next”- Handling Notetaker media files for details on transcript formats, recording specs, and download strategies
- Using calendar sync with Notetaker to automatically schedule Notetaker for meetings matching your rules
- Scheduler and Notetaker integration to add Notetaker to meetings booked through Nylas Scheduler
- Webhook notification schemas for the full reference on
notetaker.media,notetaker.meeting_state, and other trigger payloads