Event Management
Create, search, list, update, and delete calendar events. Events come from the Google Calendar API — they are NOT stored in the local SQL database.
Key Principle
Events live in Google Calendar, not SQL. Never use db-query or db-exec to work with events. Always use the dedicated scripts which query the Google Calendar API directly.
Event detail panels and popovers expose calendar.event-detail.bottom as an
ExtensionSlot with slotContext containing the event id, title, times,
timezones, location, attendees, and account email. Use the first-party
attendee-timezone UI or a source edit for adornments next to guest rows; the
slot does not inject per-row UI.
Scripts
list-events
Query events from Google Calendar within a date range.
Use list-google-calendars first when the user wants events from calendars
shared with any connected account. Pass the returned opaque sourceKey values
as calendarSourceKeys; never construct or decode them. Non-primary calendars
are view-only in Calendar, even when Google reports an editable access role,
and are excluded from booking availability.
# Today's events (--to is exclusive, so use tomorrow)
pnpm action list-events --from 2026-04-03 --to 2026-04-04
# This week
pnpm action list-events --from 2026-04-03 --to 2026-04-10
# Filter by title
pnpm action list-events --query "standup" --from 2026-04-01 --to 2026-04-30
# JSON output with full details (attendees, description, conference links)
pnpm action list-events --from 2026-04-03 --to 2026-04-04 --json
Default range: 7 days ago to 30 days forward. Always provide explicit --from and --to for predictable results.
Date format: Use ISO dates (YYYY-MM-DD). Natural language is also supported: today, tomorrow, next week, monday, friday, etc.
search-events
Search events by title. Returns JSON with full details including attendees.
pnpm action search-events --query "Builder"
pnpm action search-events --query "1:1" --from 2026-04-01 --to 2026-04-30
Always requires --query. Case-insensitive substring match on event title.
create-event
Create a new event on Google Calendar.
pnpm action create-event \
--title "Team standup" \
--start 2026-04-03T09:00:00 \
--end 2026-04-03T09:30:00
pnpm action create-event \
--title "Lunch with Alice" \
--start 2026-04-03T12:00:00 \
--end 2026-04-03T13:00:00 \
--location "Cafe" \
--description "Discuss Q2 plans"
# Invite attendees — Google sends email invitations by default
pnpm action create-event \
--title "Q2 planning" \
--start 2026-04-03T14:00:00 \
--end 2026-04-03T15:00:00 \
--attendees "alice@example.com,bob@example.com" \
--addGoogleMeet=true
# Create a real Zoom meeting and attach the link
pnpm action create-event \
--title "Q2 planning" \
--start 2026-04-03T14:00:00 \
--end 2026-04-03T15:00:00 \
--attendees "alice@example.com,bob@example.com" \
--addZoom=true
Required for ordinary events: --title, --start, --end (ISO datetime
format). Out-of-office events default the title to Out of office, and
working-location events use Google's generated display title when --title is
omitted.
Optional: --description, --location, --attendees, --addGoogleMeet, --addZoom, --sendUpdates, --accountEmail.
When multiple Google accounts are connected, choose the destination account's
primary calendar with --accountEmail:
pnpm action create-event \
--title "Team standup" \
--start 2026-04-03T09:00:00 \
--end 2026-04-03T09:30:00 \
--accountEmail secondary@example.com
Creating without accountEmail is only unambiguous when one Google account is
connected. The action does not support arbitrary non-primary Google calendar
IDs.
When attendees are invited and no video link/provider is supplied, Calendar
automatically adds a Google Meet link by default. Pass --addGoogleMeet=false
only when the user explicitly wants no video conferencing. If the user provides
a Zoom/Meet/Teams link in the location or description, or asks for
--addZoom=true, do not add Google Meet too.
Native Google Calendar status events are supported:
# Full-day out of office. Start and end are inclusive human dates; Calendar
# writes Google-compatible local-midnight timed bounds in this timezone.
pnpm action create-event \
--start 2026-04-03 \
--end 2026-04-07 \
--startTimeZone America/New_York \
--fullDay true \
--eventType outOfOffice \
--autoDeclineMode declineAllConflictingInvitations \
--declineMessage "Declined because I am out of office"
# Partial-day out of office
pnpm action create-event \
--start 2026-04-03T13:00:00-04:00 \
--end 2026-04-03T17:00:00-04:00 \
--startTimeZone America/New_York \
--eventType outOfOffice
# Focus time
pnpm action create-event \
--title "Focus time" \
--start 2026-04-03T09:00:00 \
--end 2026-04-03T11:00:00 \
--eventType focusTime
# Working location
pnpm action create-event \
--title "Working from home" \
--start 2026-04-03 \
--end 2026-04-04 \
--allDay true \
--eventType workingLocation \
--workingLocationType homeOffice
Working-location events sync from Google with workingLocationProperties and
render as native working locations in the UI instead of generic all-day events.
They are transparent/non-blocking for availability. All-day working locations
use an exclusive --end date and can span multiple days; timed working
locations use ISO datetime start and end values.
Creating from a calendar day uses the selected Home, Office, or Other type —
do not leave the draft as Home and create that instead. Office does not need a
custom building name; Other does. The Other name is workingLocationLabel
(drafts keep location empty), so create must send that label — not an empty
location. Create and update reject a blank Other name instead of storing
Working. If that day already has a working location
on the same account, update that day's occurrence (scope: "single") instead
of creating a second event. Timed (not all-day) working locations need a
summary of Home, Office, or the custom label — never the generated
Working location placeholder. All-day ones omit summary so Google can derive
the title. Converting a timed location that ends at local midnight back to
all-day keeps that single day; do not add another exclusive day.
--fullDay true is semantic only for out-of-office creation. It does not send
Google an all-day date event, which Google rejects for this event type.
Instead, the action converts inclusive dates to timed local-midnight bounds,
sets provider allDay false, and preserves the chosen IANA timezone across DST.
The default auto-decline mode covers all conflicting invitations; override it
with declineOnlyNewConflictingInvitations or declineNone when requested.
For a visible occurrence in a recurring working-location series, default to
scope: "single" and pass the occurrence's event id, not its
recurringEventId. Use scope: "all" only when the user explicitly asks to
change every day in the series. Keep office building/floor/desk metadata when
editing an office label, and clear incompatible location labels when changing
between Home, Office, and Other.
Do not use eventType for Tasks or appointment schedules. Google Calendar
Tasks are a separate product/API surface, and appointment schedules should use
booking links or availability workflows instead.
Use --transparency opaque for Busy and --transparency transparent for Free.
Use --visibility public or --visibility private when the user asks for
public/private visibility.
Use only one generated video provider per event: --addGoogleMeet=true or --addZoom=true, not both. Zoom requires the user to connect Zoom in Settings first; check with pnpm action get-zoom-status when unsure.
--attendees accepts a comma- or space-separated list of email addresses. When attendees are provided, Google sends email invitations automatically (sendUpdates=all). Use --sendUpdates=none to suppress emails.
To mark a guest optional, pass attendees as a JSON array with optional: true:
pnpm action create-event \
--title "Q2 planning" \
--start 2026-04-03T14:00:00 \
--end 2026-04-03T15:00:00 \
--attendees '[{"email":"alice@example.com"},{"email":"bob@example.com","optional":true}]'
Use --startTimeZone / --endTimeZone with IANA timezone names when the event should be anchored to a specific timezone, e.g. --startTimeZone America/Los_Angeles.
Use --reminders '[{"method":"popup","minutes":10},{"method":"email","minutes":1440}]' for multiple alerts. Use --remindersUseDefault false --reminders '[]' for no alerts.
Use --colorId 1..11 for a Google Calendar event color. Use update-calendar-visual-preferences for broad app display rules instead of per-event Google color.
Use --attachments '[{"fileUrl":"https://drive.google.com/...","title":"Agenda"}]' to attach Drive files, HTTPS file links, or files uploaded through the app's file upload storage. Google Calendar supports up to 25 attachments per event.
The event is created directly on Google Calendar. Google Calendar must be connected first.
manage-event-draft
Prepare an unsent calendar invite draft for user review. Use this when the user asks to draft, prepare, or review an invite before sending it, especially from an external agent flow.
pnpm action manage-event-draft \
--action create \
--title "Q2 planning" \
--start 2026-04-03T14:00:00 \
--end 2026-04-03T15:00:00 \
--attendees "alice@example.com,bob@example.com" \
--addGoogleMeet=true
manage-event-draft stores calendar-draft-{id} in application state and
returns a "Review invite in Calendar" deep link. Opening the link shows the
draft as a visible placeholder on the calendar with the native event detail
editor open. Nothing is written to Google Calendar, and no guest is notified,
until the user presses Create in the UI.
Use --action update --id <draft-id> to revise a draft and --action delete
to remove one. Draft fields match create-event for title, time, description,
location, attendees, reminders, attachments, color, and video provider.
update-event
Update an existing Google Calendar event. Use the event id from list-events,
search-events, or get-event. Always preserve the event's accountEmail on
the update so multi-account calendars use the right connected account.
To move an existing event between connected Google account calendars, pass its
current account as --accountEmail and the destination account as
--targetAccountEmail:
pnpm action update-event \
--id google-event-id \
--accountEmail work@example.com \
--targetAccountEmail personal@example.com
The action creates a copy on the destination account and deletes the source
event. It preserves the supported event fields and creates a fresh Google Meet
when the original has one. Do not combine a calendar move with other event
field changes. If guests are present, pass --sendUpdates all or
--sendUpdates none explicitly when the desired notification behavior matters.
Moving an entire recurring series is not supported; use --scope single for
one occurrence.
pnpm action update-event --id google-event-id --accountEmail secondary@example.com --title "New title"
pnpm action update-event --id google-event-id --start 2026-04-03T10:00:00 --end 2026-04-03T10:30:00
# Replace attendee list (Google sends invites to anyone newly added)
pnpm action update-event \
--id google-event-id \
--attendees "alice@example.com,bob@example.com,carol@example.com"
# Prefer addAttendees when inviting more people so existing RSVP metadata is preserved
pnpm action update-event \
--id google-event-id \
--addAttendees '[{"email":"dana@example.com","optional":true}]'
# Mark an existing guest optional without resetting RSVPs — fetch via get-event,
# then pass the full attendees list with optional:true on that guest
pnpm action update-event \
--id google-event-id \
--attendees '[{"email":"alice@example.com"},{"email":"bob@example.com","optional":true}]'
# Suppress invitation emails
pnpm action update-event --id google-event-id --attendees "alice@example.com" --sendUpdates none
# Add generated video conferencing
pnpm action update-event --id google-event-id --addGoogleMeet=true
pnpm action update-event --id google-event-id --addZoom=true
# Update an existing working-location event's native metadata
pnpm action update-event \
--id google-working-location-id \
--workingLocationType officeLocation \
--workingLocationLabel "Pier 57"
# Add multiple alerts, a Google event color, and an attachment
pnpm action update-event \
--id google-event-id \
--reminders '[{"method":"popup","minutes":10},{"method":"email","minutes":1440}]' \
--colorId 9 \
--attachments '[{"fileUrl":"https://drive.google.com/...","title":"Agenda"}]'
--attendees REPLACES the entire attendee list — to add someone, prefer addAttendees so existing RSVP notes/statuses are preserved. To change whether a guest is optional or required after the fact, fetch the current list via get-event and pass the full attendees array with optional: true or omit/false for required. Pass an empty string to clear all attendees.
For "add Zoom to this meeting", fetch or use the visible event id and call update-event --addZoom=true. Do not create an extension for Zoom; Zoom is a first-party calendar integration handled by the event actions and the Settings page.
Google Calendar does not allow changing an existing event's eventType; use
workingLocationType and workingLocationLabel only on events that already
have eventType: "workingLocation".
Google Calendar API v3 currently documents working locations on Events, but the Settings API/discovery document does not expose working-hours settings. Treat working-hours overlays or Find a Time constraints as a follow-up only after a real provider data path exists.
For recurring events, pass a Google Calendar RRULE in --recurrence. Example: to make a daily event weekdays only, use:
pnpm action update-event \
--id google-event-id \
--recurrence "RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
delete-event
Delete an event if the user is the organizer, or remove it from their own calendar with --removeOnly true when they are not. For recurring events, use --scope single, --scope all, or --scope thisAndFollowing.
Pass the event's accountEmail on deletes, including recurring-series choices
and attendee removals, so the operation uses the account that owns the event.
pnpm action delete-event --id google-event-id --accountEmail secondary@example.com --scope single
pnpm action delete-event --id google-event-id --scope thisAndFollowing
pnpm action delete-event --id google-event-id --removeOnly true
One event only. For more than one, use delete-events.
delete-events
Every "remove all …" / "clear …" request goes here, in one call. Looping
delete-event per event cannot finish a real weekend cleanup inside a hosted
foreground run — that is the failure a user sees as "the agent stopped before
finishing" — and a partial loop leaves the calendar half-cleaned with no record
of which events survived. See the reliable-mutations skill.
Select by range plus --daysOfWeek and/or --query, or pass explicit --ids.
A filtered selection needs both --from and --to — a one-sided range would
silently widen or shrink a destructive request. Preview with --dryRun true,
show the user the matched list, then repeat the same call without --dryRun.
# What would go?
pnpm action delete-events \
--from 2026-04-01 --to 2026-05-01 \
--daysOfWeek saturday,sunday \
--dryRun true
# Delete it
pnpm action delete-events \
--from 2026-04-01 --to 2026-05-01 \
--daysOfWeek saturday,sunday
# Explicit ids from a previous list-events/search-events
pnpm action delete-events --ids google-a,google-b --accountEmail secondary@example.com
--daysOfWeek accepts full or 3-letter day names, weekend, or weekdays, and
resolves each event's day in the timezone the calendar is pinned to (the saved
timezone setting, then the browser's) — a Sunday 5pm America/Los_Angeles
meeting is Monday in UTC, so a UTC comparison deletes the wrong day. Pass
--timezone to override; an invalid zone is rejected, never silently treated as
UTC. Events are selected by where they start: --from inclusive, --to
exclusive, so an event starting exactly on the end bound, or a multi-day event
that began before --from, is left alone. Both bounds must be real dates; a
blank or impossible date (2026-02-30, with or without a time) is rejected
rather than rolled forward.
--removeOnly true cannot honor --scope thisAndFollowing: Google only lets a
non-organizer drop one occurrence at a time, so that pair is rejected instead of
reporting a series-wide removal that did not happen. Use --scope single per
occurrence, or --scope all to drop the whole series from your own calendar.
A filtered selection only ever removes the matched occurrences, so it accepts
--scope single. all and thisAndFollowing act on a whole recurring series —
which for a daily series would also delete the weekdays the user kept, and would
not match the dry-run preview — so they require explicit --ids or
delete-event, and take exactly one id per call because they mutate the series
master.
The commit re-reads the calendar, so it acts on the user's intent ("the weekend
is clear") rather than a frozen list — an event that moved onto a Saturday
between preview and confirmation is still removed. When the user should get
exactly the reviewed set and nothing else, pass the --ids from the dry-run
result instead of repeating the filter.
The result is a per-event report, not a boolean. Read deleted, failed, and
skipped and give the user the counts; a failed entry carries the provider
error and a skipped entry says why the app cannot delete it (ICS feeds are
read-only; a booking, including the Google event backing one, is cancelled from
the booking so that deleting the event cannot leave the booking confirmed — this
holds for explicit --ids too). Never report a bulk delete as done from the
absence of a thrown error.
coverageComplete: false plus unreadableSources means a feed could not be
read, so the sweep does not account for everything the user can see. Deletable
events are still deleted — a third-party feed outage should not block a cleanup,
and ICS events were never deletable — but say plainly that the feed was not
covered instead of reporting a clean pass.
The action refuses rather than guesses when the calendar read was incomplete (an
expired account token) or when more than 200 events match — narrow the filter
and run again. An unreadableSources entry means an ICS feed could not be read,
so mention that the sweep did not cover it.
delete-events only reads the signed-in user's own accounts, bookings, and
subscribed feeds; it cannot touch an overlaid person's calendar.
rsvp-event
Accept, decline, or tentatively accept an invitation with the event's
accountEmail. Preserve it for recurring RSVP scope as well:
pnpm action rsvp-event \
--id google-event-id \
--accountEmail secondary@example.com \
--status accepted
list-events Result Formats And Source Coverage
list-events remains the UI-compatible event list by default. External MCP
callers receive its compact, paginated version 1 inventory envelope unless they
explicitly request format: "legacy"; use format: "inventory" for that same
coverage-aware result from other callers.
Preserve its account coverage, sourceCoverage, and coverageComplete fields:
Google account, ICS feed, overlay, and local-booking sources are independent,
and a partial source failure is not an empty calendar. Distinguish an empty
calendar from missing auth, reauth-needed, or fetch failures instead of
reporting "no events".
Pass accountEmails only for connected accounts; the action validates the whole
requested set before provider work.
Guests, RSVP, And Attendee Timezones
Use get-attendee-timezones / set-attendee-timezone to read or save per-guest
IANA timezone overrides (attendee-timezones user setting). The UI shows each
guest's local event-start time when a timezone is known (self from the browser
zone; others from attendee.timeZone or the override map, with the event zone
as a fallback for the organizer).
Use rsvp-event for invitation responses. Pass note when the user wants a
visible RSVP comment on a declined or tentative response; pass an empty note to
clear an existing RSVP comment.
When adding guests to an existing event, prefer update-event with
addAttendees so existing RSVP notes/statuses are preserved. Use
scope: "all" only when the user wants a recurring-event guest change applied
to the whole series.
Pass optional: true on an attendee object to mark someone optional when
creating, drafting, or adding guests. To change optional/required after the
fact, replace the full attendees list with optional set on that guest.
Working Locations
Google Calendar working locations are status events
(eventType: "workingLocation"). Sync and display them as working locations,
keep them transparent/non-blocking, and preserve workingLocationProperties
instead of treating the summary as a generic all-day event title.
When updating one visible occurrence in a recurring working-location series,
pass that occurrence's event id with scope: "single" by default. Use the
series scope only when the user explicitly chooses all days.
Google Calendar API v3 exposes working locations through Events. The current Settings API and Calendar v3 discovery document do not expose working-hours settings, so do not promise working-hours UI or overlays unless a real provider data path has been verified first.
Date Patterns
When the user says:
| User says | What to do |
|---|---|
| "today's schedule" | list-events --from <today> --to <tomorrow> |
| "this week" | list-events --from <monday> --to <next-monday> |
| "next Tuesday" | list-events --from <tuesday> --to <wednesday> |
| "meetings with Alice" | search-events --query "Alice" |
| "schedule a meeting" | create-event --title ... --start ... --end ... |
| "draft an invite" | manage-event-draft --action create --title ... --start ... --end ... |
| "schedule a Zoom meeting" | create-event --title ... --start ... --end ... --addZoom=true |
| "move an event to another connected calendar" | update-event --id ... --accountEmail ... --targetAccountEmail ... |
| "move/rename/update a meeting" | update-event --id ... |
| "add Zoom to this meeting" | update-event --id ... --addZoom=true |
| "delete/remove a meeting" | delete-event --id ... |
| "remove all Saturday and Sunday meetings" | delete-events --from ... --to ... --daysOfWeek saturday,sunday |
| "clear my calendar next week" | delete-events --from ... --to ... (preview with --dryRun true first) |
| "remove weekends from a daily recurring event" | update-event --id ... --recurrence "RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR" |
| "what's coming up" | list-events (uses default 30-day forward window) |
Google Calendar Connection
Events require a connected Google Calendar account. Check with GET /_agent-native/google/status. If not connected, tell the user to connect via the Settings page.
Event Object Shape
{
"id": "google-event-id",
"title": "Team standup",
"description": "Daily sync",
"start": "2026-04-03T09:00:00Z",
"end": "2026-04-03T09:30:00Z",
"location": "Conference Room A",
"allDay": false,
"attendees": [
{ "email": "alice@example.com", "displayName": "Alice", "responseStatus": "accepted" },
{ "email": "bob@example.com", "displayName": "Bob", "responseStatus": "needsAction", "optional": true }
],
"conferenceData": { ... },
"hangoutLink": "https://meet.google.com/...",
"status": "confirmed",
"source": "google"
}