Use when users ask to manage macOS Calendar events from terminal automation, including create/query/delete, date updates, recurrence setup, and strict duplicate-event prevention.
Use this skill when managing Calendar events on macOS from terminal automation:
/System/Applications/Calendar.appswift + EventKit for create/query/delete/update/recurrenceJXA can list calendars/events, but recurrence-instance mutation/deletion may be inconsistent in some environments.upcoming only (event end time >= now).title + startDate in the same calendar.TITLE variable in action and verification scripts.eventIdentifier/uid + concrete startDate, not only by title count.import Foundation
let startUTC = Date(timeIntervalSince1970: 1772884800) // Example absolute instant
print(startUTC)
swift - <<'SWIFT'
import Foundation
import EventKit
let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)
var granted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { ok, _ in granted = ok; sem.signal() }
} else {
store.requestAccess(to: .event) { ok, _ in granted = ok; sem.signal() }
}
_ = sem.wait(timeout: .now() + 20)
print(granted ? "ACCESS_GRANTED" : "ACCESS_DENIED")
SWIFT
swift - <<'SWIFT'
import Foundation
import EventKit
let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)
var granted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { ok, _ in granted = ok; sem.signal() }
} else {
store.requestAccess(to: .event) { ok, _ in granted = ok; sem.signal() }
}
_ = sem.wait(timeout: .now() + 20)
guard granted else { print("ACCESS_DENIED"); exit(2) }
for (idx, cal) in store.calendars(for: .event).enumerated() {
print("\(idx + 1). \(cal.title) [\(cal.source.title)]")
}
SWIFT
swift - <<'SWIFT'
import Foundation
import EventKit
let TITLE = "Sample Saturday Alarm"
let CALENDAR_NAME = "Home"
let START = Date(timeIntervalSince1970: 1772884800)
let END = Date(timeIntervalSince1970: 1772888400)
let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)
var granted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { ok, _ in granted = ok; sem.signal() }
} else {
store.requestAccess(to: .event) { ok, _ in granted = ok; sem.signal() }
}
_ = sem.wait(timeout: .now() + 20)
guard granted else { print("ACCESS_DENIED"); exit(2) }
guard let cal = store.calendars(for: .event).first(where: { $0.title == CALENDAR_NAME }) else {
print("CALENDAR_NOT_FOUND")
exit(1)
}
let windowStart = START.addingTimeInterval(-60)
let windowEnd = END.addingTimeInterval(60)
let pred = store.predicateForEvents(withStart: windowStart, end: windowEnd, calendars: [cal])
let dup = store.events(matching: pred).filter { ($0.title ?? "") == TITLE && $0.startDate == START }.count
print("duplicate_upcoming=\(dup)")
SWIFT
swift - <<'SWIFT'
import Foundation
import EventKit
let TITLE = "Sample Saturday Alarm"
let CALENDAR_NAME = "Home"
let START = Date(timeIntervalSince1970: 1772884800)
let END = Date(timeIntervalSince1970: 1772888400)
let NOTES = "Sample note"
let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)
var granted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { ok, _ in granted = ok; sem.signal() }
} else {
store.requestAccess(to: .event) { ok, _ in granted = ok; sem.signal() }
}
_ = sem.wait(timeout: .now() + 20)
guard granted else { print("ACCESS_DENIED"); exit(2) }
guard let cal = store.calendars(for: .event).first(where: { $0.title == CALENDAR_NAME }) else {
print("CALENDAR_NOT_FOUND")
exit(1)
}
let pred = store.predicateForEvents(withStart: START.addingTimeInterval(-60), end: END.addingTimeInterval(60), calendars: [cal])
let dup = store.events(matching: pred).filter { ($0.title ?? "") == TITLE && $0.startDate == START }.count
if dup > 0 {
print("DUPLICATE_EVENT_BLOCKED")
exit(0)
}
let ev = EKEvent(eventStore: store)
ev.calendar = cal
ev.title = TITLE
ev.startDate = START
ev.endDate = END
ev.notes = NOTES
try store.save(ev, span: .thisEvent, commit: true)
print("created=\(TITLE)")
print("event_id=\(ev.eventIdentifier ?? "nil")")
SWIFT
Verification after create:
swift - <<'SWIFT'
import Foundation
import EventKit
let TITLE = "Sample Saturday Alarm"
let CALENDAR_NAME = "Home"
let START = Date(timeIntervalSince1970: 1772884800)
let END = Date(timeIntervalSince1970: 1772888400)
let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)
var granted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { ok, _ in granted = ok; sem.signal() }
} else {
store.requestAccess(to: .event) { ok, _ in granted = ok; sem.signal() }
}
_ = sem.wait(timeout: .now() + 20)
guard granted else { print("ACCESS_DENIED"); exit(2) }
guard let cal = store.calendars(for: .event).first(where: { $0.title == CALENDAR_NAME }) else {
print("CALENDAR_NOT_FOUND")
exit(1)
}
let fmt = ISO8601DateFormatter()
fmt.timeZone = TimeZone.current
let pred = store.predicateForEvents(withStart: START.addingTimeInterval(-60), end: END.addingTimeInterval(60), calendars: [cal])
let matches = store.events(matching: pred).filter { ($0.title ?? "") == TITLE && $0.startDate == START }
for ev in matches {
print("start=\(fmt.string(from: ev.startDate)) end=\(fmt.string(from: ev.endDate))")
}
print("visible_total=\(matches.count)")
SWIFT
Query upcoming events by exact title:
swift - <<'SWIFT'
import Foundation
import EventKit
let TITLE = "Sample Saturday Alarm"
let calendarSpanDays = 365
let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)
var granted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { ok, _ in granted = ok; sem.signal() }
} else {
store.requestAccess(to: .event) { ok, _ in granted = ok; sem.signal() }
}
_ = sem.wait(timeout: .now() + 20)
guard granted else { print("ACCESS_DENIED"); exit(2) }
let now = Date()
let future = Calendar.current.date(byAdding: .day, value: calendarSpanDays, to: now)!
let pred = store.predicateForEvents(withStart: now, end: future, calendars: store.calendars(for: .event))
let fmt = ISO8601DateFormatter()
fmt.timeZone = TimeZone.current
let matches = store.events(matching: pred).filter { ($0.title ?? "") == TITLE && $0.endDate >= now }
for ev in matches.sorted(by: { $0.startDate < $1.startDate }) {
print("calendar=\(ev.calendar.title) start=\(fmt.string(from: ev.startDate)) end=\(fmt.string(from: ev.endDate))")
}
print("visible_total=\(matches.count)")
SWIFT
Delete upcoming events by exact title:
swift - <<'SWIFT'
import Foundation
import EventKit
let TITLE = "Sample Saturday Alarm"
let calendarSpanDays = 365
let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)
var granted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { ok, _ in granted = ok; sem.signal() }
} else {
store.requestAccess(to: .event) { ok, _ in granted = ok; sem.signal() }
}
_ = sem.wait(timeout: .now() + 20)
guard granted else { print("ACCESS_DENIED"); exit(2) }
let now = Date()
let future = Calendar.current.date(byAdding: .day, value: calendarSpanDays, to: now)!
let pred = store.predicateForEvents(withStart: now, end: future, calendars: store.calendars(for: .event))
let targets = store.events(matching: pred).filter { ($0.title ?? "") == TITLE && $0.endDate >= now }
var removed = 0
for ev in targets {
try store.remove(ev, span: .thisEvent, commit: false)
removed += 1
}
if removed > 0 {
try store.commit()
}
print("removed=\(removed)")
SWIFT
If recurrence events remain due provider sync behavior, run a second pass using eventIdentifier from the verification output and delete by identifier.
Verification after delete:
swift - <<'SWIFT'
import Foundation
import EventKit
let TITLE = "Sample Saturday Alarm"
let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)
var granted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { ok, _ in granted = ok; sem.signal() }
} else {
store.requestAccess(to: .event) { ok, _ in granted = ok; sem.signal() }
}
_ = sem.wait(timeout: .now() + 20)
guard granted else { print("ACCESS_DENIED"); exit(2) }
let now = Date()
let future = Calendar.current.date(byAdding: .day, value: 365, to: now)!
let pred = store.predicateForEvents(withStart: now, end: future, calendars: store.calendars(for: .event))
let remaining = store.events(matching: pred).filter { ($0.title ?? "") == TITLE && $0.endDate >= now }.count
print("remaining_visible=\(remaining)")
SWIFT
Update one upcoming event by exact title + old start:
swift - <<'SWIFT'
import Foundation
import EventKit
let TITLE = "Sample Saturday Alarm"
let OLD_START = Date(timeIntervalSince1970: 1772884800)
let NEW_START = Date(timeIntervalSince1970: 1772971200)
let NEW_END = Date(timeIntervalSince1970: 1772974800)
let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)
var granted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { ok, _ in granted = ok; sem.signal() }
} else {
store.requestAccess(to: .event) { ok, _ in granted = ok; sem.signal() }
}
_ = sem.wait(timeout: .now() + 20)
guard granted else { print("ACCESS_DENIED"); exit(2) }
let pred = store.predicateForEvents(withStart: OLD_START.addingTimeInterval(-60), end: OLD_START.addingTimeInterval(60), calendars: store.calendars(for: .event))
guard let target = store.events(matching: pred).first(where: { ($0.title ?? "") == TITLE && $0.startDate == OLD_START }) else {
print("NOT_FOUND")
exit(1)
}
target.startDate = NEW_START
target.endDate = NEW_END
try store.save(target, span: .thisEvent, commit: true)
print("updated=\(TITLE)")
SWIFT
Use recurrence on an event:
swift - <<'SWIFT'
import Foundation
import EventKit
let TITLE = "Sample Medication"
let START = Date(timeIntervalSince1970: 1772884800)
let END = Date(timeIntervalSince1970: 1772888400)
let RECUR_END = Date(timeIntervalSince1970: 1804420800)
let store = EKEventStore()
let sem = DispatchSemaphore(value: 0)
var granted = false
if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { ok, _ in granted = ok; sem.signal() }
} else {
store.requestAccess(to: .event) { ok, _ in granted = ok; sem.signal() }
}
_ = sem.wait(timeout: .now() + 20)
guard granted else { print("ACCESS_DENIED"); exit(2) }
let pred = store.predicateForEvents(withStart: START.addingTimeInterval(-60), end: END.addingTimeInterval(60), calendars: store.calendars(for: .event))
guard let ev = store.events(matching: pred).first(where: { ($0.title ?? "") == TITLE && $0.startDate == START }) else {
print("NOT_FOUND")
exit(1)
}
ev.recurrenceRules = [EKRecurrenceRule(
recurrenceWith: .weekly,
interval: 1,
daysOfTheWeek: [EKRecurrenceDayOfWeek(.saturday)],
daysOfTheMonth: nil,
monthsOfTheYear: nil,
weeksOfTheYear: nil,
daysOfTheYear: nil,
setPositions: nil,
end: EKRecurrenceEnd(end: RECUR_END)
)]
try store.save(ev, span: .futureEvents, commit: true)
print("updated_recurrence")
SWIFT
If this week must move to Sunday but future remains Saturday:
Sample Medication (This Week)Sample MedicationDo not keep both as Sample Medication at the same start time.
Always include:
created, queried, deleted, updated)visible_total, removed, remaining_visible)eventIdentifier/uid and post-delete residual count