DEV Community

Qasim
Qasim

Posted on

Distribute meeting notes from an agent's calendar

Notes are useless if they sit in a doc nobody opens. You've seen it: the meeting ends, someone pastes a summary into a shared doc, drops the link in a channel, and three days later the action items are still unassigned because nobody scrolled to the bottom. The information existed. It just never reached the people who needed it, in the place they actually read.

Most "AI meeting notes" demos solve the capture problem and stop there. They join the call, transcribe it, generate a tidy summary — and then hand you a URL. The distribution is left as an exercise for the reader. That's the part I want to fix here, and the interesting move is to make distribution a calendar-triggered job instead of a manual one.

The pattern: when an event on an agent's calendar ends, read the event's participant list, and email a recap to each attendee — sent from the agent's own address so any reply threads straight back into a real mailbox the agent already owns. No shared doc, no "find the link," no human in the loop.

A quick honesty note up front, because it changes the design: this post deliberately does not use Nylas Notetaker. Notetaker isn't supported for Agent Accounts. So the agent doesn't get the notes from Nylas — the notes are your application data (whatever your own pipeline produced: an LLM summary, structured action items, a CRM export). Nylas's job here is narrow and reliable: tell you who was in the meeting, and put the email in their inbox from an address that can receive replies. That's it. We lean on two primitives — Events and Send — and nothing else.

What you get

  • The participant list, straight from the event. No scraping the invite email. The event object already carries participants[], each with an email and an RSVP status.
  • A real sender identity. The recap comes from notes-bot@yourcompany.com (the agent), not a no-reply relay. Someone hits Reply, asks a question, and it lands in a mailbox the agent reads.
  • A trigger tied to the meeting, not to a human remembering. When the event's end time passes, the recap goes out.
  • Nothing new on the data plane. An Agent Account is just a grant with a grant_id. Every endpoint below is the ordinary grant-scoped Events and Messages API you'd use for any connected account. There's nothing agent-specific to learn — the agent is a grant.

Why this beats pasting a link in a channel

Channels are lossy. The recap competes with forty other messages, gets collapsed, and is unsearchable to anyone who joined the project late. Email to the exact attendee list is addressed, durable, and — because it comes from the agent's real address — conversational. The recipient can reply with "wait, who owns item 2?" and that question reaches the agent.

It also sidesteps a permission headache. You're not trying to inject the agent into someone's human inbox or borrow a person's credentials to send "as" them. The agent is a participant with its own mailbox. It reads its own calendar, it sends from its own address. Clean trust boundary.

Before you begin

You need an Agent Account, which is a grant created on a registered domain. Two ways to make one.

The API call is POST /v3/connect/custom with "provider": "nylas" and a settings.email on a domain you've registered (a custom domain, or a *.nylas.email trial subdomain). The optional top-level name sets the display name. No refresh token, because there's no external provider to authenticate against.

curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": "nylas",
    "name": "Notes Bot",
    "settings": { "email": "notes-bot@yourcompany.com" }
  }'
Enter fullscreen mode Exit fullscreen mode

I work on the CLI, so the terminal commands below are the exact ones I reach for. The CLI equivalent is one line:

nylas agent account create notes-bot@yourcompany.com --name "Notes Bot"
Enter fullscreen mode Exit fullscreen mode

The API auto-creates a default workspace and policy for the account. There's no --workspace flag on create — if you want to attach a custom policy later, you do it separately with nylas workspace update <workspace-id> --policy-id <policy-id>. The response gives you a grant_id. That's the only identifier the rest of this flow needs.

Verify the CLI is wired to your project by listing the agent grants — these are just grants with provider: nylas, so the API form is a filtered GET /v3/grants:

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants?provider=nylas" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode
nylas agent account list
Enter fullscreen mode Exit fullscreen mode

Read the event and its participants

When you want to send a recap, you start from the event. Two situations:

  1. You already know the event ID (you created it, or stored it when the agent scheduled the meeting). Fetch it directly.
  2. You're sweeping the calendar for meetings that just ended. List events and filter.

Fetch one event by ID

GET /v3/grants/{grant_id}/events/{event_id} returns the event, including its when block and participants[]. The calendar_id query parameter is required — use primary for the agent's primary calendar.

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/events/<EVENT_ID>?calendar_id=primary" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode

The CLI verb is show (aliased to get/read). It takes the event ID and an optional grant ID, plus -c/--calendar:

