App Intents Integration
SkillSecurityUse when integrating App Intents for Siri, Apple Intelligence, Shortcuts, Spotlight, or system experiences - covers AppIntent, AppEntity, parameter handling, entity queries, background execution, authentication, and debugging common integration issues for iOS 16+
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the App Intents Integration skill
What this skill tells your AI
The instructions your AI receives, as published by comeonoliver/skillshub in skills/CharlesWiltgen/Axiom/axiom-app-intents-ref/SKILL.md and read by ahel’s review.
Overview
Comprehensive guide to App Intents framework for exposing app functionality to Siri, Apple Intelligence, Shortcuts, Spotlight, and other system experiences. Replaces older SiriKit custom intents with modern Swift-first API.
Core principle App Intents make your app's actions discoverable across Apple's ecosystem. Well-designed intents feel natural in Siri conversations, Shortcuts automation, and Spotlight search.
When to Use This Skill
- Exposing app functionality to Siri and Apple Intelligence
- Making app actions available in Shortcuts app
- Enabling Spotlight search for app content
- Integrating with Focus filters, widgets, Live Activities
- Adding Action button support (Apple Watch Ultra)
- Debugging intent resolution or parameter validation failures
- Testing intents with Shortcuts app
- Implementing entity queries for app content
Related Skills
- app-shortcuts-ref — App Shortcuts for instant Siri/Spotlight availability without user setup
- core-spotlight-ref — Core Spotlight and NSUserActivity integration for content indexing
- app-discoverability — Strategic guide for making apps surface system-wide across all APIs
System Experiences Supported
App Intents integrate with:
- Siri — Voice commands and Apple Intelligence
- Shortcuts — Automation workflows
- App Shortcuts — Pre-configured actions available instantly (see app-shortcuts-ref)
- Spotlight — Search discovery
- Focus Filters — Contextual filtering
- Action Button — Quick actions (Apple Watch Ultra)
- Control Center — Custom controls
- WidgetKit — Interactive widgets
- Live Activities — Dynamic Island updates
- Visual Intelligence — Image-based interactions
Visual Intelligence Integration
IntentValueQuery
Allow users to circle objects in the Visual Intelligence camera and see matching results from your app:
@UnionValue
enum VisualSearchResult {
case landmark(LandmarkEntity)
case collection(CollectionEntity)
}
struct LandmarkIntentValueQuery: IntentValueQuery {
func values(for input: SemanticContentDescriptor) async throws -> [VisualSearchResult] {
// Match visual input to app entities
}
}
// Each entity type needs an OpenIntent
struct OpenLandmarkIntent: OpenIntent { /* ... */ }
struct OpenCollectionIntent: OpenIntent { /* ... */ }
Onscreen Entities
Associate app entities with visible content so users can ask Siri or ChatGPT about what's on screen:
struct LandmarkDetailView: View {
let landmark: LandmarkEntity
var body: some View {
Group { /* View content */ }
.userActivity("com.landmarks.ViewingLandmark") { activity in
activity.title = "Viewing \(landmark.name)"
activity.appEntityIdentifier = EntityIdentifier(for: landmark)
}
}
}
Core Concepts
The Three Building Blocks
1. AppIntent — Executable actions with parameters
struct OrderSoupIntent: AppIntent {
static var title: LocalizedStringResource = "Order Soup"
static var description: IntentDescription = "Orders soup from the restaurant"
@Parameter(title: "Soup")
var soup: SoupEntity
@Parameter(title: "Quantity")
var quantity: Int?
func perform() async throws -> some IntentResult {
guard let quantity = quantity, quantity < 10 else {
throw $quantity.needsValue("Please specify how many soups")
}
try await OrderService.shared.order(soup: soup, quantity: quantity)
return .result()
}
}
2. AppEntity — Objects users interact with
struct SoupEntity: AppEntity {
var id: String
var name: String
var price: Decimal
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Soup"
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)", subtitle: "$\(price)")
}
static var defaultQuery = SoupQuery()
}
3. AppEnum — Enumeration types for parameters
enum SoupSize: String, AppEnum {
case small
case medium
case large
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Size"
static var caseDisplayRepresentations: [SoupSize: DisplayRepresentation] = [
.small: "Small (8 oz)",
.medium: "Medium (12 oz)",
.large: "Large (16 oz)"
]
}
AppIntent: Defining Actions
Essential Properties
struct SendMessageIntent: AppIntent {
// REQUIRED: Short verb-noun phrase
static var title: LocalizedStringResource = "Send Message"
// REQUIRED: Purpose explanation
static var description: IntentDescription = "Sends a message to a contact"
// OPTIONAL: Discovery in Shortcuts/Spotlight
static var isDiscoverable: Bool = true
// OPTIONAL: Launch app when run
static var openAppWhenRun: Bool = false
// OPTIONAL: Authentication requirement
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
}
Parameter Declaration
struct BookAppointmentIntent: AppIntent {
// Required parameter (non-optional)
@Parameter(title: "Service")
var service: ServiceEntity
// Optional parameter
@Parameter(title: "Preferred Date")
var preferredDate: Date?
// Parameter with requestValueDialog for disambiguation
@Parameter(title: "Location",
requestValueDialog: "Which location would you like to visit?")
var location: LocationEntity
// Parameter with default value
@Parameter(title: "Duration")
var duration: Int = 60
}
Parameter Summary (Siri Phrasing)
struct OrderIntent: AppIntent {
@Parameter(title: "Item")
var item: MenuItem
@Parameter(title: "Quantity")
var quantity: Int
static var parameterSummary: some ParameterSummary {
Summary("Order \(\.$quantity) \(\.$item)") {
\.$quantity
\.$item
}
}
}
// Siri: "Order 2 lattes"
The perform() Method
func perform() async throws -> some IntentResult {
// 1. Validate parameters
guard quantity > 0 && quantity < 100 else {
throw ValidationError.invalidQuantity
}
// 2. Execute action
let order = try await orderService.placeOrder(
item: item,
quantity: quantity
)
// 3. Donate for learning (optional)
await donation()
// 4. Return result
return .result(
value: order,
dialog: "Your order for \(quantity) \(item.name) has been placed"
)
}
Error Handling
enum OrderError: Error, CustomLocalizedStringResourceConvertible {
case outOfStock(itemName: String)
case paymentFailed
case networkError
var localizedStringResource: LocalizedStringResource {
switch self {
case .outOfStock(let name):
return "Sorry, \(name) is out of stock"
case .paymentFailed:
return "Payment failed. Please check your payment method"
case .networkError:
return "Network error. Please try again"
}
}
}
func perform() async throws -> some IntentResult {
if !item.isInStock {
throw OrderError.outOfStock(itemName: item.name)
}
// ...
}
AppEntity: Representing App Content
Entity Definition
struct BookEntity: AppEntity {
// REQUIRED: Unique, persistent identifier
var id: UUID
// App data properties
var title: String
var author: String
var coverImageURL: URL?
// REQUIRED: Type display name
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book"
// REQUIRED: Instance display
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "by \(author)",
image: coverImageURL.map { .init(url: $0) }
)
}
// REQUIRED: Query for resolution
static var defaultQuery = BookQuery()
}
Exposing Properties
struct TaskEntity: AppEntity {
var id: UUID
@Property(title: "Title")
var title: String
@Property(title: "Due Date")
var dueDate: Date?
@Property(title: "Priority")
var priority: TaskPriority
@Property(title: "Completed")
var isCompleted: Bool
// Properties exposed to system for filtering/sorting
}
Computed and Deferred Properties
@ComputedProperty
Computed properties that read directly from a source of truth (no stored value):
struct SettingsEntity: UniqueAppEntity {
@ComputedProperty
var defaultPlace: PlaceDescriptor {
UserDefaults.standard.defaultPlace
}
init() { }
}
@DeferredProperty
Properties that are expensive to calculate, only fetched when explicitly requested:
struct LandmarkEntity: IndexedEntity {
@DeferredProperty
var crowdStatus: Int {
get async throws {
await modelData.getCrowdStatus(self)
}
}
}
Entity Query
struct BookQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
// Fetch entities by IDs
return try await BookService.shared.fetchBooks(ids: identifiers)
}
func suggestedEntities() async throws -> [BookEntity] {
// Provide suggestions (recent, favorites, etc.)
return try await BookService.shared.recentBooks(limit: 10)
}
}
// Optional: Enable string-based search
extension BookQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [BookEntity] {
return try await BookService.shared.searchBooks(query: string)
}
}
Separating Entities from Models
❌ DON'T: Modify core data models
// DON'T make your model conform to AppEntity
struct Book: AppEntity { // Bad - couples model to intents
var id: UUID
var title: String
// ...
}
✅ DO: Create dedicated entities
// Your core model
struct Book {
var id: UUID
var title: String
var isbn: String
var pages: Int
// ... lots of internal properties
}
// Separate entity for intents
struct BookEntity: AppEntity {
var id: UUID
var title: String
var author: String
// Convert from model
init(from book: Book) {
self.id = book.id
self.title = book.title
self.author = book.author.name
}
}
Authentication & Security
Authentication Policies
struct ViewAccountIntent: AppIntent {
// No authentication required
static var authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed
}
struct TransferMoneyIntent: AppIntent {
// Requires user to be logged in
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
}
struct UnlockVaultIntent: AppIntent {
// Requires device unlock (Face ID/Touch ID/passcode)
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresLocalDeviceAuthentication
}
Background vs Foreground Execution
Intent Modes
Use supportedModes for granular control over execution context instead of the boolean openAppWhenRun:
struct GetCrowdStatusIntent: AppIntent {
static let supportedModes: IntentModes = [.background, .foreground(.dynamic)]
func perform() async throws -> some ReturnsValue<Int> & ProvidesDialog {
guard await modelData.isOpen(landmark) else {
return .result(value: 0, dialog: "The landmark is currently closed.")
}
if systemContext.currentMode.canContinueInForeground {
do {
try await continueInForeground(alwaysConfirm: false)
await navigator.navigateToCrowdStatus(landmark)
} catch {
// Opening app was denied
}
}
let status = await modelData.getCrowdStatus(landmark)
return .result(value: status, dialog: "Current crowd level: \(status)")
}
}
Available Modes
| Mode | Behavior |
|---|---|
.background | Performs entirely in background |
.foreground(.immediate) | App foregrounded before perform() runs |
.foreground(.dynamic) | Can request foreground during execution |
.foreground(.deferred) | Background initially, foreground before completion |
Common Combinations
| Combination | Use When |
|---|---|
[.background, .foreground] | Foreground default, background fallback |
[.background, .foreground(.dynamic)] | Background default, can request foreground |
[.background, .foreground(.deferred)] | Background initially, guaranteed foreground when requested |
Continuing in Foreground
Request foreground transition at runtime when using .foreground(.dynamic):
// Normal transition
try await continueInForeground(alwaysConfirm: false)
// Transition after an error
throw needsToContinueInForegroundError(
IntentDialog("Need to open app to complete this action"),
alwaysConfirm: true
)
Background Execution (Legacy)
struct QuickToggleIntent: AppIntent {
static var openAppWhenRun: Bool = false // Runs in background
func perform() async throws -> some IntentResult {
// Executes without opening app
await SettingsService.shared.toggle(setting: .darkMode)
return .result()
}
}
Foreground Continuation (Legacy)
struct EditDocumentIntent: AppIntent {
@Parameter(title: "Document")
var document: DocumentEntity
func perform() async throws -> some IntentResult {
// Open app to continue in UI
return .result(opensIntent: OpenDocumentIntent(document: document))
}
}
struct OpenDocumentIntent: AppIntent {
static var openAppWhenRun: Bool = true
@Parameter(title: "Document")
var document: DocumentEntity
func perform() async throws -> some IntentResult {
// App is now foreground, safe to update UI
await MainActor.run {
DocumentCoordinator.shared.open(document: document)
}
return .result()
}
}
Confirmation Dialogs
Requesting Confirmation
struct DeleteTaskIntent: AppIntent {
@Parameter(title: "Task")
var task: TaskEntity
func perform() async throws -> some IntentResult {
// Request confirmation before destructive action
try await requestConfirmation(
result: .result(dialog: "Are you sure you want to delete '\(task.title)'?"),
confirmationActionName: .init(stringLiteral: "Delete")
)
// User confirmed, proceed
try await TaskService.shared.delete(task: task)
return .result(dialog: "Task deleted")
}
}
Multiple Choice API
Request user input with structured options:
let options = [
IntentChoiceOption(title: "Option 1", subtitle: "Description 1"),
IntentChoiceOption(title: "Option 2", subtitle: "Description 2"),
IntentChoiceOption.cancel(title: "Not now")
]
let choice = try await requestChoice(
between: options,
dialog: IntentDialog("Please select an option")
)
switch choice.id {
case options[0].id: // Option 1 selected
case options[1].id: // Option 2 selected
default: // Cancelled
}
Interactive Snippets
Static Snippets
Return a SwiftUI view showing the outcome of an intent:
func perform() async throws -> some IntentResult {
return .result(view: Text("Order placed!").font(.title))
}
SnippetIntent
Return interactive snippets with follow-up action buttons:
func perform() async throws -> some IntentResult {
let landmark = await findNearestLandmark()
return .result(
value: landmark,
opensIntent: OpenLandmarkIntent(landmark: landmark),
snippetIntent: LandmarkSnippetIntent(landmark: landmark)
)
}
struct LandmarkSnippetIntent: SnippetIntent {
@Parameter var landmark: LandmarkEntity
var snippet: some View {
VStack {
Text(landmark.name).font(.headline)
Text(landmark.description).font(.body)
HStack {
Button("Add to Favorites") { /* action */ }
Button("Search Tickets") { /* action */ }
}
}
.padding()
}
}
Swift Package Support
AppIntentsPackage
Include App Intents in Swift Packages and static libraries:
// In your framework or dynamic library
public struct LandmarksKitPackage: AppIntentsPackage { }
// In your app target
struct LandmarksPackage: AppIntentsPackage {
static var includedPackages: [any AppIntentsPackage.Type] {
[LandmarksKitPackage.self]
}
}
This enables modular intent definitions across package boundaries. The app target aggregates all packages via includedPackages.
Apple Intelligence: Use Model Action
Overview
The Use Model action in Shortcuts (iOS 18.1+) allows users to incorporate Apple Intelligence models into their automation workflows. Your app's entities can be passed to language models for filtering, transformation, and reasoning.
Key capability Under the hood, the action passes a JSON representation of your entity to the model, so you'll want to make sure to expose any information you want it to be able to reason over, in the entity definition.
Three Output Types
1. Text (AttributedString)
- Models often respond with Rich Text (bold, italic, lists, tables)
- Use
AttributedStringtype for text parameters to preserve formatting - Enables lossless transfer from model to your app
2. Dictionary
- Structured data extraction from unstructured input
- Useful for parsing PDFs, emails, documents
- Example: Extract vendor, amount, date from invoice
3. App Entities (Your Types)
- Pass lists of entities to models for filtering/reasoning
- Model receives JSON representation of entities
- Example: "Filter calendar events related to my trip"
Exposing Entities to Models
Models receive a JSON representation of your entities including:
1. All exposed properties (converted to strings)
struct EventEntity: AppEntity {
var id: UUID
@Property(title: "Title")
var title: String
@Property(title: "Start Date")
var startDate: Date
@Property(title: "End Date")
var endDate: Date
@Property(title: "Notes")
var notes: String?
// All @Property values included in JSON for model
}
2. Type display representation (hints what entity represents)
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Calendar Event"
3. Display representation (title and subtitle)
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(startDate.formatted())"
)
}
Example JSON sent to model
{
"type": "Calendar Event",
"title": "Team Meeting",
"subtitle": "Jan 15, 2025 at 2:00 PM",
"properties": {
"Title": "Team Meeting",
"Start Date": "2025-01-15T14:00:00Z",
"End Date": "2025-01-15T15:00:00Z",
"Notes": "Discuss Q1 roadmap"
}
}
Supporting Rich Text with AttributedString
Why it matters If your app supports Rich Text content, now is the time to make sure your app intents use the attributed string type for text parameters where appropriate.
❌ DON'T: Use plain String
struct CreateNoteIntent: AppIntent {
@Parameter(title: "Content")
var content: String // Loses formatting from model
}
✅ DO: Use AttributedString
struct CreateNoteIntent: AppIntent {
@Parameter(title: "Content")
var content: AttributedString // Preserves Rich Text
func perform() async throws -> some IntentResult {
let note = Note(content: content) // Rich Text preserved
try await NoteService.shared.save(note)
return .result()
}
}
Real-world example from WWDC
Bear app's Create Note accepts AttributedString, allowing diary templates from ChatGPT to include:
- Bold headings
- Mood logging tables
- Formatted lists
- All preserved losslessly
Automatic Type Conversion
When Use Model output connects to another action, the runtime automatically converts types:
Example: Boolean for If actions
// User's shortcut:
// 1. Get notes created today
// 2. For each note:
// - Use Model: "Is this note related to developing features for Shortcuts?"
// - If [model output] = yes:
// - Add to Shortcuts Projects folder
Instead of returning verbose text like "Yes, this note seems to be about developing features for the Shortcuts app", the model automatically returns a Boolean (true/false) when connected to an If action.
Explicit output types available
- Text (AttributedString)
- Number
- Boolean
- Dictionary
- Date
- App Entities
Follow-Up Feature
Enable iterative refinement before passing to next action:
// User runs shortcut:
// 1. Get recipe from Safari
// 2. Use Model: "Extract ingredients list"
// - Follow Up: enabled
// - User types: "Double the recipe"
// - Model adjusts: 800g flour instead of 400g
// 3. Add to Grocery List in Things app
When to use
- Recipe modifications (scale servings, substitute ingredients)
- Content refinement (adjust tone, length, style)
- Data validation (confirm extracted values before saving)
IndexedEntity: Automatic Find Actions
Overview
IndexedEntity dramatically reduces boilerplate by auto-generating Find actions from your Spotlight integration. Instead of manually implementing EntityQuery and EntityPropertyQuery, adopt IndexedEntity to get:
- Automatic Find action in Shortcuts
- Property-based filtering
- Search support
- Minimal code required
Basic Implementation
struct EventEntity: AppEntity, IndexedEntity {
var id: UUID
// 1. Properties with indexing keys
@Property(title: "Title", indexingKey: \.eventTitle)
var title: String
@Property(title: "Start Date", indexingKey: \.startDate)
var startDate: Date
@Property(title: "End Date", indexingKey: \.endDate)
var endDate: Date
// 2. Custom key for properties without standard Spotlight attribute
@Property(title: "Notes", customIndexingKey: "eventNotes")
var notes: String?
// Display representation automatically maps to Spotlight
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(startDate.formatted())"
// title → kMDItemTitle
// subtitle → kMDItemDescription
// image → kMDItemContentType (if provided)
)
}
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Event"
}
Indexing Key Mapping
Standard Spotlight attribute keys
// Common Spotlight keys for events
@Property(title: "Title", indexingKey: \.eventTitle)
var title: String
@Property(title: "Start Date", indexingKey: \.startDate)
var startDate: Date
@Property(title: "Location", indexingKey: \.eventLocation)
var location: String?
Custom keys for non-standard attributes
@Property(title: "Notes", customIndexingKey: "eventNotes")
var notes: String?
@Property(title: "Attendee Count", customIndexingKey: "attendeeCount")
var attendeeCount: Int
Auto-Generated Find Action
With IndexedEntity conformance, users get this Find action automatically:
In Shortcuts app
Find Events where:
- Title contains "Team"
- Start Date is today
- Location is "San Francisco"
Without IndexedEntity, you'd need to manually implement
EnumerableEntityQueryprotocolEntityPropertyQueryprotocol- Property filters for each searchable field
- Search/suggestion logic
With IndexedEntity Just add indexing keys, done!
Search Support
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 63
- Forks
- 22
- Last commit
- Jun 2026
Advanced
- Catalog kind
- skill
- Gateway key
axiom-app-intents-ref- Source
- github.com/comeonoliver/skillshub