{"slug": "let-an-agent-own-the-invite-rsvp-reconciliation-with-agent-accounts", "title": "Let an agent own the invite: RSVP reconciliation with Agent Accounts", "summary": "Nylas introduced Agent Accounts that allow AI agents to act as meeting organizers with automatic RSVP reconciliation. The system uses iCalendar standards to send invites and automatically updates participant status via webhooks, eliminating the need for manual email parsing. This approach handles the complexity of cross-provider RSVP replies from Google Calendar, Outlook, and Apple Calendar.", "body_md": "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.\n\nThat 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`\n\nmessages 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.\n\nThis 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.\n\nAn **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.\n\nThe reconciliation loop has three moving parts, and Nylas owns two of them for you:\n\n`notify_participants=true`\n\n. Nylas sends an ICS `REQUEST`\n\nfrom the Agent Account's address to each attendee.`REPLY`\n\nback to the Agent Account's mailbox. Nylas reads it and updates that participant's `status`\n\non the event object to `yes`\n\n, `no`\n\n, `maybe`\n\n, or `noreply`\n\n. You don't parse anything.`event.updated`\n\nwebhook 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`\n\n, which Nylas keeps current. The [Agent Account calendars doc](https://developer.nylas.com/docs/v3/agent-accounts/calendars/) confirms this reconciliation behavior specifically for Agent Account grants: each response lands in the mailbox, Nylas reads it, and the event's `participants[].status`\n\nis updated automatically.\n\nIf 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:\n\n`REPLY`\n\nmessages are not consistent across providers.`METHOD:REPLY`\n\nblock is consistent, but now you're parsing MIME and `text/calendar`\n\nparts by hand.`status`\n\n. 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.\n\nYou need an Agent Account and an API key. If you don't have an account yet, the [Agent Accounts quickstart](https://developer.nylas.com/docs/v3/getting-started/agent-accounts/) walks through provisioning; the short version from the CLI is:\n\n```\nnylas agent account create scheduler@yourcompany.nylas.email --name \"Scheduling Agent\"\n```\n\nThe same thing over raw HTTP is a `POST /v3/connect/custom`\n\nwith `provider: \"nylas\"`\n\nand a `settings.email`\n\non a domain you've registered — no refresh token, no OAuth dance:\n\n```\ncurl --request POST \\\n  --url \"https://api.us.nylas.com/v3/connect/custom\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"provider\": \"nylas\",\n    \"name\": \"Scheduling Agent\",\n    \"settings\": { \"email\": \"scheduler@yourcompany.nylas.email\" }\n  }'\n```\n\nEither way you get a grant with a primary calendar already provisioned. Grab the `grant_id`\n\n— every endpoint below is grant-scoped at `/v3/grants/{grant_id}/...`\n\n, 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.\n\nFor the raw HTTP examples I'll use `https://api.us.nylas.com`\n\nand a bearer token:\n\n```\nexport NYLAS_API_KEY=\"<your-api-key>\"\nexport GRANT_ID=\"<agent-account-grant-id>\"\n```\n\nCreating an event with participants and `notify_participants=true`\n\nis what makes the Agent Account the organizer of record. Nylas sends the ICS `REQUEST`\n\nand the attendees see an invite from `scheduler@yourcompany.nylas.email`\n\n.\n\nHere's the API call. Note `notify_participants=true`\n\nis a query parameter, not a body field — this is the flag that turns \"save a private event\" into \"send invitations\":\n\n```\ncurl --request POST \\\n  --url \"https://api.us.nylas.com/v3/grants/$GRANT_ID/events?calendar_id=primary&notify_participants=true\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"title\": \"Product demo\",\n    \"when\": { \"start_time\": 1744387200, \"end_time\": 1744390800 },\n    \"participants\": [\n      { \"email\": \"alice@example.com\" },\n      { \"email\": \"bob@example.com\" }\n    ]\n  }'\n```\n\nThe response includes the new event's `id`\n\nand a `participants`\n\narray where each entry currently reads `\"status\": \"noreply\"`\n\n. Hold onto that event `id`\n\n— it's the key you'll read status back from and the key the webhook will reference.\n\nThe CLI equivalent uses `nylas calendar events create`\n\nwith one `--participant`\n\nflag per attendee:\n\n```\nnylas calendar events create \"$GRANT_ID\" \\\n  --calendar primary \\\n  --title \"Product demo\" \\\n  --start \"2026-07-15 10:00\" \\\n  --end \"2026-07-15 11:00\" \\\n  --participant \"alice@example.com\" \\\n  --participant \"bob@example.com\"\n```\n\nOne thing to call out honestly: as of CLI v3.1.27, `nylas calendar events create`\n\ndoesn't expose a `--notify-participants`\n\nflag — there's no such flag, so don't invent one. When you need to be explicit about the `notify_participants=true`\n\nquery parameter (for example to guarantee invites go out, or to *suppress* them with `false`\n\nfor 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.\n\nA note on invite quota: every create, update, and delete sent with `notify_participants=true`\n\ncounts 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.\n\nOnce invites are out, the event object becomes your live attendance record. As RSVP replies arrive, Nylas updates `participants[].status`\n\nin place. You read it back with a plain `GET`\n\non the event:\n\n```\ncurl --request GET \\\n  --url \"https://api.us.nylas.com/v3/grants/$GRANT_ID/events/<EVENT_ID>?calendar_id=primary\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\"\n```\n\nThe `participants`\n\narray now reflects reality — something like:\n\n```\n\"participants\": [\n  { \"email\": \"alice@example.com\", \"status\": \"yes\" },\n  { \"email\": \"bob@example.com\",   \"status\": \"noreply\" }\n]\n```\n\nAlice accepted; Bob hasn't answered. No email parsing happened on your side — Nylas folded Alice's ICS `REPLY`\n\ninto the event for you. The four values you'll see are `yes`\n\n, `no`\n\n, `maybe`\n\n, and `noreply`\n\n.\n\nFrom the CLI, `nylas calendar events show`\n\n(aliased as `read`\n\nand `get`\n\n) fetches the same event:\n\n```\nnylas calendar events show <EVENT_ID> \"$GRANT_ID\" --calendar primary --json\n```\n\nPipe that through `jq '.data.participants'`\n\nand 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.\n\nPolling 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.\n\nWhen a participant's status changes, Nylas fires an [ event.updated](https://developer.nylas.com/docs/reference/notifications/events/event-updated/) webhook for the Agent Account's calendar. The supported-endpoints reference lists\n\n`event.updated`\n\nexplicitly 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`\n\nyou filter on. So you create the subscription against `/v3/webhooks`\n\n, not against the grant:\n\n```\ncurl --request POST \\\n  --url \"https://api.us.nylas.com/v3/webhooks\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"trigger_types\": [\"event.updated\"],\n    \"webhook_url\": \"https://yourapp.com/webhooks/nylas\",\n    \"description\": \"RSVP reconciliation for the scheduling agent\"\n  }'\n```\n\nThe CLI mirrors this with `nylas webhook create`\n\n:\n\n```\nnylas webhook create \\\n  --url \"https://yourapp.com/webhooks/nylas\" \\\n  --triggers event.updated \\\n  --description \"RSVP reconciliation for the scheduling agent\"\n```\n\n`--triggers`\n\ntakes a comma-separated list or repeated flags, so in practice I subscribe to `event.created,event.updated,event.deleted`\n\ntogether 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.\n\nWhen `event.updated`\n\narrives, the payload carries the event id and `grant_id`\n\n. The handler shape is the same regardless of why the event changed:\n\n`X-Nylas-Signature`\n\n, 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`\n\ndoes this locally while you're testing.`id`\n\n`GET`\n\nfrom 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`\n\ncorrect. Everything you *do* with a status change — that's your application logic, and it lives in your code and your database.\n\nConcretely, the things teams usually want — \"nudge everyone still on `noreply`\n\n24 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:\n\n`metadata`\n\non events, so you can't stash this on the Nylas side — own the state yourself.`event.updated`\n\n, refetch, diff against your stored row, and emit whatever actions the delta implies (a reminder email via `nylas email send`\n\n, 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`\n\nhandles cleanly.\n\nReconciliation 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`\n\non the event sends an ICS update to every participant:\n\n```\ncurl --request PUT \\\n  --url \"https://api.us.nylas.com/v3/grants/$GRANT_ID/events/<EVENT_ID>?calendar_id=primary&notify_participants=true\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{ \"when\": { \"start_time\": 1744390800, \"end_time\": 1744394400 } }'\n```\n\nThe CLI equivalent is `nylas calendar events update`\n\n, which takes the event id and the fields you're changing:\n\n```\nnylas calendar events update <EVENT_ID> \"$GRANT_ID\" \\\n  --calendar primary \\\n  --start \"2026-07-15 11:00\" \\\n  --end \"2026-07-15 12:00\"\n```\n\nSame `--notify-participants`\n\ncaveat as create: the CLI flag doesn't exist in v3.1.27, so when you need the explicit `notify_participants=true`\n\nquery parameter on the update, use the API call.\n\nCancelling is a `DELETE`\n\n. With the Agent Account as organizer, it sends an ICS `CANCEL`\n\nto every participant when `notify_participants=true`\n\n:\n\n```\ncurl --request DELETE \\\n  --url \"https://api.us.nylas.com/v3/grants/$GRANT_ID/events/<EVENT_ID>?calendar_id=primary&notify_participants=true\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\"\n```\n\nFrom the CLI that's `nylas calendar events delete`\n\n, with `--force`\n\nto skip the confirmation prompt in a script:\n\n```\nnylas calendar events delete <EVENT_ID> \"$GRANT_ID\" --calendar primary --force\n```\n\nThe gotcha: deleting without notification leaves the meeting sitting on everyone's calendar. Cancel *with* notification unless you have a specific reason not to.\n\n`send-rsvp`\n\nfor the inverse case where the agent is the `nylas calendar events`\n\nand `nylas webhook`\n\nflag, 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`\n\ntell you the moment it changes. The reaction logic is yours; the reconciliation is handled.", "url": "https://wpnews.pro/news/let-an-agent-own-the-invite-rsvp-reconciliation-with-agent-accounts", "canonical_source": "https://dev.to/mqasimca/let-an-agent-own-the-invite-rsvp-reconciliation-with-agent-accounts-2b0m", "published_at": "2026-07-01 10:49:15+00:00", "updated_at": "2026-07-01 11:19:23.299126+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-products"], "entities": ["Nylas", "Google Calendar", "Outlook", "Apple Calendar", "Microsoft 365", "iCalendar"], "alternates": {"html": "https://wpnews.pro/news/let-an-agent-own-the-invite-rsvp-reconciliation-with-agent-accounts", "markdown": "https://wpnews.pro/news/let-an-agent-own-the-invite-rsvp-reconciliation-with-agent-accounts.md", "text": "https://wpnews.pro/news/let-an-agent-own-the-invite-rsvp-reconciliation-with-agent-accounts.txt", "jsonld": "https://wpnews.pro/news/let-an-agent-own-the-invite-rsvp-reconciliation-with-agent-accounts.jsonld"}}