Build an AI scheduling agent for Google Calendar and Outlook
An AI scheduling agent reads a person's free time, offers a few slots, and books the one the caller picks on their Google Calendar or Outlook. Agents double-book for reasons that have little to do with the model: they check the wrong calendars, guess time zones, write without checking the slot again, and retry creates. With Pyas, your agent gets one set of calendar tools for both providers, scoped to the signed-in user, with idempotent writes and a read-only mode you can run against the sandbox first.
What your app does, step by step
- 09:00
Connect the user's calendar
Add PyasConnect to your app (
@pyas/connect-react, Vue, Angular or a plain HTML element) and give it a session token from your server. The user id you return is saved on the account assubjectId, so the agent can later act for that user only.import { Pyas } from '@pyas/sdk' const pyas = new Pyas({ apiKey: process.env.PYAS_API_KEY }) // PyasConnect calls this for a session token. // The id you return becomes the account's subjectId. app.post('/api/pyas-session', pyas.connect.sessionHandler((req) => req.user.id)) - 09:30
Give your model the calendar tools
@pyas/ai-toolshas the tools for the Vercel AI SDK, LangChain, OpenAI function calling and Anthropic tool use. Scope them to the signed-in user on your server, and tell the model the current time and the user's time zone.import { generateText } from 'ai' import { pyasTools } from '@pyas/ai-tools/vercel' const result = await generateText({ model, // your own model system: `Now: ${new Date().toISOString()}. User's time zone: ${user.timezone}.`, tools: pyasTools({ subjectId: user.id }), prompt: 'Find 30 minutes with Sam on Thursday afternoon', }) - 10:00
Find times that are free
meetingTimes.findreturns open slots across Google and Outlook accounts in one call, earliest first. Slot times are UTC;timezonesets where they line up on the hour or half hour.const slots = await pyas.meetingTimes.find({ accountIds: [hostAccountId], from: '2026-11-05T00:00:00Z', to: '2026-11-07T00:00:00Z', durationMinutes: 30, timezone: 'America/New_York', buffer: { before: 10, after: 10 }, minimumNoticeMinutes: 120, limit: 3, }) // slots[0].startsAt and slots[0].endsAt are UTC - 10:30
Check the slot again, then book it once
Nothing holds a slot while the caller decides, so read it again right before you write. Pass your booking id as the idempotency key, and a retried create returns the first event instead of making a second one.
const now = await pyas.freeBusy.query({ accountIds: [hostAccountId], from: slot.startsAt, to: slot.endsAt }) if (now.busy.length > 0) return offerNextSlot() // taken since you offered it const event = await pyas.events.create({ accountId: hostAccountId, title: 'Consultation', startsAt: slot.startsAt, endsAt: slot.endsAt, attendees: [{ email: caller.email, name: caller.name }], conferencing: { provider: 'meet' }, // 'teams' on Outlook }, { idempotencyKey: `booking-${booking.id}` }) - 11:00
Try the agent on the sandbox, read-only first
Test keys reach sample people with real-looking calendars, never a real one. Start the MCP server with
--read-onlyso the agent can look but not change anything, then allow writes once its answers are right and watch what it books arrive as webhooks.# Read-only: the agent can look, not change anything claude mcp add pyas --env PYAS_API_KEY=pyas_sk_test_... -- npx -y @pyas/mcp --read-only # Relay the sandbox's webhooks to your laptop, signed as in production npx @pyas/cli webhooks listen --forward http://localhost:3000/webhooks/pyas
What you use
- Agents and MCPThe Pyas MCP server and AI tools, for Claude, Cursor or your own agent.
- Free timeFree/busy, and the first times everyone has free inside their working hours.
- Calendar APIRead and write events and calendars on Google Calendar and Outlook, in one shape.
- SandboxSimulated Google, Outlook and Zoom accounts to build against before anyone connects.
- Meeting linksGoogle Meet, Microsoft Teams or Zoom on the event, from one field.
- Pyas AuthSign-in Google, Microsoft and Zoom have already verified, so you skip your own OAuth and app reviews.
Why agents double-book
Most reports look alike. A Vapi support thread is titled Google Calendar checkAvailability tool not preventing double bookings. On the n8n forum, a builder writes about "my VAPI AI double booking appointments via Google Calendar".
The model is rarely the cause. The tools around it are. Four mistakes account for most double bookings, and each one is fixable in your code.
The agent checks the wrong calendars
Google's free/busy call answers only for the calendars you name. Its items field is the "List of calendars and/or groups to query." If the person keeps appointments on a second calendar and the tool asks about primary alone, that time comes back free.
Errors look like free time. Google returns a per-calendar errors list when "computation for the calendar failed", with no busy times for that calendar. Microsoft's getSchedule lists delegated access for personal Microsoft accounts as "Not supported." A tool that swallows either error tells the model the person is free all day.
Decide for each user which calendars count as busy, query all of them, and treat a calendar you couldn't read as busy, not free. The free/busy guide covers both providers' calls and what each one counts as busy.
Time zones the model has to guess
A model doesn't know today's date or where the caller is unless you tell it. "Thursday at three" needs both.
The providers don't agree on defaults either. Google requires an offset on an event's dateTime "unless a time zone is explicitly specified in timeZone". Microsoft Graph's event resource says of start: "By default, the start time is in UTC." You get local times only if you send Prefer: outlook.timezone. An agent that reads a Graph time as local time reads the wrong hour back to the caller.
Put the current time and the user's IANA time zone in the system prompt. Make your tools take and return UTC timestamps ending in Z, and convert to local time only when the agent speaks or displays a time. Pyas gives every event startsAt and endsAt in UTC, plus startsAtLocal and an IANA timezone, whatever the provider stored. Test a booking in the week the clocks change, such as the first Sunday in November in the US.
Nothing stops a write into a busy slot
Neither Google's events.insert nor Microsoft Graph's create event has a parameter that refuses an overlapping event. The create succeeds whether the time is free or not, so the check is yours, and it has to be close to the write.
On a phone call, a minute can pass between the agent offering three times and the caller choosing one. Someone else can book the same slot on your website in that minute. Pyas has no lock either: two people can pick the same slot at once. Check that one slot again immediately before creating, and if it's gone, offer the next one.
Reschedules have the same race. When the agent moves an event, pass ifMatch: event.version so the update is refused if someone changed the event after the agent read it.
Retries create duplicates
Voice platforms and workflow tools retry calls that time out, and models sometimes call the same tool twice. Each repeated create is another event on the calendar.
The providers handle this differently. Google lets you choose an event's ID on insert: base32hex characters, 5 to 1024 long, "unique per calendar". Microsoft has transactionId, "A custom identifier specified by a client app for the server to avoid redundant POST operations in case of client retries to create the same event."
Pyas uses one rule for both. The SDK sends an Idempotency-Key on every write and reuses it on retries. Pass your booking id as the key and the API replays the first answer for 24 hours, so a retried create never makes a second event. The idempotency docs have the details.
Google and Outlook in one agent
Many agent templates cover only one of the two. Supporting both by hand means two free/busy calls with different answers (Google returns busy times only; getSchedule returns statuses and working hours), two event formats, and two sign-in setups.
With Pyas, the same tools work on either. meetingTimes.find takes Google and Outlook accounts in one call. Meeting links follow the calendar: Google Meet on Google, Teams on Outlook work accounts, and Zoom on either when the host's Zoom account is connected. Teams on a personal Outlook.com account is refused with an error that says why, so the agent can offer another option. Users connect through Pyas Auth, so you skip Google's OAuth review.
Let the agent act only through tools you control
The model should never choose whose calendar it touches. Resolve the user on your server and build the tools from that, as with pyasTools({ subjectId: user.id }). Don't take an account id from the conversation, and don't hand the model your API key or a generic HTTP tool.
Separate reading from writing. Each Pyas MCP tool is marked as reading, changing or deleting, and --read-only offers only the ones that change nothing. A receptionist that only answers "when is Dr. Lee free?" never needs delete_event. For the tools that do write, have the agent read back the time, in the caller's time zone, and the attendee before it creates anything.
Voice receptionists on Vapi, Retell or n8n
Pyas has no integration with Vapi, Retell, n8n or any voice platform. A voice or workflow tool calls Pyas through its own HTTP or function-calling features.
Point that tool at an endpoint on your own server rather than straight at Pyas. Your secret key stays on your server, and your code decides which calendar the call may read and keeps the answer short enough to speak.
// Called by your voice platform's HTTP tool. Map its payload to from and to.
app.post('/tools/:practiceId/find-times', async (req, res) => {
const practice = await practices.get(req.params.practiceId)
const slots = await pyas.meetingTimes.find({
accountIds: [practice.calendarAccountId],
from: req.body.from,
to: req.body.to,
durationMinutes: 30,
timezone: practice.timezone,
limit: 3,
})
res.json({ slots: slots.map((slot) => ({ startsAt: slot.startsAt, endsAt: slot.endsAt })) })
})When the agent creates the event, Google or Outlook sends the caller the calendar invitation. Any other confirmation or reminder, by email or text, comes from your app. Pyas also doesn't offer booking pages or round-robin; your agent decides who gets the meeting.
Test against the sandbox first
Every Pyas app has a sandbox: sample people with real-looking calendars and simulated Google, Outlook and Zoom accounts, reached with pyas_sk_test_... keys that never touch a real calendar. Run the conversations that break agents before a real customer does:
- "Next Friday", said on a Friday.
- A booking during the week clocks change.
- Two conversations picking the same slot at once.
- A create that times out and is retried.
- Teams requested on an Outlook.com account.
The MCP tools list_recent_requests, get_request and explain_error show what the agent actually sent and why a call failed, which is faster than reading the model's own account of what it did.
Questions
- Is there an MCP server for both Google Calendar and Outlook?
- Yes.
@pyas/mcpgives Claude, Cursor or any MCP client the same calendar tools for Google Calendar and Outlook, includingfind_meeting_times,get_free_busyandcreate_event. Google's own Calendar MCP server covers Google only and is in developer preview. The MCP guide compares the options. - How do I stop a Vapi or n8n agent from double-booking Google Calendar?
- Check every calendar that should count as busy, check the chosen slot again right before you create the event, and send your booking id as an idempotency key so a retry can't make a second event. Creating an event on Google or Outlook does not check for conflicts on its own.
- Does Pyas integrate with Vapi, Retell or n8n?
- Pyas has no integration with Vapi, Retell, n8n or any voice platform. A voice or workflow tool calls Pyas through its own HTTP or function-calling features.
- Can I give an AI agent read-only access to a calendar?
- Yes. Every tool on the Pyas MCP server is marked as reading, changing or deleting, and
--read-onlyoffers only the tools that change nothing. The agent can list events and find free times but can't create, move or delete anything. - Does Pyas include the AI model?
- No. Pyas runs no AI models; you bring your own. Pyas provides the calendar data and actions as MCP tools and as tools for the Vercel AI SDK, LangChain and LangGraph, OpenAI function calling and Anthropic tool use.
- Can an AI agent book meetings on Outlook work accounts?
- Yes, once the user connects. Microsoft's default consent policy can still require an administrator to approve calendar access for work accounts, with Pyas Auth too. See Need admin approval for what to tell your users.
Read next
Build it against the sandbox today
Every app gets sample people with real-looking calendars, so you can build this before anyone connects an account.