nylas calendar events show <EVENT_ID> <GRANT_ID> --calendar primary --json
Enter fullscreen mode Exit fullscreen mode

The piece you care about in the response is the participant array and the end time:

{
  "data": {
    "id": "<EVENT_ID>",
    "title": "Q1 planning",
    "when": {
      "object": "timespan",
      "start_time": 1744387200,
      "end_time": 1744390800
    },
    "participants": [
      { "email": "alice@example.com", "status": "yes" },
      { "email": "bob@example.com", "status": "yes" },
      { "email": "carol@example.com", "status": "noreply" }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

That's your distribution list, already deduplicated and already RSVP-aware. You can choose to skip the noreply and no participants if you only want to recap to people who actually showed up — status is right there on each entry. (One caveat worth stating: for Microsoft Graph events a participant's email can be missing, so guard for it before you build the to list.)

List events to find the ones that ended

If you're polling, list the calendar and look at each event's when.end_time.

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/events?calendar_id=primary&limit=50" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode
nylas calendar events list <GRANT_ID> --calendar primary --days 1 --limit 50 --json
Enter fullscreen mode Exit fullscreen mode

--days 1 keeps the window tight to recent meetings, and --limit auto-paginates past 200 if you need it. Each event in the list carries the same when and participants fields as the single-event fetch.

How you actually detect "the meeting ended"

This is the part people hand-wave, so let me be precise about it: there is no "event ended" webhook. Agent Account calendars emit event.created, event.updated, and event.deleted — those fire when an event is created, changed, or removed, not when its end time passes. The clock running out is not a state change, so nothing fires for it.

So the trigger is yours, and it's a comparison, not a notification: you compare the event's when.end_time (a Unix timestamp) to the current time, in your own scheduler. Two honest ways to do it:

  • Poll. Run a job every few minutes that lists recent events (the events list call above) and pick the ones where end_time < now and you haven't already recapped. Cheap, dead simple, and good enough for most calendars.
  • Schedule off the event lifecycle. Subscribe to event.created/event.updated at the app level, and when one arrives, read its end_time and enqueue a one-shot job for that timestamp (a delayed task, a cron entry, a setTimeout in a durable queue — whatever your stack uses). The webhook isn't the "ended" signal; it's just your cue to schedule the check. If the event later moves, event.updated fires and you reschedule.

Either way, you own the "it's over now" decision. Don't tell yourself Nylas pushes it to you — it doesn't, and a design that assumes a phantom webhook will silently never send.

Whichever you pick, keep a dedup record (the event ID plus its end_time) so a re-run or a retried job doesn't email everyone twice. If the meeting time changed and you already sent a recap for the old slot, that's a product decision: usually you don't re-send.

One more accuracy note on webhooks generally, since you'll likely subscribe to event.*: webhooks are application-scoped, not grant-scoped. You subscribe once at the app level with POST /v3/webhooks, and events for every grant arrive at that one endpoint, each payload carrying a grant_id you filter on. So you're not registering a webhook per agent — one subscription covers the whole fleet, and you route on grant_id.

The subscription itself is one call:

curl --request POST \
  --url "https://api.us.nylas.com/v3/webhooks" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "trigger_types": ["event.created", "event.updated"],
    "webhook_url": "https://your-server.com/webhooks/nylas",
    "description": "Recap scheduler triggers"
  }'
Enter fullscreen mode Exit fullscreen mode
nylas webhook create --url https://your-server.com/webhooks/nylas --triggers event.created,event.updated
Enter fullscreen mode Exit fullscreen mode

Email the recap from the agent's address

Now the payload. Remember: the notes are your data. Nylas didn't generate them — your summarization step did. You bring the summary and actionItems; Nylas delivers them.

Build the body however you like (HTML reads best across clients), set the recipient list from the participants you read earlier, and send with POST /v3/grants/{grant_id}/messages/send. The message goes out from the agent's own mailbox, which is the whole point — a reply comes back to an address the agent owns.

curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/send" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "subject": "Recap: Q1 planning - summary & action items",
    "body": "<html><body><p>Hi everyone,</p><h2>Summary</h2><p>We locked the Q1 roadmap and split ownership for the first sprint.</p><h2>Action items</h2><ul><li>Alice: finalize the API spec by Friday</li><li>Bob: book the design review</li></ul></body></html>",
    "to": [
      { "email": "alice@example.com" },
      { "email": "bob@example.com" }
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

The CLI sends the same message. --to accepts a repeatable list, so you fan it out to every participant:

nylas email send <GRANT_ID> \
  --to alice@example.com \
  --to bob@example.com \
  --subject "Recap: Q1 planning - summary & action items" \
  --body "<html><body><p>Hi everyone,</p><h2>Summary</h2><p>We locked the Q1 roadmap...</p></body></html>"
Enter fullscreen mode Exit fullscreen mode

A design choice worth thinking about: one email to everyone, or one email per person?

  • One email, everyone on the to line is simplest and makes the thread a group conversation — a reply goes to the whole list. Good for a team that wants to discuss the action items together.
  • One email per participant lets you personalize ("Carol, your two items are...") and keeps replies private, but it's N sends instead of one. If you go this way, loop the events send/messages/send call per recipient.

Either way, because the sender is the agent's real address, replies thread back to the agent. That's the property you don't get from a no-reply relay or a "view in browser" link. If you want to read those replies, that's the ordinary thread API — GET /v3/grants/{grant_id}/threads/{thread_id} returns the thread and its messages:

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/threads/<THREAD_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode
nylas email threads show <THREAD_ID> <GRANT_ID>
Enter fullscreen mode Exit fullscreen mode

And the agent answers by sending with reply_to_message_id set to the message it's replying to, which keeps everything in the same thread:

curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/send" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "reply_to_message_id": "<MESSAGE_ID>",
    "body": "Good question — Bob owns item 2.",
    "to": [ { "email": "alice@example.com" } ]
  }'
Enter fullscreen mode Exit fullscreen mode
nylas email reply <MESSAGE_ID> <GRANT_ID> --body "Good question — Bob owns item 2."
Enter fullscreen mode Exit fullscreen mode

Putting the trigger and the send together

The loop, end to end, is small:

  1. Your scheduler wakes up (poll) or a scheduled job fires (lifecycle-driven).
  2. Read the event: nylas calendar events show <EVENT_ID> <GRANT_ID> --calendar primary --json (or the GET curl).
  3. Check when.end_time < now and that you haven't recapped this event yet.
  4. Build the to list from participants[], dropping entries without an email and (optionally) anyone whose status is no.
  5. Attach your summary and action items.
  6. Send from the agent: nylas email send <GRANT_ID> --to ... --subject ... --body ... (or the messages/send curl).
  7. Record the event ID as recapped.

No Notetaker, no shared doc, no human remembering to follow up. The meeting's own end time is the trigger, and the agent's own mailbox is the sender.

Guardrails and gotchas

  • The trigger is yours — own it. There's no event-ended webhook. If your poll job dies, recaps silently stop. Alert on "events that ended more than 15 minutes ago with no recap sent" so a stuck scheduler is visible, not invisible.
  • Dedup hard. Both poll and scheduled-job designs can fire twice (a retry, an overlapping cron, an event.updated that reschedules). Key your "already sent" record on event ID, and decide your policy if end_time changed after you sent.
  • Sends count against the account's daily quota. On the free plan that's 200 messages per account per day. One email per participant burns through that faster than one group email — for a busy agent recapping large meetings, the per-recipient pattern adds up. Pick the fan-out strategy with the quota in mind.
  • Guard for missing participant emails. For Microsoft Graph events the email field can be absent on a participant. Filter those out before building to, or the send fails.
  • A new domain warms over ~4 weeks. If you spin up a fresh sending domain for the agent, deliverability ramps up gradually — early recaps may be more likely to land in spam until the domain is warm. A *.nylas.email trial subdomain is fine for testing the flow before you commit a custom domain.
  • The notes are your responsibility, not Nylas's. Worth repeating because it's the design's load-bearing assumption: Notetaker is unsupported for Agent Accounts, so nothing in this pipeline gives you a transcript or summary. You generate that content. Nylas tells you the attendees and delivers the mail.

What's next

  • How Agent Account calendars work — the full picture of an agent hosting, receiving, and RSVPing to events, all on the same grant-scoped Events API.
  • Agent Accounts overview — what a grant-as-agent is and what it can do across Messages, Calendars, and Webhooks.
  • Automate meeting follow-up emails — the connected-account version of this flow, which does use Notetaker to generate the notes. A useful contrast: same distribution idea, different capture source.
  • Nylas CLI commands — every nylas agent, nylas calendar events, and nylas email subcommand used above.

AI-answer pages for agents

When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com:

Top comments (0)