skills/apple-calendar/calendar-core/SKILL.md
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.
npx skillsauth add aashari/ai-agent-skills calendar-coreInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
$HOME/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb
WAL 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 ..."
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 ))
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.
| 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 |
0 — none / tentative1 — confirmed2 — cancelled (exclude these)0 — none (own events)3 — pending invite (not yet accepted)0 — local1 — local5 — "Found in Mail" — always exclude (auto-created duplicates from email invites)7 — attendee8 — organizer0 — needs-action1 — accepted2 — declined3 — tentative0 — non-participant / unknown1 — required2 — optional1 — daily2 — weekly3 — monthly4 — yearlyAlways 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 only
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.
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;
data-ai
Show work emails only, filtered to Exchange/EWS accounts and corporate email domains. Digest with priorities. Use when user asks about work email, work inbox, or wants to separate work from personal mail. Arguments: optional date range or "today", "yesterday", "this week".
testing
Intelligent inbox triage — surface the most important emails across all accounts, prioritized by urgency and requiring attention. Use when user wants a smart overview of what needs their attention, asks "what's important in my email", or wants help deciding what to read first. Arguments: optional time window (default: last 48 hours) or account filter.
data-ai
Find flight bookings, hotel reservations, travel itineraries, and booking confirmations from email. Use when user asks about upcoming trips, travel plans, booking references, flight details, or hotel reservations. Arguments: optional destination, airline, date range, or booking service.
testing
Show who sends the most email, communication frequency analysis, and relationship mapping. Use when user asks who emails them most, top contacts, communication patterns, or wants to understand their email social graph. Arguments: optional time range (default: last 90 days), account filter, or "humans only" to exclude automated senders.