Skip to main content

Siri & Shortcuts

How LucidPal integrates with Siri using the AppIntents framework.

Overview

LucidPal registers ten Siri intents via the AppIntents framework. Calendar and AI intents use a handoff pattern: they store a pending query in UserDefaults, tell Siri a brief spoken confirmation, and open the app. The app picks up the pending query when its scene becomes active. Four background-action intents — SaveNoteIntent, FindContactIntent, SetReminderIntent, and DeleteCalendarEventIntent — run entirely without opening the app (openAppWhenRun: false) and write directly to the app's shared document storage.

User: "Add dentist Friday at 10am to LucidPal"

AddCalendarEventIntent.perform()

UserDefaults["pm_siri_pending_event"] = "Add dentist Friday at 10am"

return .result(dialog: "Opening LucidPal to add dentist Friday at 10am.")

App foregrounds → LucidPalApp reads UserDefaults key

SessionListViewModel.handleSiriEvent() → CreateEventSheet shown

User confirms → CalendarService.createEvent() → EKEventStore

Intent Inventory

IntentPatternTrigger phrasesUser parameter
StartVoiceIntenthandoff"Talk to LucidPal", "Open LucidPal voice", "Start LucidPal voice", "Listen with LucidPal"— (sets pendingVoiceStart flag)
AskLucidPalIntenthandoff"Ask LucidPal [question]"@Parameter query: String
CheckCalendarIntenthandoff"Check my LucidPal calendar", "What's on my LucidPal calendar"
AddCalendarEventIntenthandoff"Add [event] to LucidPal"@Parameter event: String
FindFreeTimeIntenthandoff"Find free time in LucidPal"
DeleteCalendarEventIntentbackground"Delete [event] in LucidPal", "Delete event in LucidPal"@Parameter eventName: String
UndoLastDeletionIntenthandoff"Undo my last LucidPal action", "Undo what I just did in LucidPal"
AgentTaskIntenthandoff"Ask LucidPal Agent"@Parameter task: String
SaveNoteIntentbackground"Save note to LucidPal", "Add note to LucidPal"@Parameter title: String, @Parameter content: String
FindContactIntentbackground"Find contact in LucidPal", "Get phone number from LucidPal"@Parameter name: String
SetReminderIntentbackground"Set reminder in LucidPal"@Parameter title: String, @Parameter body: String?, @Parameter at: Date

Handoff Keys

Most intents write to UserDefaults and are consumed when the scene activates:

KeyIntentConsumer
pm_siri_pending_queryAskLucidPalIntentconsumePendingSiriQuery()scheduleSiriQuery()
pm_siri_pending_eventAddCalendarEventIntentconsumePendingSiriEvent()scheduleCreateEvent()
pm_pending_agent_taskAgentTaskIntentconsumePendingAgentTask()agentViewModel.submitTask()
pm_pending_voice_startStartVoiceIntentconsumePendingVoiceStart()scheduleVoiceSession()

UndoLastDeletionIntent reads from SiriContextStore (a separate UserDefaults key) which records the last calendar action with full event data for proper undo.

Keys are cleared immediately after forwarding to prevent replaying on subsequent launches.

StartVoiceIntent Flow

StartVoiceIntent is unique — it opens the app and starts the microphone without requiring a text query:

// StartVoiceIntent.perform()
UserDefaults.standard.set(true, forKey: "pm_pending_voice_start")

// LucidPalApp.consumePendingVoiceStart()
sessionListViewModel.scheduleVoiceSession()

// SessionListViewModel.scheduleVoiceSession()
// Creates a new session, sets pendingVoiceSessionMeta

// SessionListView watches pendingVoiceSessionMeta
// Sets pendingVoiceSessionID = meta.id, pushes onto navigation stack
// ChatSessionContainer receives startWithVoice: true → mic auto-starts

Audio Feedback (ProvidesDialog)

Every intent conforms to ProvidesDialog, giving Siri a spoken response:

func perform() async throws -> some IntentResult & ProvidesDialog {
// ...store pending query...
return .result(dialog: "Opening LucidPal.")
}

Without ProvidesDialog, Siri would show a generic "Done" card with no audio confirmation.

AppShortcutsProvider

LucidPalShortcuts registers suggested phrases with the system (iOS 16.4+). Phrases use .applicationName interpolation so they survive app renames:

AppShortcut(
intent: CheckCalendarIntent(),
phrases: [
"Check my \(.applicationName) calendar",
"What's on my \(.applicationName) schedule",
"Show my \(.applicationName) schedule"
],
shortTitle: "Check Calendar",
systemImageName: "calendar"
)

On iOS < 16.4, the intents still work but users must add the shortcuts manually via the Shortcuts app.

After a Siri handoff intent fires, LucidPalApp reads the UserDefaults key and calls SessionListViewModel.scheduleSiriQuery(_:). This method:

  1. Creates a new ChatSession and saves it via SessionManager.
  2. Stores the query string in pendingQueryBySessionID[session.id].
  3. Sets siriNavigationMeta = session.meta — a @Published ChatSessionMeta? on SessionListViewModel.

SessionListView observes siriNavigationMeta to navigate to the new session. ChatSessionContainer reads pendingQueryBySessionID[session.id] and pre-fills the text field (or auto-sends the message), then removes the entry.

// SessionListViewModel
@Published var siriNavigationMeta: ChatSessionMeta?
var pendingQueryBySessionID: [UUID: String] = [:]

func scheduleSiriQuery(_ query: String) {
let session = ChatSession.new()
sessionManager.save(session)
pendingQueryBySessionID[session.id] = query
siriNavigationMeta = session.meta
}

SiriContextStore

SiriContextStore is a lightweight persistence layer that records the last calendar action taken — whether triggered by Siri or performed inside the app. UndoLastDeletionIntent reads from this store to know what to reverse.

Data model

struct SiriLastAction: Codable {
let type: ActionType // created | deleted | updated | rescheduled
let eventTitle: String
let eventStart: Date?
let eventEnd: Date?
let calendarName: String
let calendarIdentifier: String
let isAllDay: Bool
let location: String?
let notes: String?
let eventIdentifier: String // EKEvent.eventIdentifier used for undo
let timestamp: Date
}

ActionType is a String raw-value enum with four cases: created, deleted, updated, rescheduled.

Storage

SiriContextStore is a caseless enum with three static methods backed by UserDefaults key "pm_siri_last_action". JSON encoding/decoding uses JSONEncoder / JSONDecoder.

MethodSignatureDescription
writewrite(_ action: SiriLastAction)Encodes and persists the action
readread() -> SiriLastAction?Decodes and returns the last action, or nil
clearclear()Removes the stored value

Write sites

Call siteAction type written
CalendarActionController.createEvent().created (after EKEvent is saved)
ChatViewModel+CalendarConfirmation.confirmDeletion().deleted (after user confirms delete card)
ChatViewModel+CalendarConfirmation.confirmUpdate().updated or .rescheduled (after user confirms update card)

UndoLastDeletionIntent behaviour

UndoLastDeletionIntent.perform() reads SiriContextStore.read() then branches on type:

TypeBehaviour
.deletedRecreates the event via CalendarService; asks user to confirm first
.createdDeletes the event using eventIdentifier; asks user to confirm first
.updated / .rescheduledReturns a dialog informing the user that undo of edits isn't supported yet
nil (nothing stored)Returns a dialog stating there is no recent action to undo