Background technical reference for Apple Calendar data on macOS. Auto-loaded when any calendar-* skill executes. Contains DB path, CoreData epoch conversion, schema, canonical query, and filtering rules.
Install
npx skillscat add aashari/ai-agent-skills/calendar-core Install via the SkillsCat registry.
Apple Calendar — Local Data Reference
Database Location
$HOME/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedbWAL mode — always accompanied by .sqlitedb-shm and .sqlitedb-wal. Reads work fine against the live database without locking.
DB="$HOME/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb"
sqlite3 "$DB" "SELECT ..."Critical: CoreData Epoch (NOT Unix Epoch)
All timestamps are seconds since January 1, 2001, not 1970. Convert in SQLite:
datetime(field + 978307200, 'unixepoch', 'localtime')Convert current time to CoreData timestamp in bash:
NOW_CD=$(( $(date +%s) - 978307200 ))
FUTURE_CD=$(( $(date +%s) + N * 86400 - 978307200 ))Critical: OccurrenceCache for Recurring Events
Never query CalendarItem.start_date directly for date-range queries. It only holds the original first occurrence. Future instances of recurring events only exist in OccurrenceCache.
Always use OccurrenceCache as the entry point for any time-windowed query.
occurrence_start_date is NULL for some recurring events — use COALESCE(occurrence_start_date, occurrence_date) for start time.
SQLite Schema
Key Tables
| Table | Key Columns |
|---|---|
| CalendarItem | ROWID, summary, start_date, end_date, start_tz, end_tz, all_day, description, url, conference_url, location_id, has_attendees, has_recurrences, status, invitation_status, hidden, organizer_id, self_attendee_id, calendar_id |
| OccurrenceCache | event_id (→CalendarItem.ROWID), occurrence_date, occurrence_start_date, occurrence_end_date |
| Calendar | ROWID, title, color, type, store_id |
| Store | ROWID, name (email/account name), type, disabled |
| Location | ROWID, title, latitude, longitude, address |
| Participant | ROWID, owner_id (→CalendarItem.ROWID), email, entity_type, role, status |
| Recurrence | ROWID, owner_id, frequency, interval, specifier |
CalendarItem.status Values
0— none / tentative1— confirmed2— cancelled (exclude these)
CalendarItem.invitation_status Values
0— none (own events)3— pending invite (not yet accepted)
Store.type Values
0— local1— local5— "Found in Mail" — always exclude (auto-created duplicates from email invites)
Participant.entity_type Values
7— attendee8— organizer
Participant.status Values
0— needs-action1— accepted2— declined3— tentative
Participant.role Values
0— non-participant / unknown1— required2— optional
Recurrence.frequency Values
1— daily2— weekly3— monthly4— yearly
Standard Exclusion Filters
Always apply:
WHERE ci.hidden = 0 -- exclude hidden events
AND ci.status != 2 -- exclude cancelled events
AND s.type != 5 -- exclude "Found in Mail" duplicates
AND s.disabled = 0 -- active accounts onlyCanonical Query Template
SELECT
ci.ROWID as id,
ci.summary as title,
COALESCE(
datetime(oc.occurrence_start_date + 978307200, 'unixepoch', 'localtime'),
datetime(oc.occurrence_date + 978307200, 'unixepoch', 'localtime')
) as start_local,
datetime(oc.occurrence_end_date + 978307200, 'unixepoch', 'localtime') as end_local,
ci.all_day,
ci.start_tz as timezone,
COALESCE(l.title, '') as location,
ci.conference_url,
ci.has_attendees,
ci.has_recurrences,
ci.status,
ci.invitation_status,
c.title as calendar_name,
s.name as account_name
FROM OccurrenceCache oc
JOIN CalendarItem ci ON oc.event_id = ci.ROWID
LEFT JOIN Calendar c ON ci.calendar_id = c.ROWID
LEFT JOIN Store s ON c.store_id = s.ROWID
LEFT JOIN Location l ON ci.location_id = l.ROWID
WHERE oc.occurrence_date >= NOW_CD
AND oc.occurrence_date <= FUTURE_CD
AND ci.hidden = 0
AND ci.status != 2
AND s.type != 5
AND s.disabled = 0
GROUP BY ci.ROWID, date(oc.occurrence_date + 978307200, 'unixepoch', 'localtime')
ORDER BY oc.occurrence_date;GROUP BY ci.ROWID, date(...) deduplicates multi-day spanning events that appear once per day in OccurrenceCache.
Fetching Attendees (On-Demand Per Event)
Only fetch attendees for specific events — never bulk join, it's slow:
SELECT email, entity_type, role, status
FROM Participant
WHERE owner_id = EVENT_ROWID
ORDER BY entity_type DESC, status;Filter s.disabled = 0 to get only active accounts. Discover your active accounts with:
SELECT s.name, s.type, COUNT(*) as calendars
FROM Store s
JOIN Calendar c ON c.store_id = s.ROWID
WHERE s.type != 5 AND s.disabled = 0
GROUP BY s.ROWID
ORDER BY s.name;