| name | ekctl |
| description | Manage macOS Calendar events and Reminders using the ekctl CLI tool |
ekctl - macOS Calendar & Reminders CLI
Use the ekctl command-line tool to manage Calendar events and Reminders on macOS. Output defaults to JSON; CSV and plain-text are available on every command via --format csv and --format text.
Prerequisites
Ensure ekctl is installed:
brew tap schappim/ekctl && brew install ekctl
If not installed, guide the user to install it first.
Verify version (this skill targets ekctl >= 1.4.0 — earlier versions don't have today/tomorrow/next, --search, --availability, or --format):
ekctl --version
Workflow
1. Check for aliases first
Before any calendar/reminder operation, check if the user has aliases configured:
ekctl alias list
If aliases exist, use them instead of raw IDs. If no aliases exist, help the user set them up after listing calendars.
2. List available calendars
ekctl list calendars
This returns both event calendars (type: "event") and reminder lists (type: "reminder").
3. Set up aliases for convenience
Help users create aliases for frequently used calendars:
ekctl alias set work "CALENDAR_ID"
ekctl alias set personal "CALENDAR_ID"
ekctl alias set groceries "REMINDER_LIST_ID"
4. Perform requested operations
Use the appropriate command based on what the user wants to do. See command-reference.md for all commands and scripting-examples.md for jq patterns and integration recipes.
Quick date ranges
Three top-level subcommands cover the most common queries with no date arithmetic required. Prefer these over computing --from/--to yourself:
ekctl today --calendar work
ekctl tomorrow --calendar work
ekctl next --calendar work
ekctl next --calendar work --count 5
ekctl next --calendar work --days 7
next returns events sorted by start time and includes events currently in progress (their endDate is still in the future).
All three accept the same filter and format flags as list events, so they compose:
ekctl today --calendar work,personal --availability busy --format csv
ekctl next --calendar work --search standup --count 3 --format text
Filtering
Two filters narrow list events and (--search only) list reminders. They compose with everything else:
--search <term> — case-insensitive substring match across an event's title, location, and notes (or a reminder's title and notes). Use this instead of post-filtering through jq.
--availability busy|free|tentative|unavailable|notSupported — events only. Filters to events whose EKEventAvailability matches.
ekctl next --calendar work --search standup --days 7
ekctl today --calendar work --availability busy
ekctl list reminders --list groceries --search milk
Output formats
Every output-producing command takes --format:
json (default) — full structure, pretty-printed.
csv — RFC 4180. Header is the union of every field across the returned items, alphabetised. Nested objects flatten to dot-notated columns (calendar.id, calendar.title); nested arrays (like attendees) become a JSON-encoded cell.
text — key: value lines per item, blank line between items. Greppable.
CSV and text are auto-discovered from the same dictionary that produces JSON, so new fields appear in all three formats simultaneously — no drift.
ekctl today --calendar work --format csv > today.csv
ekctl list calendars --format text
Multi-calendar
--calendar accepts a comma-separated list. Each returned event keeps its calendar.id and calendar.title, so the merged stream is still distinguishable:
ekctl today --calendar work,personal,family
ekctl list events --calendar work,personal --from "2026-02-01T00:00:00Z" --to "2026-02-28T23:59:59Z"
Date format
For commands that take explicit --from / --to / --start / --end / --due, use ISO 8601 with timezone:
| Example | Description |
|---|
2026-01-15T09:00:00Z | 9:00 AM UTC |
2026-01-15T09:00:00+10:00 | 9:00 AM AEST |
2026-01-15T00:00:00Z | Start of day (midnight UTC) |
2026-01-15T23:59:59Z | End of day |
For "today" / "tomorrow" / "this week", use the subcommands above instead of computing dates — today/tomorrow/next are local-timezone-aware and don't rely on the BSD-only date -v+1d flag (which breaks on Linux).
If you genuinely need a custom range outside today/tomorrow/next, generate dates like this:
START="2026-01-15T09:00:00Z"
Common patterns
Get today's busy events
ekctl today --calendar work --availability busy
Get the next 3 meetings
ekctl next --calendar work --count 3
Search across all my calendars
ekctl today --calendar work,personal --search "1:1"
Create a meeting
ekctl add event \
--calendar work \
--title "Team Standup" \
--start "2026-01-15T09:00:00Z" \
--end "2026-01-15T09:30:00Z" \
--location "Conference Room A" \
--notes "Weekly sync"
Create a recurring meeting
ekctl add event \
--calendar work \
--title "Weekly 1:1" \
--start "2026-01-15T14:00:00Z" \
--end "2026-01-15T14:30:00Z" \
--recurrence-frequency weekly \
--recurrence-end-count 12
Create an all-day event
ekctl add event \
--calendar personal \
--title "Vacation Day" \
--start "2026-01-20T00:00:00Z" \
--end "2026-01-21T00:00:00Z" \
--all-day
Add a reminder with due date
ekctl add reminder \
--list personal \
--title "Call the dentist" \
--due "2026-01-16T10:00:00Z" \
--priority 1
List incomplete reminders
ekctl list reminders --list personal --completed false
Complete a reminder
ekctl complete reminder "REMINDER_ID"
Update an event
ekctl update event "EVENT_ID" --title "Renamed meeting" --location "New room"
Update a reminder
ekctl update reminder "REMINDER_ID" --due "2026-01-17T10:00:00Z" --priority 5
Output JSON shape
Events include these fields (since 1.4.0):
{
"id": "ABC123:DEF456",
"title": "Team Meeting",
"calendar": { "id": "...", "title": "Work" },
"startDate": "2026-01-15T09:00:00+11:00",
"endDate": "2026-01-15T10:00:00+11:00",
"location": "Conference Room A",
"notes": null,
"url": null,
"allDay": false,
"hasAlarms": true,
"hasRecurrenceRules": false,
"availability": "busy",
"attendees": [
{ "name": "Jane Doe", "email": "jane@example.com", "status": "accepted", "role": "required" }
]
}
Reminders include:
{
"id": "REM123-456-789",
"title": "Buy groceries",
"list": { "id": "...", "title": "Personal" },
"dueDate": "2026-01-20T17:00:00+11:00",
"completed": false,
"priority": 0,
"notes": null,
"url": null
}
JSON output handling
ekctl returns JSON by default. For most filtering needs, prefer the built-in flags over jq — they're typically clearer and don't require a date / substring round-trip:
| Need | Old way (jq) | New way (built-in) |
|---|
| Events for today | --from $TODAY --to $TOMORROW | ekctl today --calendar work |
| Title contains "standup" | ... | jq '.events[] | select(.title | contains("standup"))' | --search standup |
| Only busy events | ... | jq '.events[] | select(.availability == "busy")' | --availability busy |
| Multiple calendars | run twice + merge | --calendar work,personal |
| CSV export | ... | jq -r '... | @csv' | --format csv |
| The next event only | sort + head -n1 | ekctl next --calendar work |
jq is still the right tool for cross-cutting transformations (e.g., extracting just titles, computing durations, joining with external data). See scripting-examples.md.
Error handling
Check the status field in responses:
{
"status": "error",
"error": "Calendar not found with ID: invalid-id"
}
In CSV format, errors become a single row:
error,status
Calendar not found with ID: invalid-id,error
In text format:
error: Calendar not found with ID: invalid-id
status: error
Common errors:
- Permission denied — User needs to grant Calendar/Reminders access in System Settings → Privacy & Security.
- Calendar not found — Invalid calendar ID or alias. Run
ekctl list calendars and ekctl alias list.
- Invalid date format — Must use ISO 8601 with timezone.
--count must be a positive integer / --days must be a positive integer — next rejects non-positive values.
Anti-patterns
-
❌ Computing today/tomorrow with date -u -v+1d shell arithmetic — BSD-only, breaks on Linux containers
-
✅ Use ekctl today / ekctl tomorrow / ekctl next instead
-
❌ Post-filtering with jq '.events[] | select(.title | contains(…))'
-
✅ Use --search — same result, far simpler
-
❌ Filtering by availability with jq '.events[] | select(.availability == "busy")'
-
✅ Use --availability busy — server-side, also cleaner
-
❌ Building CSV with jq -r '... | @csv' hand-mapped column lists
-
✅ Use --format csv — picks up new fields automatically, RFC 4180 escaping built in
-
❌ Running list events twice to merge two calendars
-
✅ --calendar work,personal does it in one call, with calendar.id per event so you can still demux
-
❌ Using raw calendar IDs repeatedly without suggesting aliases
-
✅ Help users set up aliases for calendars they use often
-
❌ Ignoring the JSON status field
-
✅ Always check status and surface errors to the user
-
❌ Listing all events without a date range
-
✅ Use today / tomorrow / next, or sensible explicit --from/--to
References
- See
command-reference.md for complete command and flag documentation
- See
scripting-examples.md for jq patterns and integration recipes