A scheduling agent that replies from a human’s inbox is useful, but it’s also a compromise — responses come from the human’s address, the calendar gets cluttered, and the agent has no identity of its own. This tutorial builds a scheduling agent that has its own email address and calendar: it receives meeting requests at [email protected], parses them with an LLM, proposes times against its own calendar, creates events on its own calendar, and tracks RSVPs — all without a human mailbox in the loop.
The agent runs on a Nylas Agent Account: a fully Nylas-hosted mailbox and calendar you provision through the API. Everything you’d normally do with a connected grant (list messages, send mail, create events, RSVP) works the same way — just against a grant you own.
What you’ll build
Section titled “What you’ll build”The complete loop is webhook-driven and works like this:
- Provision an Agent Account at
[email protected]via the BYO Auth endpoint. You get agrant_idyou address with every subsequent call. - Subscribe to webhooks for
message.createdand the event triggers so the agent reacts in real time. - Receive a meeting request — a human sends an email to the Agent Account’s address asking to meet.
- Parse the request with your LLM: extract duration, timezone, urgency, and any preferred times.
- Propose candidate times by querying the Agent Account’s free/busy calendar and replying with slots.
- Create the event once the human confirms a slot. Nylas sends an ICS
REQUESTfrom the agent’s address. - Track RSVPs and follow up by listening for
event.updatedand the attendee reply.
By the end, you’ll have a scheduling identity that coordinates meetings end-to-end without touching any human’s account.
Before you begin
Section titled “Before you begin”Make sure you have the following before starting this tutorial:
- A Nylas account 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)
You also need:
- A domain registered with Nylas — either a Nylas-provided
*.nylas.emailtrial subdomain for prototyping, or your own domain with MX + TXT records in place. See Provisioning and domains. - A publicly accessible HTTPS webhook endpoint. During development, use VS Code port forwarding or Hookdeck to expose your local server.
- Access to an LLM for parsing meeting requests and drafting replies. Any OpenAI-compatible API works — this guide keeps the LLM calls abstract so you can drop in Claude, GPT, Gemini, or a local model.
Step 1: Provision the Agent Account
Section titled “Step 1: Provision the Agent Account”The quickest path is the Nylas CLI:
Save the grant ID the CLI prints — that’s what you’ll use on every subsequent call. Or, if you’d rather use the API, the same thing through POST /v3/connect/custom with "provider": "nylas" (no OAuth refresh token required):
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", "settings": { "email": "[email protected]" } }'Either way, the agent’s primary calendar is provisioned automatically — no extra call is needed before you can create events on it.
Optionally attach a policy
Section titled “Optionally attach a policy”If you want to enforce send quotas, spam detection, or inbound rules on the agent, create a policy first and pass settings.policy_id on the grant. A scheduling agent typically has relaxed send limits but strict DNSBL + header-anomaly spam detection to keep auto-generated replies from flowing to garbage senders.
Step 2: Subscribe to webhooks
Section titled “Step 2: Subscribe to webhooks”The agent needs to hear about two kinds of events: new inbound messages (so it can respond to requests) and calendar activity on its own events (so it can follow up on RSVPs or reschedules).
From the Nylas CLI:
nylas webhook create \ --url https://youragent.example.com/webhooks/nylas \ --triggers "message.created,event.created,event.updated,event.deleted"Or through the API:
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": [ "message.created", "event.created", "event.updated", "event.deleted" ], "description": "Scheduling agent", "webhook_url": "https://youragent.example.com/webhooks/nylas", "notification_email_addresses": ["[email protected]"] }'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. See Using webhooks with Nylas for the signature-verification code your handler should run on every incoming POST.
Step 3: Handle an inbound meeting request
Section titled “Step 3: Handle an inbound meeting request”When a human emails [email protected] to ask for a meeting, Nylas fires message.created with the Message object in data.object. The webhook only carries summary fields — your handler fetches the full body from the API.
// Node.js / Express handler sketchapp.post("/webhooks/nylas", async (req, res) => { // (Verify X-Nylas-Signature here — see the webhooks guide.) res.status(200).end(); // Acknowledge immediately; do work async.
const event = req.body; if (event.type !== "message.created") return;
const message = event.data.object;
// Fetch the full message to get the body and headers. const full = await nylas.messages.find({ identifier: AGENT_GRANT_ID, messageId: message.id, });
// Hand to the LLM for intent extraction. const parsed = await llm.parseMeetingRequest(full.data);
if (parsed.intent !== "schedule_meeting") return;
// Propose times (next step). await proposeTimes(message, parsed);});Step 4: Propose candidate times
Section titled “Step 4: Propose candidate times”Ask the LLM for the requested duration and timezone, then check the agent’s availability for matching slots. The agent calls /calendars/free-busy against its own primary calendar.
async function proposeTimes(message, parsed) { const candidates = generateCandidateSlots(parsed.preferredWindow, parsed.durationMinutes);
const freeBusy = await nylas.calendars.getFreeBusy({ identifier: AGENT_GRANT_ID, requestBody: { startTime: Math.floor(parsed.preferredWindow.start.getTime() / 1000), endTime: Math.floor(parsed.preferredWindow.end.getTime() / 1000), }, });
const openSlots = candidates.filter( (slot) => !overlapsAnyBusyBlock(slot, freeBusy.data), );
const reply = await llm.draftProposal({ originalMessage: message, openSlots: openSlots.slice(0, 3), tone: "friendly, concise", });
await nylas.messages.send({ identifier: AGENT_GRANT_ID, requestBody: { replyToMessageId: message.id, to: message.from, subject: `Re: ${message.subject}`, body: reply, }, });}The recipient sees a normal reply from [email protected] — no relay footer, no sent-via branding. If they’ve emailed the agent before, the reply threads correctly because Nylas preserves the Message-ID, In-Reply-To, and References headers on outbound.
Step 5: Create the event when the human confirms
Section titled “Step 5: Create the event when the human confirms”When the human replies with their chosen slot, the agent receives another message.created webhook, parses it with the LLM to extract the selected time, and creates the event. Pass notify_participants=true so Nylas sends an ICS REQUEST from the agent’s address — the recipient’s calendar client (Google, Microsoft, Apple) displays it as a normal invitation.
async function createEvent(message, parsed) { const event = await nylas.events.create({ identifier: AGENT_GRANT_ID, queryParams: { calendarId: "primary", notifyParticipants: true, }, requestBody: { title: parsed.title, description: parsed.description, when: { startTime: parsed.startUnix, endTime: parsed.endUnix, }, participants: [{ email: message.from[0].email, name: message.from[0].name }], }, });
return event.data.id;}Nylas fires an event.created webhook on your endpoint as the event lands on the agent’s calendar.
Step 6: Track RSVPs and handle changes
Section titled “Step 6: Track RSVPs and handle changes”When the invitee accepts, declines, or proposes a new time, Nylas fires event.updated (with the attendee’s status change) on your webhook. Pair that with LLM-drafted follow-up logic to keep the loop closed:
if (event.type === "event.updated") { const changedEvent = event.data.object; const rsvp = changedEvent.participants.find( );
if (rsvp?.status === "declined") { // LLM-driven: offer alternative times automatically. await offerAlternatives(changedEvent); } else if (rsvp?.status === "yes") { // Confirm and queue reminders. await confirmBooking(changedEvent); }}If the invitee proposes a reschedule instead of accepting, you can RSVP on the agent’s behalf with /events/{id}/send-rsvp — yes, no, or maybe. The response goes out as a standard ICS REPLY visible to every participant.
Step 7: (Optional) Give humans access to the mailbox
Section titled “Step 7: (Optional) Give humans access to the mailbox”If your ops team needs to see what the agent is doing day-to-day — read the inbox, audit replies, intervene in edge cases — enable protocol access by setting app_password on the grant and having humans connect Outlook, Apple Mail, or Thunderbird. Every IMAP action syncs back to the API, so humans and the agent operate on the same mailbox. See Mail client access (IMAP & SMTP).
Keep in mind
Section titled “Keep in mind”- Rate limits are per grant. A busy scheduling agent can hit the default 100-message-per-day send cap on an Agent Account. Provision a policy with a higher cap if you expect volume.
- The agent doesn’t know what it doesn’t see. A meeting request that lands in junk won’t fire a
message.createdwebhook on the inbox — if you’re using rules to auto-route, confirm important senders aren’t caught by spam detection. The rule evaluations endpoint is the quickest way to audit. - LLM reliability matters more than latency here. Parse-time is forgiving (minutes, not milliseconds), but wrong intent extraction creates real calendar chaos. Add guardrails that require a human-in-the-loop confirmation step for first-time senders or high-value meetings.
- Use separate agents for separate roles. Sales outreach, customer support, and scheduling have very different send quotas, spam sensitivities, and reply patterns. Model each as its own Agent Account with its own policy rather than one catch-all.
What’s next
Section titled “What’s next”- Agent Accounts overview — the full product doc, including limits and architecture
- Supported endpoints for Agent Accounts — every grant-scoped endpoint and webhook the agent can use
- Policies, Rules, and Lists — set spam tuning, send quotas, and inbound filtering on the agent
- Mail client access (IMAP & SMTP) — let humans log into the agent’s mailbox alongside the API
- Using webhooks with Nylas — signature verification and retry handling for your handler
- Automate meeting follow-ups — related Notetaker-driven pattern you can compose with a scheduling agent