app-intents
SKILL.md
App Intents Framework
Comprehensive guide to App Intents for Siri integration, Shortcuts, Spotlight, Action Button, and interactive snippets in iOS 26.
Prerequisites
- iOS 16+ for App Intents (iOS 26 recommended)
- Xcode 26+
Framework Overview
Core Concepts
- Intents = Verbs (actions your app can perform)
- App Entities = Dynamic Nouns (content in your app)
- App Enums = Static Nouns (fixed options)
Where App Intents Appear
- Siri - Voice-activated commands
- Shortcuts - User-created automations
- Spotlight - Search suggestions and actions
- Action Button - iPhone 15 Pro hardware button
- Apple Pencil - Squeeze gesture
- Focus Filters - Customize app behavior per focus
Import
import AppIntents
Creating App Intents
Basic Intent
import AppIntents
struct OpenNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Open Note"
static var description = IntentDescription("Opens a specific note in the app")
@Parameter(title: "Note")
var note: NoteEntity
func perform() async throws -> some IntentResult {
// Open the note in your app
await NoteManager.shared.open(note.id)
return .result()
}
}
Intent with Parameters
struct CreateNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Create Note"
static var description = IntentDescription("Creates a new note with the specified content")
@Parameter(title: "Title")
var title: String
@Parameter(title: "Content", default: "")
var content: String
@Parameter(title: "Folder", optionsProvider: FolderOptionsProvider())
var folder: FolderEntity?
func perform() async throws -> some IntentResult {
let note = await NoteManager.shared.create(
title: title,
content: content,
in: folder?.id
)
return .result(value: NoteEntity(note: note))
}
struct FolderOptionsProvider: DynamicOptionsProvider {
func results() async throws -> [FolderEntity] {
let folders = await FolderManager.shared.all()
return folders.map { FolderEntity(folder: $0) }
}
}
}
Intent Results
// Simple result
return .result()
// Result with value
return .result(value: noteEntity)
// Result with dialog (for Siri)
return .result(dialog: "Note created successfully")
// Result with view snippet
return .result(
dialog: "Here's your note",
view: NoteSnippetView(note: note)
)
// Result opening app
return .result(opensIntent: OpenNoteIntent(note: noteEntity))
App Entities
Defining an Entity
import AppIntents
struct NoteEntity: AppEntity {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Note")
var id: UUID
var title: String
var content: String
var createdAt: Date
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(content.prefix(50))...",
image: .init(systemName: "doc.text")
)
}
static var defaultQuery = NoteEntityQuery()
init(note: Note) {
self.id = note.id
self.title = note.title
self.content = note.content
self.createdAt = note.createdAt
}
}
Entity Query
struct NoteEntityQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [NoteEntity] {
let notes = await NoteManager.shared.fetch(ids: identifiers)
return notes.map { NoteEntity(note: $0) }
}
func suggestedEntities() async throws -> [NoteEntity] {
let recentNotes = await NoteManager.shared.recentNotes(limit: 5)
return recentNotes.map { NoteEntity(note: $0) }
}
}
Searchable Entity Query
struct NoteEntityQuery: EntityStringQuery {
func entities(for identifiers: [UUID]) async throws -> [NoteEntity] {
let notes = await NoteManager.shared.fetch(ids: identifiers)
return notes.map { NoteEntity(note: $0) }
}
func entities(matching string: String) async throws -> [NoteEntity] {
let notes = await NoteManager.shared.search(query: string)
return notes.map { NoteEntity(note: $0) }
}
func suggestedEntities() async throws -> [NoteEntity] {
let recentNotes = await NoteManager.shared.recentNotes(limit: 5)
return recentNotes.map { NoteEntity(note: $0) }
}
}
Property Queries (iOS 17+)
struct NoteEntityQuery: EntityPropertyQuery {
static var properties = QueryProperties {
Property(\NoteEntity.$title) {
EqualToComparator { $0 }
ContainsComparator { $0 }
}
Property(\NoteEntity.$createdAt) {
LessThanComparator { $0 }
GreaterThanComparator { $0 }
}
}
static var sortingOptions = SortingOptions {
SortableBy(\NoteEntity.$title)
SortableBy(\NoteEntity.$createdAt)
}
func entities(
matching comparators: [EntityQueryComparator<NoteEntity>],
mode: ComparatorMode,
sortedBy: [EntityQuerySort<NoteEntity>],
limit: Int?
) async throws -> [NoteEntity] {
// Apply filters and sorting
var notes = await NoteManager.shared.all()
// Apply comparators
for comparator in comparators {
notes = notes.filter { comparator.evaluate($0) }
}
// Apply sorting
for sort in sortedBy {
notes.sort(by: sort.compare)
}
// Apply limit
if let limit {
notes = Array(notes.prefix(limit))
}
return notes.map { NoteEntity(note: $0) }
}
}
App Enums
Defining Enums
import AppIntents
enum NoteCategory: String, AppEnum {
case personal
case work
case ideas
case todo
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Category")
static var caseDisplayRepresentations: [NoteCategory: DisplayRepresentation] = [
.personal: DisplayRepresentation(title: "Personal", image: .init(systemName: "person")),
.work: DisplayRepresentation(title: "Work", image: .init(systemName: "briefcase")),
.ideas: DisplayRepresentation(title: "Ideas", image: .init(systemName: "lightbulb")),
.todo: DisplayRepresentation(title: "To-Do", image: .init(systemName: "checklist"))
]
}
Using Enums in Intents
struct FilterNotesIntent: AppIntent {
static var title: LocalizedStringResource = "Filter Notes"
@Parameter(title: "Category")
var category: NoteCategory
func perform() async throws -> some IntentResult {
let notes = await NoteManager.shared.filter(by: category)
return .result(value: notes.map { NoteEntity(note: $0) })
}
}
App Shortcuts
AppShortcutsProvider
import AppIntents
struct MyAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: CreateNoteIntent(),
phrases: [
"Create a note in \(.applicationName)",
"New note in \(.applicationName)",
"Add note to \(.applicationName)"
],
shortTitle: "Create Note",
systemImageName: "plus.circle"
)
AppShortcut(
intent: OpenRecentNoteIntent(),
phrases: [
"Open my recent note in \(.applicationName)",
"Show last note in \(.applicationName)"
],
shortTitle: "Recent Note",
systemImageName: "clock"
)
AppShortcut(
intent: SearchNotesIntent(),
phrases: [
"Search notes in \(.applicationName)",
"Find \(\.$query) in \(.applicationName)"
],
shortTitle: "Search",
systemImageName: "magnifyingglass"
)
}
}
Phrase Rules
- Must include
\(.applicationName)placeholder - Maximum one parameter reference per phrase
- Parameter must use
\(\.$parameterName)syntax - Keep phrases natural and varied
Automatic Registration
App Shortcuts are automatically registered when:
- App is installed
- App is updated
- AppShortcutsProvider is modified
Interactive Snippets (MicroUI)
Result Snippets
struct ShowNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Show Note"
@Parameter(title: "Note")
var note: NoteEntity
func perform() async throws -> some IntentResult & ShowsSnippetView {
return .result(
dialog: "Here's your note",
view: NoteSnippetView(note: note)
)
}
}
struct NoteSnippetView: View {
let note: NoteEntity
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(note.title)
.font(.headline)
Text(note.content)
.font(.body)
.lineLimit(3)
Text(note.createdAt, style: .relative)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
}
}
Confirmation Snippets
struct DeleteNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Delete Note"
@Parameter(title: "Note")
var note: NoteEntity
func perform() async throws -> some IntentResult {
try await requestConfirmation(
result: .result(
dialog: "Are you sure you want to delete this note?",
view: DeleteConfirmationView(note: note)
)
)
await NoteManager.shared.delete(note.id)
return .result(dialog: "Note deleted")
}
}
struct DeleteConfirmationView: View {
let note: NoteEntity
var body: some View {
VStack(spacing: 12) {
Image(systemName: "trash")
.font(.largeTitle)
.foregroundStyle(.red)
Text("Delete '\(note.title)'?")
.font(.headline)
Text("This action cannot be undone.")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
}
}
Interactive Snippet Buttons
struct NoteActionsSnippetView: View {
let note: NoteEntity
var body: some View {
VStack(spacing: 12) {
Text(note.title)
.font(.headline)
HStack(spacing: 16) {
Button(intent: EditNoteIntent(note: note)) {
Label("Edit", systemImage: "pencil")
}
Button(intent: ShareNoteIntent(note: note)) {
Label("Share", systemImage: "square.and.arrow.up")
}
}
.buttonStyle(.bordered)
}
.padding()
}
}
Snippet Design Guidelines
- Keep snippets compact (fits in Siri/Spotlight card)
- Use larger text for readability
- High contrast colors
- Clear, tappable buttons
- Concise content
Foreground vs Background Execution
Background Execution (Default)
struct QuickNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Quick Note"
@Parameter(title: "Content")
var content: String
// Runs in background without opening app
func perform() async throws -> some IntentResult {
await NoteManager.shared.quickCreate(content: content)
return .result(dialog: "Note saved!")
}
}
Foreground Execution
struct EditNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Edit Note"
// Opens app when intent runs
static var openAppWhenRun = true
@Parameter(title: "Note")
var note: NoteEntity
func perform() async throws -> some IntentResult {
// App is now in foreground
await NoteManager.shared.openEditor(for: note.id)
return .result()
}
}
Conditional Foreground
struct ViewNoteIntent: AppIntent {
static var title: LocalizedStringResource = "View Note"
@Parameter(title: "Note")
var note: NoteEntity
@Parameter(title: "Open in App")
var openInApp: Bool
static var openAppWhenRun: Bool {
// Dynamically determined
return false
}
func perform() async throws -> some IntentResult {
if openInApp {
// Return result that opens app
return .result(opensIntent: OpenNoteIntent(note: note))
} else {
// Return snippet view
return .result(view: NoteSnippetView(note: note))
}
}
}
Dependency Injection
@Dependency Property Wrapper
struct CreateNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Create Note"
@Dependency
var noteService: NoteService
@Parameter(title: "Title")
var title: String
func perform() async throws -> some IntentResult {
let note = try await noteService.create(title: title)
return .result(value: NoteEntity(note: note))
}
}
Registering Dependencies
Register dependencies early in app lifecycle:
@main
struct MyApp: App {
init() {
// Register dependencies for App Intents
AppDependencyManager.shared.add(dependency: NoteService.shared)
AppDependencyManager.shared.add(dependency: FolderService.shared)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Focus Filters
Defining a Focus Filter
import AppIntents
struct NoteFocusFilter: SetFocusFilterIntent {
static var title: LocalizedStringResource = "Set Note Filter"
static var description = IntentDescription("Filter notes during this Focus")
@Parameter(title: "Show Categories")
var categories: [NoteCategory]?
@Parameter(title: "Hide Work Notes")
var hideWork: Bool?
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "Note Filter")
}
func perform() async throws -> some IntentResult {
// Apply filter settings
if let categories {
FocusState.shared.visibleCategories = categories
}
if let hideWork {
FocusState.shared.hideWorkNotes = hideWork
}
return .result()
}
}
Spotlight Integration
Featured in Spotlight
Intents automatically appear in Spotlight when:
- User searches for related terms
- App Shortcuts are defined
- Entities match search queries
Donating Activities
import Intents
func userViewedNote(_ note: Note) {
let activity = NSUserActivity(activityType: "com.app.viewNote")
activity.title = note.title
activity.userInfo = ["noteId": note.id.uuidString]
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
// Associate with App Intent
activity.shortcutAvailability = .sleepInBed
UIApplication.shared.currentUserActivity = activity
}
Action Button & Apple Pencil
Action Button Intent
struct QuickCaptureIntent: AppIntent {
static var title: LocalizedStringResource = "Quick Capture"
static var description = IntentDescription("Quickly capture a thought")
// Good for Action Button - fast execution
func perform() async throws -> some IntentResult & OpensIntent {
// Create new capture and open editor
let capture = await CaptureManager.shared.createQuick()
return .result(opensIntent: OpenCaptureIntent(capture: capture))
}
}
Users configure Action Button in Settings → Action Button → Shortcut → [Your App Shortcut]
Apple Pencil Squeeze
Same intents work for Apple Pencil squeeze gesture on supported devices.
Testing Intents
Testing in Shortcuts App
- Build and run your app
- Open Shortcuts app
- Create new shortcut
- Search for your app's intents
- Configure parameters
- Run shortcut
Testing with Siri
- Build and run app
- Say: "Hey Siri, [your phrase]"
- Verify Siri understands and executes
Programmatic Testing
import Testing
import AppIntents
@Test
func testCreateNoteIntent() async throws {
var intent = CreateNoteIntent()
intent.title = "Test Note"
intent.content = "Test content"
let result = try await intent.perform()
// Verify result
#expect(result != nil)
}
Best Practices
1. Natural Phrases
// GOOD: Natural language
"Create a note in \(.applicationName)"
"Add \(\.$title) to my notes"
// AVOID: Technical language
"Execute CreateNote command in \(.applicationName)"
2. Meaningful Dialogs
// GOOD: Contextual confirmation
return .result(dialog: "Created note '\(title)' in your Ideas folder")
// AVOID: Generic responses
return .result(dialog: "Done")
3. Fast Background Execution
// Keep background intents fast
func perform() async throws -> some IntentResult {
// Quick operation
await quickSave(data)
return .result(dialog: "Saved!")
}
4. Graceful Error Handling
func perform() async throws -> some IntentResult {
guard let note = await NoteManager.shared.find(id: noteId) else {
throw IntentError.noteNotFound
}
// Continue...
}
enum IntentError: Error, CustomLocalizedStringResourceConvertible {
case noteNotFound
var localizedStringResource: LocalizedStringResource {
switch self {
case .noteNotFound:
return "Note not found. It may have been deleted."
}
}
}
Official Resources
Weekly Installs
3
Repository
bluewaves-creat…s-skillsGitHub Stars
1
First Seen
Jan 26, 2026
Security Audits
Installed on
opencode3
claude-code2
codex2
gemini-cli2
kode1
mcpjam1