Skip to content
Skip to main content

How to build a scheduling agent with a dedicated identity

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.

The complete loop is webhook-driven and works like this:

  1. Provision an Agent Account at [email protected] via the BYO Auth endpoint. You get a grant_id you address with every subsequent call.
  2. Subscribe to webhooks for message.created and the event triggers so the agent reacts in real time.
  3. Receive a meeting request — a human sends an email to the Agent Account’s address asking to meet.
  4. Parse the request with your LLM: extract duration, timezone, urgency, and any preferred times.
  5. Propose candidate times by querying the Agent Account’s free/busy calendar and replying with slots.
  6. Create the event once the human confirms a slot. Nylas sends an ICS REQUEST from the agent’s address.
  7. Track RSVPs and follow up by listening for event.updated and the attendee reply.

By the end, you’ll have a scheduling identity that coordinates meetings end-to-end without touching any human’s account.

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.email trial 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.

The quickest path is the Nylas CLI:

nylas agent account create [email protected]

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):

Either way, the agent’s primary calendar is provisioned automatically — no extra call is needed before you can create events on it.

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.

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:

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.

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 sketch
app.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);
});

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),
emails: ["[email protected]"],
},
});
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.

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(
(p) => p.email !== "[email protected]",
);
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-rsvpyes, 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).

  • 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.created webhook 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.