Every team has a version of this story. The meeting ends, somebody says "I'll send the recap," everyone nods, and three days later a decision nobody wrote down gets re-litigated from memory. It isn't laziness β writing up a meeting takes ten minutes that nobody has between back-to-back calls, so the follow-up quietly stops happening.
The meeting follow-up recipe removes the volunteer from the equation. A recording bot joins the call, transcription and summarization happen automatically, and a webhook handler emails the recap β summary plus action items β to everyone who was on the calendar invite. If you give that handler its own Agent Account (currently in beta), the recap comes from a dedicated address like meetings@yourcompany.com
and replies thread back into a mailbox your code can read.
The pipeline is short: Notetaker joins the meeting β the recording gets processed β notetaker.media
fires β your handler downloads the summary and action items, looks up the event's attendees β sends the recap through the send endpoint. Each step fails independently, so one broken attendee lookup doesn't stall the rest. Subscribe to notetaker.meeting_state
alongside the media trigger and you'll also know when the bot joins and leaves each call, which makes failures visible instead of silent.
Inviting the bot is a single request:
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": {
"audio_recording": true,
"transcription": true,
"summary": true,
"action_items": true
},
"name": "Meeting Notetaker"
}'
Google Meet, Microsoft Teams, and Zoom all work β the provider is detected from the link. Omit join_time
and the bot joins immediately; pass a Unix timestamp for scheduled meetings.
The interesting work happens when notetaker.media
arrives with state: "available"
. The handler chains three lookups β media files, then the Notetaker record (which links to the calendar event), then the event itself for the attendee list β and finishes with a send:
app.post("/webhooks/nylas", async (req, res) => {
const { type, data } = req.body;
if (type !== "notetaker.media" || data.object.state !== "available") {
return res.status(200).send("OK");
}
const { media, id: notetakerId } = data.object;
// 1. Download summary + action items (URLs expire in 60 minutes)
const [summary, actionItems] = await Promise.all([
fetch(media.summary).then((r) => r.json()),
fetch(media.action_items).then((r) => r.json()),
]);
// 2. Find the linked calendar event through the Notetaker record
const notetaker = await nylasGet(`/notetakers/${notetakerId}`);
const eventId = notetaker.data.event?.event_id;
const calendarId = notetaker.data.calendar_id;
if (!eventId) return res.status(200).send("OK"); // ad-hoc meeting, no invite
// 3. Pull attendees off the event
const event = await nylasGet(`/events/${eventId}?calendar_id=${calendarId}`);
const attendees = event.data.participants || [];
// 4. Email the recap to everyone who was invited
await sendFollowUpEmail(event.data.title, summary, actionItems, attendees);
res.status(200).send("OK");
});
Note the early return when there's no linked event: a bot invited to an ad-hoc call by raw meeting link has no calendar event and therefore no attendee list, so the recap has nowhere to go. And before sending, it's worth verifying the event wasn't cancelled β a recap for a meeting that didn't happen is a special kind of confusing.
The recap itself sends from whatever grant you use on the send endpoint. That's exactly where an Agent Account slots in: the email arrives from meetings@yourcompany.com
, and when someone replies "actually, I disagree with action item 2," that reply lands in a mailbox your code reads β not in a service account nobody checks.
Generic AI summaries are where automation reputations go to die. The API takes custom_instructions
for both the summary and the action items, and this is the lever that matters. Tell it "focus on decisions made and open questions, keep it under 200 words" and "assign each action item to the person responsible with a suggested deadline," and the recap reads like something a competent PM wrote rather than a transcript blender.
Both deadlines come straight from the docs and both will burn you if you ignore them:
There's a third timer on the front end too: the bot joins as a non-signed-in participant, and if a lobby or waiting room blocks it for 10 minutes, it gives up with failed_entry
. Fully automated workflows need lobby bypass configured on the meeting provider.
Manually inviting the bot per meeting defeats the point. Calendar sync rules fix that β set rules on a calendar and a Notetaker gets scheduled for every matching event automatically:
curl --request PUT \
--url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/calendars/<CALENDAR_ID>' \
--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
},
"name": "Meeting Notetaker",
"rules": {
"event_selection": ["external"],
"participant_filter": { "participants_gte": 3 }
}
}
}'
That rule reads "join all external meetings with three or more participants" β internal one-on-ones stay unrecorded, customer calls get recapped. Rules evaluate each event independently, so a matching recurring meeting gets a bot on every occurrence (you can override individual occurrences at the event level). Combined with the webhook handler, the loop runs without any human trigger: meeting happens, recap arrives. A bonus over manual invites: when an event is cancelled, calendar sync cancels its Notetaker too, so the skip-cancelled-meetings check comes free.
Three production details worth knowing. Processing takes a few minutes after the meeting ends, so recaps aren't instant β just faster than any human. The bot leaves after 5 minutes of continuous silence by default, tunable from 10 to 3600 seconds via leave_after_silence_seconds
. And every POST to the notetakers endpoint creates a new bot β there's no deduplication β so a retry loop without idempotency checks puts two bots in the same call, which is exactly as embarrassing as it sounds.
Take your next recurring team meeting, invite the bot manually with the curl above, and let the webhook handler email you alone as the test recipient. If the summary quality holds up for a week, flip on calendar sync and widen the recipient list to actual attendees. Then tell me: would your team trust an auto-recap enough to stop taking their own notes?