# Automate meeting follow-up emails

Source: https://developer.nylas.com/docs/cookbook/use-cases/act/automate-meeting-follow-ups/

After every meeting, someone has to write up what happened, pull the action items, and email them to the team. Ten minutes on a good day, doesn't happen at all on a busy one. The information ends up scattered across personal notes -- or lost entirely.

This recipe wires Notetaker, Calendar, and Email together so the recap sends itself. Notetaker joins the call, records and transcribes, and produces a summary plus action items. When the media is ready, your webhook handler downloads it, looks up the calendar event's attendees, and sends them the recap. No manual note-taking, no forgotten follow-ups.

## The pipeline

```
Notetaker joins meeting ─▶ Nylas processes recording ─▶ notetaker.media webhook fires
                                                                  │
                                                                  ▼
                                          Download summary + action items
                                                                  │
                                                                  ▼
                                          Look up attendees on the calendar event
                                                                  │
                                                                  ▼
                                          Send recap email via Email API
```

Five steps, one webhook handler. Each piece fails independently -- if attendee lookup breaks for one event, the rest of the pipeline keeps running.

## Before you begin


Make sure you have the following before starting this tutorial:

- A [Nylas account](https://dashboard-v3.nylas.com/) 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)

> **Info:** 
> **New to Nylas?** Start with the [quickstart guide](/docs/v3/getting-started/) 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](https://dashboard-v3.nylas.com/) to confirm)
- A publicly accessible **webhook endpoint** that can receive POST requests from Nylas. During development, use [VS Code port forwarding](https://code.visualstudio.com/docs/editor/port-forwarding) or [Hookdeck](https://hookdeck.com/) to expose your local server.

> **Warn:** 
> **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

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`](/docs/reference/api/webhook-notifications/post-webhook-destinations/) request:

```bash
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"
    ]
  }'
```

> **Info:** 
> **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](/docs/v3/notifications/) for details on handling the verification handshake.

## Send Notetaker to a meeting

Invite Notetaker to a meeting by making a [`POST /v3/grants/<NYLAS_GRANT_ID>/notetakers`](/docs/reference/api/notetaker/invite-notetaker/) request. Enable `summary` and `action_items` so Nylas generates the content your follow-up email needs.

```bash
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"
  }'
```

```json
{
  "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
    }
  }
}
```

> **Success:** 
> **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:

```json
{
  "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:

```json
{
  "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

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:

```json
{
  "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,
        "transcription_settings": {
          "expected_languages": ["en", "es"],
          "fallback_language": "en"
        },
        "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:

```js [mediaWebhook-Node.js]
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"));
```

> **Warn:** 
> **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](/docs/reference/api/notetaker/get-notetaker-media/) to get fresh URLs.

## 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

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.

```js [composeEmail-Node.js]
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

Use the [`POST /v3/grants/<NYLAS_GRANT_ID>/messages/send`](/docs/reference/api/messages/send-message/) endpoint to deliver the follow-up to all attendees.

Here is the curl version:

```bash
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:

```js [sendEmail-Node.js]
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;
}
```

> **Info:** 
> **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

Manually sending Notetaker to each meeting works, but the real value comes from full automation. Nylas supports [calendar sync rules](/docs/v3/notetaker/calendar-sync/) 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:

```bash
curl --request PUT \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/calendars/<CALENDAR_ID>' \
  --header 'Accept: application/json' \
  --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.

> **Info:** 
> **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](/docs/v3/notetaker/calendar-sync/) for the full set of rule options.

## 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_entry` state. 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.media` webhook 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>/notetakers`](/docs/reference/api/notetaker/invite-notetaker/) request, 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.media` webhook 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](/docs/reference/api/notetaker/get-notetaker-media/).

- **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.

## Next steps

- [Auto-log meeting notes to your CRM](/docs/cookbook/use-cases/sync/auto-log-meeting-notes-crm/) -- same pipeline, different destination
- [Scheduling with notetaking](/docs/cookbook/use-cases/build/scheduling-with-notetaking/) -- attach Notetaker to every meeting booked through Scheduler
- [Interview scheduling pipeline](/docs/cookbook/use-cases/build/interview-scheduling-pipeline/) -- round-robin assignment plus auto-recording for hiring
- [Handling Notetaker media files](/docs/v3/notetaker/media-handling/) -- transcript format, recording specs, download strategies
- [Using calendar sync with Notetaker](/docs/v3/notetaker/calendar-sync/) -- auto-schedule Notetaker for meetings that match your rules