
Automating Calendar
- 48 installs
- 39 repo stars
- Updated January 14, 2026
- spillwavesolutions/automating-mac-apps-plugin
Helps with ai & agent building tasks during AI-assisted development.
About
automating-calendar is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- automating-calendar
- AI & Agent Building
- AI-coding skill
Automating Calendar by the numbers
- 48 all-time installs (skills.sh)
- Ranked #7,473 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spillwavesolutions/automating-mac-apps-plugin --skill automating-calendarAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 48 |
|---|---|
| repo stars | ★ 39 |
| Last updated | January 14, 2026 |
| Repository | spillwavesolutions/automating-mac-apps-plugin ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Automating Calendar (JXA with AppleScript Discovery)
Contents
Relationship to the macOS automation skill
- Use
automating-mac-appsfor general app permissions, shell commands, UI scripting, and cross-app automation patterns. - This skill focuses specifically on Calendar events, calendars, and EventKit bridge functionality.
- PyXA Installation: To use PyXA examples in this skill, see the installation instructions in
automating-mac-appsskill (PyXA Installation section).
Core Framing
- Calendar dictionary is AppleScript-first; discover there
- JXA provides logic, data handling, and ObjC/EventKit bridge access
- PyXA offers modern Python alternative with cleaner syntax
Implementation Notes: See automating-calendar/references/calendar-basics.md
Workflow (default)
1) [ ] Discover Calendar dictionary terms in Script Editor. 2) [ ] Prototype minimal AppleScript commands. 3) [ ] Port to JXA with defensive error handling. 4) [ ] Implemented batch reads (avoided heavy .whose()). 5) [ ] Added EventKit bridge for advanced queries. 6) [ ] Tested with sample events and verified results.
Error Handling
- Wrap operations in try-catch blocks
- Verify calendar access:
Calendar.calendars.length > 0 - Check event creation:
if (!event.id()) throw new Error('Event creation failed') - Log operations for debugging
Validation Checklist
- [ ] Calendar permissions granted (System Settings > Privacy & Security > Calendars)
- [ ] Calendar access confirmed:
Calendar.calendars.length > 0 - [ ] Event created with valid start/end dates
- [ ] Event visible in Calendar UI after save
- [ ] EventKit bridge queries return expected results
- [ ] Error handling wraps all operations
- [ ] Output matches expected event properties
Core Examples
Basic Event Creation (JXA - Legacy):
const Calendar = Application("Calendar");
const event = Calendar.calendars[0].events.push(Calendar.Event({
summary: "Meeting",
startDate: new Date(),
endDate: new Date(Date.now() + 3600000)
}));Basic Event Creation (PyXA - Recommended):
import PyXA
from datetime import datetime, timedelta
calendar = PyXA.Calendar()
# Get first calendar
work_calendar = calendar.calendars()[0]
# Create event
event = work_calendar.events().push({
"summary": "Meeting",
"start_date": datetime.now(),
"end_date": datetime.now() + timedelta(hours=1),
"location": "Conference Room A"
})
print(f"Created event: {event.summary()}")PyObjC with EventKit (Advanced):
from EventKit import EKEventStore, EKEvent, EKCalendar
from Foundation import NSDate, NSTimeInterval
import objc
# Initialize event store
store = EKEventStore.alloc().init()
# Request access (async in real implementation)
# store.requestAccessToEntityType_completion_(EKEntityTypeEvent, None)
# Get default calendar
calendars = store.calendarsForEntityType_(EKEntityTypeEvent)
if calendars:
default_calendar = calendars[0]
# Create event
event = EKEvent.eventWithEventStore_(store)
event.setTitle_("Meeting")
event.setStartDate_(NSDate.date())
event.setEndDate_(NSDate.dateWithTimeIntervalSinceNow_(3600)) # 1 hour
event.setCalendar_(default_calendar)
# Save event
error = objc.nil
success = store.saveEvent_span_error_(event, EKSpanThisEvent, error)
if success:
print(f"Event created: {event.title()}")
else:
print(f"Error creating event: {error}")EventKit Bridge Query (JXA - Legacy):
ObjC.import('EventKit');
const store = $.EKEventStore.alloc.init;
// See 'eventkit-query.md' for full predicate implementationEventKit Bridge Query (PyObjC - Modern):
from EventKit import EKEventStore, EKEntityTypeEvent, NSPredicate
from Foundation import NSDate
store = EKEventStore.alloc().init()
# Get events for today
start_date = NSDate.date() # Today
end_date = NSDate.dateWithTimeIntervalSinceNow_(86400) # Tomorrow
calendars = store.calendarsForEntityType_(EKEntityTypeEvent)
predicate = store.predicateForEventsWithStartDate_endDate_calendars_(
start_date, end_date, calendars)
events = store.eventsMatchingPredicate_(predicate)
for event in events:
print(f"Event: {event.title()}, Start: {event.startDate()}")When Not to Use
- Cross-platform calendar automation (use Google Calendar API or CalDAV)
- iCloud sync operations (use EventKit directly)
- Non-macOS platforms
- Simple AppleScript-only tasks (skip JXA complexity)
- Calendar sharing or permissions management (use Calendar UI)
What to load
- Calendar JXA basics:
automating-calendar/references/calendar-basics.md - Recipes (events, alarms, recurrence):
automating-calendar/references/calendar-recipes.md - Advanced patterns (time zones, batch reads, EventKit bridge):
automating-calendar/references/calendar-advanced.md - Dictionary translation table:
automating-calendar/references/calendar-dictionary.md - PyXA API Reference (complete class/method docs):
automating-calendar/references/calendar-pyxa-api-reference.md - EventKit query example:
automating-calendar/references/eventkit-query.md - EventKit create with recurrence:
automating-calendar/references/eventkit-create.md - EventKit time zone example:
automating-calendar/references/eventkit-timezones.md - EventKit exceptions/occurrences:
automating-calendar/references/eventkit-exceptions.md - EventKit cancel occurrence example:
automating-calendar/references/eventkit-occurrence-cancel.md
Calendar JXA advanced patterns
Batch read warnings
- Avoid heavy
.whose()on events; it is slow/unreliable. - Prefer EventKit bridge for large queries.
Time zones
- JS Date is local; to target another TZ, adjust offset manually or use EventKit.
EventKit bridge (outline)
ObjC.import('EventKit');
const store = $.EKEventStore.alloc.init;
// Request access and wait
// Build predicate and fetch events via eventsMatchingPredicateUI scripting (view changes)
const se = Application("System Events");
const calProc = se.processes.byName("Calendar");
calProc.menuBars.menuBarItems.byName("View").menus.menuItems.byName("Day").click();Calendar JXA basics
Bootstrapping
const App = Application.currentApplication();
App.includeStandardAdditions = true;
const Cal = Application("Calendar");List calendars
const names = Cal.calendars.name();Create simple event
const cal = Cal.calendars.byName("Work");
const start = new Date();
const end = new Date(start.getTime() + 60 * 60000);
cal.events.push(Cal.Event({ summary: "Meeting", startDate: start, endDate: end }));Calendar dictionary translation table
AppleScript JXA
----------------------------------- ------------------------------------------
calendar "Work" Cal.calendars.byName("Work")
events of calendar Cal.calendars.events
summary of event evt.summary()
start date of event evt.startDate()
make new event cal.events.push(Cal.Event({...}))Notes:
- Collections are specifiers; call methods to read values.
- Use recurrence strings for RRULEs.
PyXA Calendar Module API Reference
PyXA Module - Control macOS Calendar.app using JXA-like syntax from Python.
This reference documents all classes, methods, properties, and enums in the PyXA Calendar module. For practical examples and usage patterns, see calendar-basics.md and calendar-recipes.md.
Contents
- Class Hierarchy
- XACalendarApplication
- XACalendarCalendar
- XACalendarEvent
- XACalendarAttendee
- XACalendarAttachment
- XACalendarAlarm
- XACalendarDocument
- XACalendarWindow
- List Classes
- Enumerations
- Quick Reference Tables
---
Class Hierarchy
XAObject
├── XACalendarApplication (XASBApplication, XACanOpenPath)
│ ├── XACalendarCalendar
│ │ └── XACalendarEvent
│ │ ├── XACalendarAttendee
│ │ └── XACalendarAttachment
│ ├── XACalendarDocument
│ └── XACalendarWindow (XASBWindow)
├── XACalendarAlarm
│ ├── XACalendarDisplayAlarm
│ ├── XACalendarMailAlarm
│ ├── XACalendarOpenFileAlarm
│ └── XACalendarSoundAlarm
└── List Classes
├── XACalendarCalendarList
├── XACalendarEventList
├── XACalendarAttendeeList
├── XACalendarAttachmentList
└── XACalendarDocumentList---
XACalendarApplication
Bases: XASBApplication, XACanOpenPath
Main entry point for interacting with Calendar.app.
Properties
| Property | Type | Description |
|---|---|---|
default_calendar | XACalendarCalendar | The default calendar for new events |
frontmost | bool | Whether Calendar is the frontmost application |
name | str | Application name ("Calendar") |
version | str | Application version |
properties | dict | All properties of the Calendar application |
Methods
calendars(filter=None) -> XACalendarCalendarList
Returns a list of calendars matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
import PyXA
calendar = PyXA.Application("Calendar")
calendars = calendar.calendars()
for cal in calendars:
print(cal.name)documents(filter=None) -> XACalendarDocumentList
Returns a list of open documents matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
new_calendar(name='New Calendar') -> XACalendarCalendar
Creates a new calendar with the specified name.
Parameters:
name(str) - Name for the new calendar (default: "New Calendar")
Returns: The newly created calendar
Example:
calendar = PyXA.Application("Calendar")
work_cal = calendar.new_calendar("Work Events")
print(f"Created: {work_cal.name}")new_event(summary, start_date, end_date, calendar=None) -> XACalendarEvent
Creates a new event.
Parameters:
summary(str) - Event title/summarystart_date(datetime) - Event start timeend_date(datetime) - Event end timecalendar(XACalendarCalendar | None) - Calendar to add event to (default: default calendar)
Returns: The newly created event
Example:
from datetime import datetime, timedelta
calendar = PyXA.Application("Calendar")
event = calendar.new_event(
summary="Team Meeting",
start_date=datetime.now(),
end_date=datetime.now() + timedelta(hours=1)
)
print(f"Created event: {event.summary}")make(specifier, properties=None, data=None) -> XAObject
Creates a new element without adding to any list. Use XAList.push() to add.
Parameters:
specifier(str | ObjectType) - Class name to createproperties(dict) - Properties for the objectdata(Any) - Initialization data
reload_calendars() -> XACalendarApplication
Reloads calendars from all sources. Returns self for chaining.
subscribe_to(url) -> XACalendarCalendar
Subscribes to a calendar at the specified URL.
Parameters:
url(str) - URL of the calendar to subscribe to (iCal format)
Returns: The subscribed calendar
Example:
calendar = PyXA.Application("Calendar")
subscribed = calendar.subscribe_to("https://example.com/calendar.ics")switch_view_to(view) -> XACalendarApplication
Switches the calendar view.
Parameters:
view(ViewType) - View to switch to (DAY, WEEK, MONTH, YEAR)
Returns: Self for method chaining
Example:
calendar = PyXA.Application("Calendar")
calendar.switch_view_to(XACalendarApplication.ViewType.WEEK)view_calendar_at(date, view=None) -> XACalendarApplication
Navigates to a specific date, optionally changing view.
Parameters:
date(datetime) - Date to navigate toview(ViewType | None) - Optional view to switch to
Returns: Self for method chaining
Example:
from datetime import datetime
calendar = PyXA.Application("Calendar")
# View January 1st in month view
calendar.view_calendar_at(
datetime(2025, 1, 1),
XACalendarApplication.ViewType.MONTH
)---
XACalendarCalendar
Bases: XAObject
Represents a calendar in Calendar.app.
Properties
| Property | Type | Description |
|---|---|---|
name | str | Calendar name |
description | str | Calendar description |
color | XAColor | Calendar display color |
writable | bool | Whether the calendar can be modified |
calendar_obj | EKCalendar | Underlying EventKit calendar object |
properties | dict | All calendar properties |
Methods
events(filter=None) -> XACalendarEventList
Returns events in this calendar matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
calendar = PyXA.Application("Calendar")
work_cal = calendar.calendars().by_name("Work")
all_events = work_cal.events()events_in_range(start_date, end_date) -> XACalendarEventList
Returns events within a date range.
Parameters:
start_date(datetime) - Range startend_date(datetime) - Range end
Example:
from datetime import datetime, timedelta
calendar = PyXA.Application("Calendar")
cal = calendar.calendars()[0]
# Get events for next week
start = datetime.now()
end = start + timedelta(days=7)
events = cal.events_in_range(start, end)
for event in events:
print(f"{event.summary}: {event.start_date}")events_today() -> XACalendarEventList
Returns all events occurring today.
Example:
calendar = PyXA.Application("Calendar")
cal = calendar.calendars()[0]
today_events = cal.events_today()
print(f"You have {len(today_events)} events today")new_event(name, start_date, end_date) -> XACalendarEvent
Creates a new event in this calendar.
Parameters:
name(str) - Event titlestart_date(datetime) - Event start timeend_date(datetime) - Event end time
Returns: The newly created event
Example:
from datetime import datetime, timedelta
calendar = PyXA.Application("Calendar")
work_cal = calendar.calendars().by_name("Work")
event = work_cal.new_event(
"Project Review",
datetime(2025, 1, 15, 14, 0),
datetime(2025, 1, 15, 15, 0)
)delete() -> XACalendarEvent
Deletes this calendar.
---
XACalendarEvent
Bases: XAObject
Represents a calendar event.
Properties
| Property | Type | Description |
|---|---|---|
summary | str | Event title/summary |
description | str | Event description/notes |
start_date | datetime | Event start date and time |
end_date | datetime | Event end date and time |
allday_event | bool | Whether this is an all-day event |
location | str | Event location |
url | str | Associated URL |
recurrence | str | Recurrence rule string |
status | EventStatus | Event status (CONFIRMED, TENTATIVE, CANCELLED, NONE) |
sequence | int | Modification sequence number |
stamp_date | datetime | Last modification timestamp |
excluded_dates | list[datetime] | Dates excluded from recurrence |
uid | str | Unique event identifier |
xa_event_obj | type | Underlying EventKit event object |
properties | dict | All event properties |
Methods
attendees(filter=None) -> XACalendarAttendeeList
Returns attendees for this event.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
calendar = PyXA.Application("Calendar")
event = calendar.calendars()[0].events()[0]
attendees = event.attendees()
for attendee in attendees:
print(f"{attendee.display_name}: {attendee.email}")attachments(filter=None) -> XACalendarAttachmentList
Returns attachments for this event.
Parameters:
filter(dict | None) - Property-value pairs to filter by
add_attachment(path) -> XACalendarEvent
Adds an attachment to this event.
Parameters:
path(str) - Path to the file to attach
Returns: Self for method chaining
Example:
event = calendar.calendars()[0].events()[0]
event.add_attachment("/Users/me/Documents/agenda.pdf")delete() -> None
Deletes this event.
duplicate() -> XACalendarEvent
Creates a copy of this event in the same calendar.
Returns: The duplicated event
duplicate_to(calendar) -> XACalendarEvent
Duplicates this event to another calendar.
Parameters:
calendar(XACalendarCalendar) - Target calendar
Returns: The duplicated event
Example:
calendar = PyXA.Application("Calendar")
work_cal = calendar.calendars().by_name("Work")
personal_cal = calendar.calendars().by_name("Personal")
# Copy event to personal calendar
event = work_cal.events()[0]
copied = event.duplicate_to(personal_cal)move_to(calendar) -> XACalendarEvent
Moves this event to another calendar.
Parameters:
calendar(XACalendarCalendar) - Target calendar
Returns: Self for method chaining
show() -> XACalendarEvent
Opens the event in Calendar.app for viewing.
Returns: Self for method chaining
---
XACalendarAttendee
Bases: XAObject
Represents an event attendee.
Properties
| Property | Type | Description |
|---|---|---|
display_name | str | Attendee's display name |
email | str | Attendee's email address |
participation_status | ParticipationStatus | RSVP status (ACCEPTED, DECLINED, TENTATIVE, UNKNOWN) |
properties | dict | All attendee properties |
---
XACalendarAttachment
Bases: XAObject
Represents an event attachment.
Properties
| Property | Type | Description |
|---|---|---|
file | XAPath | File path of attachment |
file_name | str | Name of the attached file |
type | str | MIME type of the attachment |
url | XAURL | URL of the attachment (if URL-based) |
uuid | str | Unique identifier |
Methods
open() -> XACalendarAttachment
Opens the attachment in its default application.
Returns: Self for method chaining
---
XACalendarAlarm
Bases: XAObject
Base class for all alarm types.
Properties
| Property | Type | Description |
|---|---|---|
trigger_date | datetime | Absolute alarm trigger time |
trigger_interval | int | Relative offset in seconds from event start |
properties | dict | All alarm properties |
XACalendarDisplayAlarm
Bases: XACalendarAlarm
A visual notification alarm.
XACalendarMailAlarm
Bases: XACalendarAlarm
An email notification alarm.
XACalendarOpenFileAlarm
Bases: XACalendarAlarm
An alarm that opens a file.
Additional Properties:
| Property | Type | Description |
|---|---|---|
file_path | str | Path to file to open when alarm triggers |
XACalendarSoundAlarm
Bases: XACalendarAlarm
An alarm that plays a sound.
Additional Properties:
| Property | Type | Description |
|---|---|---|
sound_file | str | Path to sound file |
sound_name | str | Name of system sound |
---
XACalendarDocument
Bases: XAObject
Represents a Calendar document.
Properties
| Property | Type | Description |
|---|---|---|
name | str | Document name |
file | XAPath | Document file path |
modified | bool | Whether document has unsaved changes |
properties | dict | All document properties |
---
XACalendarWindow
Bases: XASBWindow
Represents a Calendar window.
Properties
| Property | Type | Description |
|---|---|---|
document | XACalendarDocument | Document displayed in window |
properties | dict | All window properties |
---
List Classes
PyXA provides list wrapper classes with fast enumeration and bulk property access.
XACalendarCalendarList
Bulk methods for calendar lists:
calendars = app.calendars()
calendars.name() # -> list[str]
calendars.description() # -> list[str]
calendars.color() # -> list[XAColor]
calendars.writable() # -> list[bool]
calendars.properties() # -> list[dict]Filter methods:
calendars.by_name("Work")
calendars.by_writable(True)
calendars.by_description("Personal calendar")XACalendarEventList
Bulk methods for event lists:
events = calendar.events()
events.summary() # -> list[str]
events.description() # -> list[str]
events.start_date() # -> list[datetime]
events.end_date() # -> list[datetime]
events.allday_event() # -> list[bool]
events.location() # -> list[str]
events.url() # -> list[str]
events.recurrence() # -> list[str]
events.status() # -> list[EventStatus]
events.uid() # -> list[str]
events.properties() # -> list[dict]Filter methods:
events.by_summary("Meeting")
events.by_location("Conference Room")
events.by_allday_event(True)
events.by_status(XACalendarApplication.EventStatus.CONFIRMED)XACalendarAttendeeList
Bulk methods for attendee lists:
attendees = event.attendees()
attendees.display_name() # -> list[str]
attendees.email() # -> list[str]
attendees.participation_status() # -> list[ParticipationStatus]
attendees.properties() # -> list[dict]Filter methods:
attendees.by_email("user@example.com")
attendees.by_display_name("John Doe")
attendees.by_participation_status(XACalendarApplication.ParticipationStatus.ACCEPTED)XACalendarAttachmentList
Bulk methods for attachment lists:
attachments = event.attachments()
attachments.file_name() # -> list[str]
attachments.type() # -> list[str]
attachments.uuid() # -> list[str]
attachments.properties() # -> list[dict]Filter methods:
attachments.by_file_name("document.pdf")
attachments.by_type("application/pdf")XACalendarDocumentList
Bulk methods for document lists:
docs = app.documents()
docs.name() # -> list[str]
docs.modified() # -> list[bool]
docs.file() # -> list[XAPath]
docs.properties() # -> list[dict]Filter methods:
docs.by_name("Calendar")
docs.by_modified(True)---
Enumerations
EventStatus
Event confirmation status.
| Value | Description |
|---|---|
NONE | No status set |
CONFIRMED | Event is confirmed |
TENTATIVE | Event is tentative |
CANCELLED | Event is cancelled |
Usage:
from PyXA import XACalendarApplication
# Filter confirmed events
confirmed = events.by_status(XACalendarApplication.EventStatus.CONFIRMED)
# Check event status
if event.status == XACalendarApplication.EventStatus.CANCELLED:
print("Event was cancelled")ParticipationStatus
Attendee RSVP status.
| Value | Description |
|---|---|
UNKNOWN | Status not known |
ACCEPTED | Attendee accepted |
DECLINED | Attendee declined |
TENTATIVE | Attendee tentatively accepted |
Usage:
# Get accepted attendees
accepted = attendees.by_participation_status(
XACalendarApplication.ParticipationStatus.ACCEPTED
)Priority
Event priority levels.
| Value | Description |
|---|---|
NONE | No priority set |
LOW | Low priority |
MEDIUM | Medium priority |
HIGH | High priority |
ViewType
Calendar view options.
| Value | Description |
|---|---|
DAY | Day view |
WEEK | Week view |
MONTH | Month view |
YEAR | Year view |
Usage:
# Switch to week view
calendar.switch_view_to(XACalendarApplication.ViewType.WEEK)
# Navigate to specific date in month view
calendar.view_calendar_at(
datetime(2025, 6, 1),
XACalendarApplication.ViewType.MONTH
)ObjectType
Creatable object types for the make() method.
| Value | Description |
|---|---|
CALENDAR | Calendar |
EVENT | Event |
DOCUMENT | Document |
DISPLAY_ALARM | Display notification alarm |
MAIL_ALARM | Email alarm |
OPEN_FILE_ALARM | Open file alarm |
SOUND_ALARM | Sound alarm |
Usage:
# Create an event using make()
event = calendar.make(
XACalendarApplication.ObjectType.EVENT,
properties={
"summary": "New Meeting",
"start_date": datetime.now(),
"end_date": datetime.now() + timedelta(hours=1)
}
)---
Quick Reference Tables
Common Operations
| Task | Code |
|---|---|
| Get Calendar app | calendar = PyXA.Application("Calendar") |
| Get all calendars | calendars = calendar.calendars() |
| Get calendar by name | cal = calendar.calendars().by_name("Work") |
| Get default calendar | default = calendar.default_calendar |
| Create new calendar | cal = calendar.new_calendar("My Calendar") |
| Create event | event = calendar.new_event("Meeting", start, end) |
| Get today's events | events = cal.events_today() |
| Get events in range | events = cal.events_in_range(start, end) |
| Get event attendees | attendees = event.attendees() |
| Move event | event.move_to(other_calendar) |
| Duplicate event | copy = event.duplicate_to(other_calendar) |
| Delete event | event.delete() |
| Switch view | calendar.switch_view_to(ViewType.WEEK) |
| Subscribe to calendar | cal = calendar.subscribe_to(url) |
| Reload calendars | calendar.reload_calendars() |
Property Access Patterns
# Single object property access
event = cal.events()[0]
print(event.summary)
print(event.start_date)
print(event.location)
# Bulk access on lists (returns list)
events = cal.events()
print(events.summary()) # -> list[str]
print(events.start_date()) # -> list[datetime]
print(events.location()) # -> list[str]
# Filtering
meetings = events.by_summary("Meeting")
all_day = events.by_allday_event(True)Date Range Queries
from datetime import datetime, timedelta
# Today's events
today = cal.events_today()
# This week
start = datetime.now()
end = start + timedelta(days=7)
this_week = cal.events_in_range(start, end)
# Specific month
jan_start = datetime(2025, 1, 1)
jan_end = datetime(2025, 1, 31, 23, 59, 59)
january_events = cal.events_in_range(jan_start, jan_end)Creating Events with Properties
from datetime import datetime, timedelta
# Basic event
event = cal.new_event(
"Team Standup",
datetime(2025, 1, 15, 9, 0),
datetime(2025, 1, 15, 9, 30)
)
# Event via application with calendar selection
event = calendar.new_event(
summary="Quarterly Review",
start_date=datetime(2025, 3, 15, 14, 0),
end_date=datetime(2025, 3, 15, 16, 0),
calendar=calendar.calendars().by_name("Work")
)---
See Also
- PyXA Calendar Documentation - Official PyXA documentation
- calendar-basics.md - JXA fundamentals for Calendar
- calendar-recipes.md - Common automation patterns
- calendar-advanced.md - EventKit bridge and advanced patterns
- eventkit-query.md - EventKit query examples
Calendar JXA recipes
Create recurring event
const cal = Cal.calendars.byName("Work");
const start = new Date();
const end = new Date(start.getTime() + 30 * 60000);
cal.events.push(Cal.Event({
summary: "Daily standup",
startDate: start,
endDate: end,
recurrence: "FREQ=DAILY;INTERVAL=1"
}));Add alarm
const evt = cal.events[0];
const alarm = Cal.DisplayAlarm({ triggerInterval: -900 }); // 15 min
evt.displayAlarms.push(alarm);Add multiple alerts (1d, 1h, 15m)
const evt = cal.events[0];
evt.displayAlarms.push(Cal.DisplayAlarm({ triggerInterval: -86400 })); // 1 day
evt.displayAlarms.push(Cal.DisplayAlarm({ triggerInterval: -3600 })); // 1 hour
evt.displayAlarms.push(Cal.DisplayAlarm({ triggerInterval: -900 })); // 15 minMove event (delete + recreate pattern)
const evt = cal.events[0];
const props = {
summary: evt.summary(),
startDate: new Date(Date.now() + 3600 * 1000),
endDate: new Date(Date.now() + 7200 * 1000)
};
cal.events.push(Cal.Event(props));
evt.delete();EventKit create event with recurrence (JXA + ObjC)
ObjC.import('EventKit');
ObjC.import('Foundation');
const store = $.EKEventStore.alloc.init;
let granted = false;
const g = $.dispatch_group_create();
$.dispatch_group_enter(g);
store.requestAccessToEntityTypeCompletion($.EKEntityTypeEvent, (ok, err) => {
granted = ok;
$.dispatch_group_leave(g);
});
$.dispatch_group_wait(g, $.DISPATCH_TIME_FOREVER);
if (!granted) throw new Error("Calendar access denied");
// Find calendar
const cals = store.calendarsForEntityType($.EKEntityTypeEvent);
let target = null;
for (let i = 0; i < cals.count; i++) {
const c = cals.objectAtIndex(i);
if (c.title.js === "Work") { target = c; break; }
}
if (!target) throw new Error("Calendar 'Work' not found");
// Create event
const evt = $.EKEvent.eventWithEventStore(store);
evt.title = "JXA EventKit";
evt.startDate = $.NSDate.dateWithTimeIntervalSinceNow(3600);
evt.endDate = $.NSDate.dateWithTimeIntervalSinceNow(7200);
evt.calendar = target;
// Recurrence: weekly on Mon/Fri
const mon = $.EKRecurrenceDayOfWeek.dayOfWeek(2);
const fri = $.EKRecurrenceDayOfWeek.dayOfWeek(6);
const rule = $.EKRecurrenceRule.alloc.initRecurrenceWithFrequencyIntervalDaysOfTheWeekDaysOfTheMonthMonthsOfTheYearWeeksOfTheYearDaysOfTheYearSetPositionsEnd(
$.EKRecurrenceFrequencyWeekly,
1,
[mon, fri],
$.nil, $.nil, $.nil, $.nil, $.nil,
$.nil
);
evt.addRecurrenceRule(rule);
const err = $();
const ok = store.saveEventSpanCommitError(evt, $.EKSpanThisEvent, true, err);
if (!ok) console.log("Save failed: " + err.localizedDescription.js);EventKit exceptions and occurrences (notes)
Modify a single occurrence
// Given an eventIdentifier for a recurring event
const id = "<event-id>";
const master = store.eventWithIdentifier(id);
// Fetch occurrence on a specific date
const cal = $.NSCalendar.currentCalendar;
const comps = cal.componentsFromDate($.NSCalendarUnitYear | $.NSCalendarUnitMonth | $.NSCalendarUnitDay, master.startDate);
// adjust comps to target occurrence date
// build date for that occurrence window, then:
const occ = store.eventWithIdentifier(master.eventIdentifier).copy();
// In practice, use eventStore.eventsMatchingPredicate with date window to get the occurrence
// Then modify and save with EKSpanFutureEvents or EKSpanThisEventCancel a single occurrence
- Find the occurrence (via predicate around that date) and use
store.removeEventSpanCommitError(occ, $.EKSpanThisEvent, true, err).
Notes:
- EventKit requires precise date windows to fetch individual occurrences.
- For complex exception sets, enumerate occurrences via predicate and modify/remove selectively.
EventKit: cancel a single occurrence
ObjC.import('EventKit');
ObjC.import('Foundation');
const store = $.EKEventStore.alloc.init;
let granted = false;
const g = $.dispatch_group_create();
$.dispatch_group_enter(g);
store.requestAccessToEntityTypeCompletion($.EKEntityTypeEvent, (ok, err) => { granted = ok; $.dispatch_group_leave(g); });
$.dispatch_group_wait(g, $.DISPATCH_TIME_FOREVER);
if (!granted) throw new Error("Calendar access denied");
function findCalendar(title) {
const cals = store.calendarsForEntityType($.EKEntityTypeEvent);
for (let i = 0; i < cals.count; i++) {
const c = cals.objectAtIndex(i);
if (c.title.js === title) return c;
}
return null;
}
const targetCal = findCalendar("Work");
if (!targetCal) throw new Error("Calendar not found");
// Define the date window around the occurrence you want to cancel
const cal = $.NSCalendar.currentCalendar;
const startWindow = $.NSDate.dateWithTimeIntervalSinceNow(0); // now
const endWindow = $.NSDate.dateWithTimeIntervalSinceNow(7 * 24 * 3600); // next 7 days
const predicate = store.predicateForEventsWithStartDateEndDateCalendars(startWindow, endWindow, [targetCal]);
const events = store.eventsMatchingPredicate(predicate);
// Find the occurrence by title and date match
let targetOcc = null;
for (let i = 0; i < events.count; i++) {
const e = events.objectAtIndex(i);
if (e.title.js === "Weekly Sync" && e.startDate.timeIntervalSinceNow() > 0) {
targetOcc = e; break;
}
}
if (targetOcc) {
const err = $();
const ok = store.removeEventSpanCommitError(targetOcc, $.EKSpanThisEvent, true, err);
if (!ok) console.log("Remove failed: " + err.localizedDescription.js);
}Notes:
- The occurrence is identified by title and date window; refine matching as needed.
EKSpanThisEventremoves only this occurrence; the series remains.
EventKit query example (JXA + ObjC)
ObjC.import('EventKit');
ObjC.import('Foundation');
const store = $.EKEventStore.alloc.init;
let granted = false;
const group = $.dispatch_group_create();
$.dispatch_group_enter(group);
store.requestAccessToEntityTypeCompletion($.EKEntityTypeEvent, (ok, err) => {
granted = ok;
$.dispatch_group_leave(group);
});
$.dispatch_group_wait(group, $.DISPATCH_TIME_FOREVER);
if (!granted) throw new Error("Calendar access denied");
// Date range
const now = $.NSDate.date;
const end = $.NSDate.dateWithTimeIntervalSinceNow(30 * 24 * 3600);
// Pick calendar by title
const cals = store.calendarsForEntityType($.EKEntityTypeEvent);
let target = null;
for (let i = 0; i < cals.count; i++) {
const c = cals.objectAtIndex(i);
if (c.title.js === "Work") { target = c; break; }
}
if (!target) throw new Error("Calendar 'Work' not found");
// Predicate + fetch
const predicate = store.predicateForEventsWithStartDateEndDateCalendars(now, end, [target]);
const events = store.eventsMatchingPredicate(predicate);
const result = [];
for (let i = 0; i < events.count; i++) {
const e = events.objectAtIndex(i);
result.push({
title: e.title.js,
start: e.startDate.js,
id: e.eventIdentifier.js
});
}
console.log(JSON.stringify(result, null, 2));EventKit time zone example
ObjC.import('EventKit');
ObjC.import('Foundation');
const store = $.EKEventStore.alloc.init;
let granted = false;
const g = $.dispatch_group_create();
$.dispatch_group_enter(g);
store.requestAccessToEntityTypeCompletion($.EKEntityTypeEvent, (ok, err) => { granted = ok; $.dispatch_group_leave(g); });
$.dispatch_group_wait(g, $.DISPATCH_TIME_FOREVER);
if (!granted) throw new Error("Calendar access denied");
const cals = store.calendarsForEntityType($.EKEntityTypeEvent);
let target = null;
for (let i = 0; i < cals.count; i++) {
const c = cals.objectAtIndex(i);
if (c.title.js === "Work") { target = c; break; }
}
if (!target) throw new Error("Calendar 'Work' not found");
// Time zone
const tz = $.NSTimeZone.timeZoneWithName("America/New_York");
const evt = $.EKEvent.eventWithEventStore(store);
evt.title = "TZ Event";
evt.timeZone = tz;
// Build start/end using NSDateComponents (safer for TZ)
const cal = $.NSCalendar.currentCalendar;
const comps = $.NSDateComponents.alloc.init;
comps.year = 2026; comps.month = 1; comps.day = 15; comps.hour = 9; comps.minute = 0;
const start = cal.dateFromComponents(comps);
comps.hour = 10; // end time
const end = cal.dateFromComponents(comps);
evt.startDate = start;
evt.endDate = end;
evt.calendar = target;
const err = $();
const ok = store.saveEventSpanCommitError(evt, $.EKSpanThisEvent, true, err);
if (!ok) console.log("Save failed: " + err.localizedDescription.js);#!/usr/bin/env python3
"""
Calendar Event Summary Script - PyXA Implementation
Generates a summary report of calendar events for a date range
Usage: python calendar_summary.py "2024-01-01" "2024-01-31" [--calendar "Work"]
"""
import sys
import PyXA
from datetime import datetime, timedelta
from collections import defaultdict
def generate_calendar_summary(start_date_str, end_date_str, calendar_name=None):
"""Generate a summary report of calendar events"""
try:
calendar_app = PyXA.Application("Calendar")
# Parse dates
start_date = datetime.fromisoformat(start_date_str)
end_date = datetime.fromisoformat(end_date_str)
# Get calendars to analyze
calendars = []
if calendar_name:
for cal in calendar_app.calendars():
if calendar_name.lower() in cal.name.lower():
calendars = [cal]
break
else:
calendars = calendar_app.calendars()
if not calendars:
print(f"Calendar '{calendar_name}' not found" if calendar_name else "No calendars found")
return None
# Collect all events in date range
all_events = []
calendar_stats = defaultdict(int)
for calendar in calendars:
events = calendar.events()
for event in events:
event_start = event.start_date()
# Check if event falls within date range
if start_date <= event_start <= end_date:
event_data = {
'title': event.summary() or 'Untitled',
'start': event_start,
'end': event.end_date(),
'calendar': calendar.name(),
'location': event.location() or '',
'duration_hours': (event.end_date() - event_start).total_seconds() / 3600
}
all_events.append(event_data)
calendar_stats[calendar.name()] += 1
# Generate summary report
total_events = len(all_events)
total_duration = sum(event['duration_hours'] for event in all_events)
# Group by day
events_by_day = defaultdict(list)
for event in all_events:
day = event['start'].date()
events_by_day[day].append(event)
# Display summary
print(f"Calendar Summary: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
print("=" * 60)
print(f"Total Events: {total_events}")
print(f"Total Duration: {total_duration:.1f} hours")
print(f"Calendars: {', '.join(calendar_stats.keys())}")
print()
# Calendar breakdown
print("Events by Calendar:")
for cal_name, count in calendar_stats.items():
print(f" {cal_name}: {count} events")
print()
# Daily breakdown
print("Daily Breakdown:")
for day in sorted(events_by_day.keys()):
day_events = events_by_day[day]
day_duration = sum(e['duration_hours'] for e in day_events)
print(f" {day.strftime('%Y-%m-%d')}: {len(day_events)} events ({day_duration:.1f} hours)")
for event in sorted(day_events, key=lambda x: x['start']):
start_time = event['start'].strftime('%H:%M')
print(f" {start_time}: {event['title']}")
print()
# Top event titles
title_counts = defaultdict(int)
for event in all_events:
title_counts[event['title']] += 1
print("Most Common Event Titles:")
for title, count in sorted(title_counts.items(), key=lambda x: x[1], reverse=True)[:5]:
print(f" {title}: {count} times")
return {
'total_events': total_events,
'total_duration': total_duration,
'events_by_calendar': dict(calendar_stats),
'events_by_day': dict(events_by_day)
}
except Exception as e:
print(f"Error generating calendar summary: {e}")
return None
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python calendar_summary.py '2024-01-01' '2024-01-31' [--calendar 'Work']")
sys.exit(1)
start_date = sys.argv[1]
end_date = sys.argv[2]
calendar = None
# Parse optional calendar argument
for arg in sys.argv[3:]:
if arg.startswith('--calendar'):
calendar = arg.split('=')[1] if '=' in arg else sys.argv[sys.argv.index(arg) + 1]
summary = generate_calendar_summary(start_date, end_date, calendar)
sys.exit(0 if summary else 1)#!/usr/bin/env python3
"""
Create Calendar Event Script - PyXA Implementation
Creates a new calendar event
Usage: python create_calendar_event.py "Event Title" "2024-01-15 10:00" "2024-01-15 11:00" ["Event Location"]
"""
import sys
import PyXA
from datetime import datetime
def create_calendar_event(title, start_time_str, end_time_str, location=None):
"""Create a new calendar event"""
try:
calendar_app = PyXA.Application("Calendar")
# Parse datetime strings
start_time = datetime.fromisoformat(start_time_str)
end_time = datetime.fromisoformat(end_time_str)
# Get first calendar (usually default)
default_calendar = calendar_app.calendars()[0]
# Create event
event = default_calendar.events().push({
"summary": title,
"start_date": start_time,
"end_date": end_time,
"location": location or ""
})
print(f"Created event: {title}")
print(f"Time: {start_time.strftime('%Y-%m-%d %H:%M')} - {end_time.strftime('%H:%M')}")
if location:
print(f"Location: {location}")
return True
except Exception as e:
print(f"Error creating calendar event: {e}")
return False
if __name__ == "__main__":
if len(sys.argv) < 4:
print("Usage: python create_calendar_event.py 'Event Title' '2024-01-15 10:00' '2024-01-15 11:00' ['Event Location']")
sys.exit(1)
title = sys.argv[1]
start_time = sys.argv[2]
end_time = sys.argv[3]
location = sys.argv[4] if len(sys.argv) > 4 else None
success = create_calendar_event(title, start_time, end_time, location)
sys.exit(0 if success else 1)#!/usr/bin/env python3
"""
Delete Calendar Events Script - PyXA Implementation
Deletes calendar events based on criteria
Usage: python delete_calendar_events.py "event title pattern" [--dry-run]
"""
import sys
import PyXA
def delete_calendar_events(title_pattern, dry_run=False):
"""Delete calendar events matching the title pattern"""
try:
calendar_app = PyXA.Application("Calendar")
# Get all calendars
calendars = calendar_app.calendars()
deleted_count = 0
found_events = []
# Search through all calendars
for calendar in calendars:
events = calendar.events()
# Find events matching the pattern
matching_events = []
for event in events:
title = event.summary() or ""
if title_pattern.lower() in title.lower():
matching_events.append(event)
for event in matching_events:
found_events.append({
'title': event.summary(),
'start': event.start_date(),
'calendar': calendar.name()
})
if not dry_run:
try:
event.delete()
deleted_count += 1
print(f"Deleted: {event.summary()}")
except Exception as e:
print(f"Failed to delete '{event.summary()}': {e}")
else:
print(f"Would delete: {event.summary()}")
if dry_run:
print(f"\nDry run complete. Found {len(found_events)} matching events.")
else:
print(f"\nDeleted {deleted_count} events matching '{title_pattern}'")
return found_events
except Exception as e:
print(f"Error deleting calendar events: {e}")
return []
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python delete_calendar_events.py 'title pattern' [--dry-run]")
sys.exit(1)
title_pattern = sys.argv[1]
dry_run = "--dry-run" in sys.argv
events = delete_calendar_events(title_pattern, dry_run)
sys.exit(0 if events else 1)#!/usr/bin/env python3
"""
Find Free Time Slots Script - PyXA Implementation
Finds available time slots in calendar for scheduling
Usage: python find_free_slots.py "2024-01-15" [--duration 60] [--calendar "Work"]
"""
import sys
import PyXA
from datetime import datetime, timedelta, time
def find_free_slots(date_str, duration_minutes=60, calendar_name=None):
"""Find free time slots on a given date"""
try:
calendar_app = PyXA.Application("Calendar")
# Parse date
target_date = datetime.fromisoformat(date_str).date()
# Get calendars to check
calendars = []
if calendar_name:
for cal in calendar_app.calendars():
if calendar_name.lower() in cal.name.lower():
calendars = [cal]
break
else:
calendars = calendar_app.calendars()
if not calendars:
print(f"Calendar '{calendar_name}' not found" if calendar_name else "No calendars found")
return []
# Define workday (9 AM to 5 PM)
workday_start = time(9, 0)
workday_end = time(17, 0)
# Create datetime objects for the day
day_start = datetime.combine(target_date, workday_start)
day_end = datetime.combine(target_date, workday_end)
# Collect all events for the day
day_events = []
for calendar in calendars:
events = calendar.events()
for event in events:
event_start = event.start_date()
event_end = event.end_date()
# Check if event overlaps with target day
if (event_start.date() == target_date or
event_end.date() == target_date or
(event_start.date() < target_date and event_end.date() > target_date)):
# Adjust for multi-day events
actual_start = max(event_start, day_start)
actual_end = min(event_end, day_end)
if actual_start < actual_end:
day_events.append({
'start': actual_start,
'end': actual_end,
'title': event.summary()
})
# Sort events by start time
day_events.sort(key=lambda x: x['start'])
# Find free slots
free_slots = []
current_time = day_start
for event in day_events:
if current_time < event['start']:
# There's free time before this event
slot_duration = (event['start'] - current_time).total_seconds() / 60
if slot_duration >= duration_minutes:
free_slots.append({
'start': current_time,
'end': event['start'],
'duration_minutes': slot_duration
})
# Move current time to end of event
current_time = max(current_time, event['end'])
# Check for free time after last event
if current_time < day_end:
slot_duration = (day_end - current_time).total_seconds() / 60
if slot_duration >= duration_minutes:
free_slots.append({
'start': current_time,
'end': day_end,
'duration_minutes': slot_duration
})
# Display results
if free_slots:
print(f"Free {duration_minutes}-minute slots on {target_date.strftime('%Y-%m-%d')}:")
for i, slot in enumerate(free_slots, 1):
start_str = slot['start'].strftime('%H:%M')
end_str = slot['end'].strftime('%H:%M')
print(f"{i}. {start_str} - {end_str} ({slot['duration_minutes']:.0f} minutes)")
else:
print(f"No free {duration_minutes}-minute slots found on {target_date.strftime('%Y-%m-%d')}")
return free_slots
except Exception as e:
print(f"Error finding free slots: {e}")
return []
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python find_free_slots.py '2024-01-15' [--duration 60] [--calendar 'Work']")
sys.exit(1)
date_str = sys.argv[1]
duration = 60
calendar = None
# Parse optional arguments
for arg in sys.argv[2:]:
if arg.startswith('--duration'):
duration = int(arg.split('=')[1]) if '=' in arg else int(sys.argv[sys.argv.index(arg) + 1])
elif arg.startswith('--calendar'):
calendar = arg.split('=')[1] if '=' in arg else sys.argv[sys.argv.index(arg) + 1]
slots = find_free_slots(date_str, duration, calendar)
sys.exit(0 if slots else 1)#!/usr/bin/env python3
"""
List Upcoming Events Script - PyXA Implementation
Lists upcoming calendar events
Usage: python list_upcoming_events.py [days_ahead] [calendar_name]
"""
import sys
import PyXA
from datetime import datetime, timedelta
def list_upcoming_events(days_ahead=7, calendar_name=None):
"""List upcoming calendar events"""
try:
calendar_app = PyXA.Application("Calendar")
# Calculate date range
start_date = datetime.now()
end_date = start_date + timedelta(days=days_ahead)
# Get calendars to search
calendars = []
if calendar_name:
# Find specific calendar
for cal in calendar_app.calendars():
if calendar_name.lower() in cal.name.lower():
calendars = [cal]
break
else:
# Use all calendars
calendars = calendar_app.calendars()
if not calendars:
print(f"Calendar '{calendar_name}' not found" if calendar_name else "No calendars found")
return []
upcoming_events = []
# Search each calendar
for calendar in calendars:
events = calendar.events()
# Filter events in date range
for event in events:
event_start = event.start_date()
if start_date <= event_start <= end_date:
upcoming_events.append({
'title': event.summary(),
'start': event_start,
'end': event.end_date(),
'calendar': calendar.name(),
'location': event.location() or ""
})
# Sort by start time
upcoming_events.sort(key=lambda x: x['start'])
# Display results
if upcoming_events:
print(f"Upcoming events in the next {days_ahead} days:")
print("=" * 50)
for event in upcoming_events:
start_str = event['start'].strftime('%Y-%m-%d %H:%M')
end_str = event['end'].strftime('%H:%M')
print(f"📅 {event['title']}")
print(f" 🕐 {start_str} - {end_str}")
print(f" 📍 {event['calendar']}")
if event['location']:
print(f" 📌 {event['location']}")
print()
else:
print(f"No upcoming events found in the next {days_ahead} days.")
return upcoming_events
except Exception as e:
print(f"Error listing upcoming events: {e}")
return []
if __name__ == "__main__":
days = int(sys.argv[1]) if len(sys.argv) > 1 else 7
calendar = sys.argv[2] if len(sys.argv) > 2 else None
events = list_upcoming_events(days, calendar)
sys.exit(0 if events else 1)#!/usr/bin/env python3
"""Trigger Calendar Automation prompt via a read-only AppleScript call."""
import subprocess
import sys
from textwrap import dedent
APPLESCRIPT = dedent(
"""
tell application "Calendar"
activate
set accountNames to name of every calendar account
set calendarNames to name of every calendar
return "Accounts: " & (accountNames as text) & " | Calendars: " & (calendarNames as text)
end tell
"""
)
def main() -> int:
print("Requesting Automation permission for Calendar...")
result = subprocess.run(
["osascript", "-e", APPLESCRIPT],
capture_output=True,
text=True,
)
if result.stdout.strip():
print(result.stdout.strip())
if result.returncode != 0:
print(result.stderr.strip() or "Calendar check failed without error output.")
return result.returncode
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env bash
# Trigger Calendar Automation prompt via a read-only AppleScript call.
set -euo pipefail
echo "Requesting Automation permission for Calendar..."
osascript -e 'tell application "Calendar"
activate
set accountNames to name of every calendar account
set calendarNames to name of every calendar
return "Accounts: " & (accountNames as text) & " | Calendars: " & (calendarNames as text)
end tell'
echo "Calendar responded. If prompted, grant Terminal/Python permission."