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)
New to Nylas? Start with the quickstart guide to set up your app and connect a test account before continuing here.
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.
Nylas blocks requests to ngrok URLs because of throughput limiting concerns. Use VS Code port forwarding or Hookdeck instead.
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": [ ] }'Nylas sends a challenge request to your webhook URL during creation. Your endpoint must respond with the challenge query parameter value to verify ownership. See the webhooks documentation for details on handling the verification handshake.
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 } }}Notetaker supports Google Meet, Microsoft Teams, and Zoom. Pass any valid meeting link and Notetaker detects the provider automatically.
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"));Media URLs in notetaker.media webhooks expire after 60 minutes. Download the files as soon as you receive the notification. If you need to access them later, use the Download Notetaker Media endpoint to get fresh URLs.
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": "[email protected]"}, {"name": "Alex Chen", "email": "[email protected]"} ] }'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;}The grant you use to send the email must have email send permissions. The follow-up email is sent from the account associated with the grant, so make sure that grant belongs to the person (or service account) you want the email to come from.
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.
Calendar sync rules evaluate each event independently. If a recurring meeting matches your rules, Notetaker joins every occurrence. You can override individual occurrences by updating the event-level Notetaker settings. See the calendar sync documentation for the full set of rule options.
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