Most "AI scheduling" demos point a model at a human's inbox, parse a few invite emails, and call it a day. That's fine until you want the agent to actually run the meeting β to be the organizer whose name is at the top of the invite, whose calendar is the source of truth, and who has to know, in real time, who's coming.
That last part is the interesting one. When your agent is the organizer, sending the invite is the easy 20%. The other 80% is reconciliation: someone accepts, someone declines, someone flips from "yes" to "maybe" the morning of, and the agent's internal picture of "who is attending this meeting" has to stay correct without you babysitting it. RSVP replies arrive over the next few hours and days as ICS REPLY
messages bouncing back from Google Calendar, Outlook, and Apple Calendar. If you're treating those as emails to parse, you've signed up for the worst job in calendaring.
This post is about doing it the boring, correct way: let the agent send the invite, let Nylas fold the incoming RSVP replies into the event's participant list, and react to a single webhook when status changes. I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I'm poking at this by hand before wiring it into app code.
An Agent Account is a Nylas grant with its own real mailbox and its own real calendar. From a participant's side there's nothing special about it β it shows up as a normal organizer on a normal invite. Under the hood it speaks standard iCalendar, so it interoperates with Google Calendar, Microsoft 365, and Apple Calendar as a first-class participant.
The reconciliation loop has three moving parts, and Nylas owns two of them for you:
notify_participants=true
. Nylas sends an ICS REQUEST
from the Agent Account's address to each attendee.REPLY
back to the Agent Account's mailbox. Nylas reads it and updates that participant's status
on the event object to yes
, no
, maybe
, or noreply
. You don't parse anything.event.updated
webhook fires for the Agent Account's calendar. That's your cue to recompute attendance and do whatever your app does with it.The conceptual pivot worth internalizing: the event object is your participant database. You don't reconstruct attendance from a pile of reply emails β you read it off participants[].status
, which Nylas keeps current. The Agent Account calendars doc confirms this reconciliation behavior specifically for Agent Account grants: each response lands in the mailbox, Nylas reads it, and the event's participants[].status
is updated automatically.
If you went the naive route β agent reads its inbox, finds the "Accepted: Product demo" email, regexes out who and what β you'd be reimplementing an iCalendar parser badly. A few reasons not to:
REPLY
messages are not consistent across providers.METHOD:REPLY
block is consistent, but now you're parsing MIME and text/calendar
parts by hand.status
. Re-deriving that from the raw email is strictly more work for a worse result.The honest tradeoff: you're trusting Nylas's reconciliation instead of owning the parse. For organizer-side RSVP tracking, that's the trade you want. If you needed full round-trip time negotiation β propose slots, collect picks, book the winner β that's a different problem, and one the Events API doesn't try to solve on its own.
You need an Agent Account and an API key. If you don't have an account yet, the Agent Accounts quickstart walks through provisioning; the short version from the CLI is:
nylas agent account create scheduler@yourcompany.nylas.email --name "Scheduling Agent"
The same thing over raw HTTP is a POST /v3/connect/custom
with provider: "nylas"
and a settings.email
on a domain you've registered β no refresh token, no OAuth dance:
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": "Scheduling Agent",
"settings": { "email": "scheduler@yourcompany.nylas.email" }
}'
Either way you get a grant with a primary calendar already provisioned. Grab the grant_id
β every endpoint below is grant-scoped at /v3/grants/{grant_id}/...
, which is the whole point of the grant abstraction: there's nothing new to learn on the data plane. An Agent Account hits the same Events endpoints as any connected Google or Microsoft grant.
For the raw HTTP examples I'll use https://api.us.nylas.com
and a bearer token:
export NYLAS_API_KEY="<your-api-key>"
export GRANT_ID="<agent-account-grant-id>"
Creating an event with participants and notify_participants=true
is what makes the Agent Account the organizer of record. Nylas sends the ICS REQUEST
and the attendees see an invite from scheduler@yourcompany.nylas.email
.
Here's the API call. Note notify_participants=true
is a query parameter, not a body field β this is the flag that turns "save a private event" into "send invitations":
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/$GRANT_ID/events?calendar_id=primary¬ify_participants=true" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"title": "Product demo",
"when": { "start_time": 1744387200, "end_time": 1744390800 },
"participants": [
{ "email": "alice@example.com" },
{ "email": "bob@example.com" }
]
}'
The response includes the new event's id
and a participants
array where each entry currently reads "status": "noreply"
. Hold onto that event id
β it's the key you'll read status back from and the key the webhook will reference.
The CLI equivalent uses nylas calendar events create
with one --participant
flag per attendee:
nylas calendar events create "$GRANT_ID" \
--calendar primary \
--title "Product demo" \
--start "2026-07-15 10:00" \
--end "2026-07-15 11:00" \
--participant "alice@example.com" \
--participant "bob@example.com"
One thing to call out honestly: as of CLI v3.1.27, nylas calendar events create
doesn't expose a --notify-participants
flag β there's no such flag, so don't invent one. When you need to be explicit about the notify_participants=true
query parameter (for example to guarantee invites go out, or to suppress them with false
for a silent backfill), reach for the API call. The CLI is the fast path for creating the event; the curl form is where you control the notification semantics precisely. I use the CLI to set things up interactively and the API in the actual service.
A note on invite quota: every create, update, and delete sent with notify_participants=true
counts against the Agent Account's daily send limit, because each invitation is an outbound email. If the account is over quota the event still saves but the invitation is skipped silently. Worth a guard in your app.
Once invites are out, the event object becomes your live attendance record. As RSVP replies arrive, Nylas updates participants[].status
in place. You read it back with a plain GET
on the event:
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"
The participants
array now reflects reality β something like:
"participants": [
{ "email": "alice@example.com", "status": "yes" },
{ "email": "bob@example.com", "status": "noreply" }
]
Alice accepted; Bob hasn't answered. No email parsing happened on your side β Nylas folded Alice's ICS REPLY
into the event for you. The four values you'll see are yes
, no
, maybe
, and noreply
.
From the CLI, nylas calendar events show
(aliased as read
and get
) fetches the same event:
nylas calendar events show <EVENT_ID> "$GRANT_ID" --calendar primary --json
Pipe that through jq '.data.participants'
and you've got the same status list in the terminal. This is genuinely the move I make first when something looks off β read the event, look at the statuses, trust the object before I trust anything I think I remember sending.
Polling this endpoint on a cadence is a legitimate pattern for batch jobs. But if you want to react the moment someone responds, polling is the wrong tool. That's what the webhook is for.
When a participant's status changes, Nylas fires an event.updated webhook for the Agent Account's calendar. The supported-endpoints reference lists
event.updated
explicitly as a trigger for Agent Account grants, so this is a supported path, not a hopeful one. This is what turns the loop from "the agent checks occasionally" into "the agent knows within seconds."One important detail about scope: webhooks are application-scoped, not grant-scoped. You subscribe once at the app level, and notifications for every grant in the app land at the same endpoint, each payload carrying the grant_id
you filter on. So you create the subscription against /v3/webhooks
, not against the grant:
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.updated"],
"webhook_url": "https://yourapp.com/webhooks/nylas",
"description": "RSVP reconciliation for the scheduling agent"
}'
The CLI mirrors this with nylas webhook create
:
nylas webhook create \
--url "https://yourapp.com/webhooks/nylas" \
--triggers event.updated \
--description "RSVP reconciliation for the scheduling agent"
--triggers
takes a comma-separated list or repeated flags, so in practice I subscribe to event.created,event.updated,event.deleted
together and branch in the handler. The part I like as an SRE: one subscription covers every Agent Account you ever provision in that app. You don't re-subscribe per agent.
When event.updated
arrives, the payload carries the event id and grant_id
. The handler shape is the same regardless of why the event changed:
X-Nylas-Signature
, a hex HMAC-SHA256 of the raw request body using your webhook secret. Compare it constant-time, and guard that both buffers are the same length first or the comparison throws on mismatch. nylas webhook verify
does this locally while you're testing.id
GET
from Step 2. Don't trust a status snapshot embedded in the payload β read the current participant list from the source of truth.Here's the boundary worth being precise about. Nylas keeps participants[].status
correct. Everything you do with a status change β that's your application logic, and it lives in your code and your database.
Concretely, the things teams usually want β "nudge everyone still on noreply
24 hours out," "alert the host if the key stakeholder declines," "auto-cancel if fewer than three people accept" β none of those are Nylas features. They're a function over the participant list that you run when the webhook fires. The pattern is:
metadata
on events, so you can't stash this on the Nylas side β own the state yourself.event.updated
, refetch, diff against your stored row, and emit whatever actions the delta implies (a reminder email via nylas email send
, a Slack ping, a row update).And to be clear about what's not on the table here: Scheduler isn't available for Agent Account grants, so this isn't a "let people pick a slot" flow. The agent already knows the time. Its job is to send the invite and keep an accurate, real-time picture of who's in β which the Events API plus event.updated
handles cleanly.
Reconciliation isn't only inbound. When the agent changes the meeting β say it pushes the start time, or swaps the participant list to nudge non-responders β those changes have to propagate too. A PUT
on the event sends an ICS update to every participant:
curl --request PUT \
--url "https://api.us.nylas.com/v3/grants/$GRANT_ID/events/<EVENT_ID>?calendar_id=primary¬ify_participants=true" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
--header "Content-Type: application/json" \
--data '{ "when": { "start_time": 1744390800, "end_time": 1744394400 } }'
The CLI equivalent is nylas calendar events update
, which takes the event id and the fields you're changing:
nylas calendar events update <EVENT_ID> "$GRANT_ID" \
--calendar primary \
--start "2026-07-15 11:00" \
--end "2026-07-15 12:00"
Same --notify-participants
caveat as create: the CLI flag doesn't exist in v3.1.27, so when you need the explicit notify_participants=true
query parameter on the update, use the API call.
Cancelling is a DELETE
. With the Agent Account as organizer, it sends an ICS CANCEL
to every participant when notify_participants=true
:
curl --request DELETE \
--url "https://api.us.nylas.com/v3/grants/$GRANT_ID/events/<EVENT_ID>?calendar_id=primary¬ify_participants=true" \
--header "Authorization: Bearer $NYLAS_API_KEY"
From the CLI that's nylas calendar events delete
, with --force
to skip the confirmation prompt in a script:
nylas calendar events delete <EVENT_ID> "$GRANT_ID" --calendar primary --force
The gotcha: deleting without notification leaves the meeting sitting on everyone's calendar. Cancel with notification unless you have a specific reason not to.
send-rsvp
for the inverse case where the agent is the nylas calendar events
and nylas webhook
flag, verified against v3.1.27.The short version: when the agent owns the invite, don't parse reply emails β let Nylas reconcile RSVPs onto the event, read status off the object, and let event.updated
tell you the moment it changes. The reaction logic is yours; the reconciliation is handled.