How to Give Your AI Agent a Calendar
A step-by-step guide to giving an AI agent its own calendar with AgentMail: create the inbox, book meetings, answer invitations and act when meetings start, end or move.
- calendar
- ai-agents
- webhooks
- scheduling
- tutorial
TL;DR
- Every AgentMail inbox comes with its own calendar, so giving an agent a calendar starts with creating an inbox.
- The agent can book meetings, answer invitations and get a webhook when a meeting changes, starts or ends. A recurring event can also replace a cron job.
- Give the agent free/busy access to your own calendar, not read and write access.
An agent that books meetings needs somewhere to keep them. It has to know what it agreed to, notice when the other side moves a meeting, and do something when a meeting starts or ends. A calendar does all three.
The usual workarounds are giving the agent access to your own calendar, or setting up a Google account for each agent. The first hands the agent your whole schedule. The second costs a paid Workspace seat per agent. Google's push notifications also only tell you that something on a calendar changed. Nothing fires when a meeting starts, so you still run a scheduler.
This guide uses AgentMail, where every inbox comes with a calendar. The examples are in Python, and the TypeScript SDK has the same methods.
Why does an agent need its own calendar?
An agent needs its own calendar because the meetings it books belong to other people too, and they can move or cancel them at any time. A calendar keeps the agent's copy of each meeting in step with those changes, and tells the agent when a meeting moves, starts or ends.
A reminder or a cron timer belongs to the agent, and nobody else can change it. A meeting belongs to everyone on it. Once an agent books meetings and then acts on them, by sending a quote, a call link or a follow-up, it needs to hear about it when someone else moves one.
Our in-house sales agent, Jordan, ran into exactly this. It books demos with prospects and sends each one a custom quote 30 minutes before the call. Recently, one prospect moved their call to Friday. Jordan had set itself a timer when the demo was booked, nothing told the timer the meeting had moved, and the quote went out 30 minutes before a call that wasn't happening anymore.
Step 1: How do you give your agent a calendar?
Log into the AgentMail console and create an inbox for the agent, or create one with the SDK. The calendar is created with the inbox, at the same address, and there is nothing to turn on. In the console it sits in the inbox's Calendar tab.
Python
from agentmail import AgentMailfrom agentmail.inboxes import CreateInboxRequestclient = AgentMail() # reads AGENTMAIL_API_KEYinbox = client.inboxes.create(request=CreateInboxRequest(username="jordan"))inbox_id = inbox.inbox_id # jordan@agentmail.toIf your agent already has an AgentMail inbox, it already has a calendar, so skip to step 2.
Step 2: How do you set the calendar's time zone?
A new calendar starts in UTC. Events created without a time zone use the calendar's default, so set it to the zone your agent works in.
Python
client.inboxes.calendar.update(inbox_id, timezone="America/New_York")Event times are wall-clock times in an IANA time zone, the way people write meetings. A weekly 9:00 meeting stays at 9:00 local time across daylight saving changes.
Step 3: Which webhooks should the agent subscribe to?
Subscribe to the six calendar events, plus message.received so the agent sees invitation emails as they arrive.
Python
client.webhooks.create( url="https://your-agent.example.com/webhooks/agentmail", event_types=[ "message.received", "calendar.event.created", "calendar.event.updated", "calendar.event.deleted", "calendar.event.responded", "calendar.event.starting", "calendar.event.ending", ], inbox_ids=[inbox_id], client_id="jordan-calendar-webhook",)| Event | Sent when |
|---|---|
calendar.event.created | The agent creates an event, or an invitation it received lands on its calendar |
calendar.event.updated | An event changes. For one-off and recurring events, the payload includes previous, the old values of what changed |
calendar.event.deleted | An event is deleted |
calendar.event.responded | The agent answers an invitation, or someone answers one the agent sent |
calendar.event.starting and calendar.event.ending | An event starts or ends. Usually within a minute, never early, and not for an event cancelled before it starts |
Webhooks are delivered at least once, so deduplicate on event_id.
How does the agent book a meeting?
Create an event with attendees and send_invites=True. The inbox becomes the organizer, and each attendee gets a standard invitation that opens in Google Calendar, Outlook or Apple Calendar.
Python
created = client.inboxes.calendar.create_event( inbox_id, title="AgentMail demo with Acme", start="2026-11-12T14:00:00", end="2026-11-12T14:30:00", timezone="America/New_York", location="https://meet.example.com/acme-demo", attendees=[{"email": "jane@acme.com", "name": "Jane Doe"}], send_invites=True, client_id="demo-acme-2026-11-12", metadata={"crm_deal_id": "D-1042"},)client_id makes the request safe to retry. A retry with the same client_id returns the original event and doesn't send the invites again. metadata is for your own records. It comes back in every webhook for the event and is never sent to attendees, so your handler knows which deal a meeting belongs to without a lookup.
When Jane accepts, her calendar app emails a reply to the inbox, her status on the event changes to accepted, and the agent gets calendar.event.responded.
If the meeting has to move, update the event and pass send_invites=True again. Jane gets an updated invitation, and the start and end webhooks move to the new time. Send start and end together, even if only one changed.
Python
client.inboxes.calendar.update_event( inbox_id, created.event.event_id, start="2026-11-13T14:00:00", end="2026-11-13T14:30:00", send_invites=True,)To call it off, delete the event with send_invites=True and every attendee gets a cancellation. Without send_invites, a change stays on the agent's calendar and attendees keep the old version.
Invitations count toward the inbox's normal send limits, one per attendee.
How does the agent answer invitations?
When someone invites the agent's address to a meeting, the event goes on its calendar automatically. The message.received webhook for that email waits until the event is on the calendar, so the message already carries calendar_event_id, and the agent can answer straight from the webhook.
Python
def on_message_received(payload): message = payload["message"] event_id = message.get("calendar_event_id") if event_id and organizer_is_trusted(message): client.inboxes.calendar.respond_to_event( inbox_id, event_id, status="accepted", # or "declined", "tentative" )organizer_is_trusted is yours to write. Most agents should accept invitations from people they already work with and leave the rest for a human.
To catch anything the agent hasn't answered, check the agenda for events still marked needs_action:
Python
agenda = client.inboxes.calendar.get_agenda(inbox_id)waiting = [e for e in agenda.events if e.response_status == "needs_action"]An invitation the agent received belongs to whoever sent it. The agent can edit its own copy, for example to add metadata, but those edits aren't sent to anyone. To move the meeting, the agent has to email the organizer and ask.
How does the agent act when a meeting starts, ends or moves?
Handle the start and end webhooks for anything that should happen at the meeting itself, like sending the call link or a follow-up.
Python
def on_calendar_event(payload): event = payload["calendar_event"] kind = payload["event_type"] if kind == "calendar.event.starting": send_call_link(event) elif kind == "calendar.event.ending": draft_follow_up(event) elif kind == "calendar.event.updated" and "start" in payload.get("previous", {}): reschedule_pre_meeting_work(event["event_id"], event["start_at"])The last branch is the fix for Jordan. Start webhooks never fire early, so anything that has to happen before a meeting, like a quote 30 minutes ahead, still needs its own timer. Key that timer to the event, and when calendar.event.updated arrives with a changed start, move the timer to the new start_at. Changes to a single date of a recurring meeting arrive without previous, so for those, compare start_at with the time you stored.
Can the calendar run the agent's recurring jobs?
An agent that does something every week usually runs on a cron job, from a hosted service or written by hand. Writing one is more work than it looks. A coding challenge that asks developers to build an agent scheduler lists parsing cron expressions, keeping runs from drifting or getting missed, retrying failures with backoff, and saving schedules so they survive a restart.
A recurring event does the same job. On Hacker News, one developer complained that agent products offer "only toy-level configuration" for scheduled work and wished they had the recurrence options of a calendar app. A calendar event has those options already, because it uses the same RRULE format calendar apps use.
Python
client.inboxes.calendar.create_event( inbox_id, title="Weekly pipeline summary", start="2026-11-09T09:00:00", # a Monday end="2026-11-09T09:15:00", timezone="America/New_York", recurrence={"rule": "FREQ=WEEKLY;BYDAY=MO"}, metadata={"job": "pipeline_summary"},)Every Monday at 9:00 New York time, the agent gets calendar.event.starting with the event's metadata, reads the job name and runs it. To skip a week, cancel that date. To move the job to Tuesdays, update the series. The schedule also shows up on the agent's calendar in the AgentMail console, so anyone on the team can see what the agent runs and when.
How do you stop the agent booking over your own meetings?
The agent's calendar only knows about the agent's events. It can't look up when other people are free, so it proposes times over email and books once the other side agrees.
For your own availability, give the agent free/busy access to your calendar instead of read and write access. The Google Calendar API has a calendar.freebusy scope that returns your busy times and nothing else. The agent checks it before proposing times, books on its own calendar and invites you. We wrote about why that split matters in Should You Give Your Agent Access to Your Calendar?.
How do you limit what the agent's calendar accepts?
An invitation only lands on the agent's calendar if the email passed sender authentication and came from the event's organizer. You can narrow that further in two places.
A receive allow list on the inbox limits who can email it, which also limits who can invite it. An entry can be one address or a whole domain:
Python
client.inboxes.lists.create(inbox_id, "receive", "allow", entry="yourcompany.com")A scoped API key gives the agent only the calendar permissions it needs. There are six: calendar_read, calendar_update, calendar_event_read, calendar_event_create, calendar_event_update and calendar_event_delete. An agent that only books and answers meetings doesn't need calendar_event_delete.
Can you give an agent a calendar without writing code?
If your agent runs in Claude, Cursor or another MCP client, connect the AgentMail MCP server. It includes calendar tools: get_agenda, get_event, create_event, update_event, delete_event, respond_to_invitation, get_calendar and update_calendar. You can ask the agent what's on its calendar this week, or tell it to accept the invite from Jane, and it calls the tools itself.
The webhooks from step 3 still need a server. An MCP client can read and change the calendar, but it can't wake up on its own when a meeting starts.
What should you know before going live?
- Calendar is available in US production. It isn't in EU production yet.
- A calendar holds up to 10,000 events by default.
- Webhooks can arrive out of order. An event created a moment before it starts can deliver
calendar.event.startingbeforecalendar.event.created. - A start or end webhook carries the event as it was when that moment was first processed, and a retry carries the same body. Read the event from the API when you need its current state.
- If more than one process writes to the same calendar, pass the event's
etagasif_matchwhen you update it. A write based on an old read then fails with a412instead of overwriting someone else's change. - All-day events have an inclusive end date. A one-day event has the same
startandend. - Recurring invitations from Google Calendar that include edited dates aren't imported yet.
- You can see the agent's calendar in the AgentMail console, with a badge on anything still waiting for a reply.
The full field list, payloads and error codes are in the calendar documentation.



