| name | app-intents |
| description | Expert guidance for the App Intents framework on Apple platforms. Use when developers mention: (1) Siri or Shortcuts integration, (2) AppIntent, AppEntity, AppEnum, or AppShortcutsProvider, (3) Spotlight actions or indexed entities, (4) exposing app features to Apple Intelligence, (5) parameters, parameter summaries, or intent donation, (6) widget/control configuration intents. Not for widget timeline design itself (see widgetkit-live-activities skill). |
| license | MIT |
| metadata | {"author":"ulk","version":"1.0","source":"ulk (interne) — extrait des sections natives d'isaac (27), agents/mobile/27-isaac.md","source-version":"ios-26","maintainer":"izo"} |
App Intents
Scope: the App Intents framework (iOS 16+, unified surface for Siri, Shortcuts, Spotlight, widgets configuration, interactive widgets, Control Center, Action button, Apple Intelligence). Intent design, entities, queries, App Shortcuts, donation-free discovery.
Out of scope: the legacy SiriKit INIntent domains (messaging/CarPlay retain SiriKit), widget timeline mechanics (→ swift-widgetkit-live-activities).
Mental Model
AppIntent = a verb ("Start timer", "Add expense"). One focused action, performs in-process.
AppEntity = a noun your app exposes (a project, an order). Identifiable, queryable, displayable.
EntityQuery = how the system resolves entities (by ID, by string, suggested list).
AppShortcutsProvider = zero-setup Siri phrases; available the moment the app is installed — no donation needed.
- Everything the system shows (titles, parameter summaries, entity representations) is static metadata read at build time — no network, no user state.
Core Rules (non-negotiable)
- One intent = one outcome. No mode-switch boolean parameters that change what the intent fundamentally does; ship two intents instead.
perform() must be fast and resumable. Target < 10 s (the system cancels laggards). Long work → kick off, return .result(dialog:) describing what started; never block on user-invisible network retries.
- Declare thread affinity, don't guess it. UI-touching intents:
@MainActor func perform(). Everything else stays off the main actor.
- Titles and summaries are static.
static let title / static var parameterSummary cannot depend on runtime state — write them so they read naturally in the Shortcuts editor ("Add ⟨amount⟩ to ⟨budget⟩").
- Every user-facing entity gets a
DisplayRepresentation (title + optional subtitle/image). The raw id must never leak into UI.
- Queries must handle deleted/stale IDs gracefully — return
[], never throw for a missing entity (Shortcuts persist entity IDs for years).
- Use
@Parameter(requestValueDialog:) + needsValueError for missing input instead of failing; Siri then asks a follow-up.
- App Shortcuts: ≤ 10 per app, every phrase contains
\(.applicationName). Phrases are compiled statically — no string interpolation of runtime values (entity parameters use \(\.$param)).
openAppWhenRun = true only when the outcome is inherently visual. Background execution is the default and the point of the framework.
- Never fabricate availability. Interactive widgets need iOS 17; ControlWidget intents iOS 18; Apple Intelligence entity surfacing iOS 18+; interactive snippets iOS 26. Tag versions in code comments when guarding.
Canonical Patterns
Intent + entity + query
struct Project: AppEntity, Sendable {
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Project"
static let defaultQuery = ProjectQuery()
let id: UUID
let name: String
var displayRepresentation: DisplayRepresentation { .init(title: "\(name)") }
}
struct ProjectQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [Project] {
await ProjectStore.shared.projects(ids: identifiers)
}
func suggestedEntities() async throws -> [Project] {
await ProjectStore.shared.recentProjects(limit: 5)
}
}
struct StartTimerIntent: AppIntent {
static let title: LocalizedStringResource = "Start Timer"
static var parameterSummary: some ParameterSummary {
Summary("Start a timer for \(\.$project)")
}
@Parameter(title: "Project") var project: Project
func perform() async throws -> some IntentResult & ProvidesDialog {
try await TimerService.shared.start(projectID: project.id)
return .result(dialog: "Timer started for \(project.name).")
}
}
App Shortcuts (installed = discoverable, zero donation)
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: StartTimerIntent(),
phrases: ["Start a timer in \(.applicationName)",
"Track \(\.$project) in \(.applicationName)"],
shortTitle: "Start Timer",
systemImageName: "timer"
)
}
}
String-searchable entities (Spotlight / Siri "find")
Conform the query to EntityStringQuery (entities(matching:)) and, for indexed content, IndexedEntity (iOS 18+) so Spotlight and Apple Intelligence can surface instances without the app running.
Widget configuration intent
WidgetConfigurationIntent is just an AppIntent subprotocol — same parameters/queries; the widget receives it in AppIntentTimelineProvider (see swift-widgetkit-live-activities).
Common AI Mistakes
| ❌ Generated pattern | Why it fails | ✅ Correct |
|---|
INIntent / Intents extension for new features | Legacy SiriKit; App Intents replaced it (iOS 16+) | AppIntent in the app target |
"Donate" calls everywhere (donate(), INInteraction) | App Shortcuts need no donation | AppShortcutsProvider static phrases |
Runtime values in phrases | Phrases compile statically → silently unregistered | \(.applicationName) + \(\.$parameter) only |
Throwing from entities(for:) on unknown ID | Breaks users' saved Shortcuts | Return the subset that still exists |
perform() doing UI work off-main | Crash / undefined | @MainActor on the method |
One giant "DoThing" intent with a mode enum | Unusable in Shortcuts editor, un-summarizable | One intent per verb |
| Dialog text built from unlocalized string concat | Siri reads it aloud, unlocalized | LocalizedStringResource + interpolation |
Checklist before shipping