If you are building an AI agent that books meetings for sales, product workflows, or personal assistants, you’ve probably hit the same wall. A scheduling agent can draft an email and propose three times. It cannot create the event. The moment the user picks a slot, a human steps in, or the agent calls a second system that owns a calendar.
Nylas Agent Accounts close that gap. The same identity that proposes the times creates the event, sends the invite, and handles RSVPs. Here is how to build the loop.
Before you start, you’ll need an Agent Account on a registered domain and a webhook endpoint Nylas can reach. The quickstart sets both up in about 5 minutes.
Provision the account
Create a dedicated scheduling inbox by provisioning a Nylas Agent Account. Your backend makes one API call and gets back a grant ID. That grant ID works with every Nylas endpoint, MCP server, and CLI immediately.
The grant ID is at data.id. That is what you pass on every subsequent API call. The account’s primary calendar is provisioned automatically alongside the mailbox, so you can create events on it immediately.
To test the flow locally before wiring it into your backend, the CLI runs the same provisioning:
The agent’s flow runs on two event types: inbound messages to receive meeting requests and confirmation replies, and calendar activity to track RSVPs and reschedules.
Nylas sends a challenge GET to your endpoint when the webhook is created. Respond with the value of the challenge query parameter within 10 seconds to activate it. Every subsequent POST carries an X-Nylas-Signature header you should verify before processing.
One note: Nylas blocks requests to ngrok URLs. Use VS Code port forwarding or Hookdeck for local development.
The request-to-booking loop
When someone emails [email protected], Nylas fires message.created with the message object in data.object. The webhook payload carries summary fields; your handler fetches the full body to pass to the LLM. One note on field casing: webhook payloads arrive as raw JSON in snake_case (grant_id, thread_id), while values the SDK returns are camelCase (threadId, messageIds). Seeing both in one handler is expected, not a typo.
const crypto = require("crypto");const SCHEDULING_EMAIL = "[email protected]";// Raw body required so HMAC matches the exact bytes Nylas signsapp.use("/webhooks/nylas", express.raw({ type: "application/json" }));// Nylas sends a GET with ?challenge=... when the webhook is first activated.// Echo the value back within 10 seconds to verify the endpoint.app.get("/webhooks/nylas", (req, res) => res.status(200).send(req.query.challenge));app.post("/webhooks/nylas", async (req, res) => { // Verify the request is from Nylas const signature = req.headers["x-nylas-signature"]; const digest = crypto .createHmac("sha256", process.env.NYLAS_WEBHOOK_SECRET) .update(req.body) // raw Buffer, not parsed JSON .digest("hex"); if (!signature || signature.length !== digest.length || !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))) return res.status(401).end(); res.status(200).end(); // Acknowledge immediately, do work async const event = JSON.parse(req.body.toString()); if (event.type !== "message.created") return; const message = event.data.object; // Ignore messages from other grants if (message.grant_id !== AGENT_GRANT_ID) return; // Ignore the agent's own outbound messages if (message.from?.[0]?.email === SCHEDULING_EMAIL) return; const full = await nylas.messages.find({ identifier: AGENT_GRANT_ID, messageId: message.id, }); const parsed = await llm.parseMeetingRequest(full.data); if (parsed.intent !== "schedule_meeting") return; await proposeTimes(message, parsed);});
To propose times, check the agent’s availability against its own calendar using the free/busy endpoint, then hand the open slots to the LLM to draft a reply:
Nylas preserves the Message-ID, In-Reply-To, and References headers on outbound, so the reply lands in the original thread in Gmail, Outlook, or any other client. When the prospect replies with their chosen slot, that reply groups under the same thread ID and your handler picks it up with full context.
When the prospect confirms, create the event with notifyParticipants: true. Nylas sends an ICS REQUEST from the agent’s address and the recipient’s calendar client treats it as a standard invitation:
When the invitee accepts, declines, or proposes a new time, Nylas fires event.updated with the attendee status change. Your handler branches on the RSVP:
If the invitee proposes a different time, RSVP on the agent’s behalf with POST /v3/grants/{grant_id}/events/{id}/send-rsvp. The response goes out as a standard ICS REPLY visible to every participant.
Know when the invite lands
Creating the event is only half the job. An agent that can’t tell a delivered invite from a silent bounce is flying blind, and a meeting that never reached the prospect is worse than no meeting at all. The same webhook stream that carries inbound mail also carries deliverability signals for everything the agent sends.
Add message.send_success, message.send_failed, and message.bounce_detected to your triggers. send_success confirms the invite or reply actually left the building; send_failed and bounce_detected tell you the address is wrong or the mail was rejected, so the agent can fix the address or flag the booking for a human instead of waiting on a confirmation that will never come.