A scheduling agent that replies from a human’s inbox is a compromise. Responses come from the human’s address, their calendar fills with bookings the agent owns, and the agent has no identity of its own. This recipe gives the agent its own mailbox and calendar: meeting requests land at [email protected], an LLM parses them, the agent proposes times against its own free/busy, and creates events on its own calendar. No human mailbox in the loop.
The agent runs on a Nylas Agent Account — a Nylas-hosted mailbox and calendar you provision through the API. Every grant-scoped endpoint you’d normally hit against a connected user (list messages, send mail, create events, RSVP) works the same way against a grant you own.
The loop
Section titled “The loop”human emails [email protected] │message.created webhook ─▶ LLM parses intent (duration, timezone, urgency) │ ▼ agent queries own free/busy ─▶ replies with 3 candidate slots │human picks one ─▶ message.created webhook ─▶ agent creates event with notify_participants=true │ ▼ attendee accepts/declines/proposes new time │ event.updated webhook ─▶ agent confirms or offers alternativesBy 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).
Things to know
Section titled “Things to know”- 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 before you launch.
- 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 fastest 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. Require a human-in-the-loop confirmation 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.
- Don’t ship without thread continuity testing. The agent’s reply must preserve
Message-ID,In-Reply-To, andReferencesso the conversation threads in Gmail/Outlook. Send yourself a request and verify the reply lands in the original thread.
Next steps
Section titled “Next steps”- Support agent with multi-day threads — the same Agent Account pattern for support inboxes
- Handle email replies in an agent loop — the underlying reply-detection recipe
- Multi-turn email conversations — conversation state machine for agents
- Agent Accounts overview — product doc, limits, and architecture
- Supported endpoints for Agent Accounts — every grant-scoped endpoint and webhook the agent can use
- Policies, Rules, and Lists — spam tuning, send quotas, and inbound filtering
- Mail client access (IMAP & SMTP) — let humans log into the agent’s mailbox alongside the API