Use 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+
Generates App Intents for Siri, Shortcuts, and Spotlight integration on iOS and macOS.
npx claudepluginhub charleswiltgen/axiomThis skill inherits all available tools. When active, it can use any tool Claude has access to.
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.
App Intents integrate with:
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 { /* ... */ }
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)
}
}
}
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)"
]
}
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
}
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
}
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"
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"
)
}
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)
}
// ...
}
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()
}
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 properties that read directly from a source of truth (no stored value):
struct SettingsEntity: UniqueAppEntity {
@ComputedProperty
var defaultPlace: PlaceDescriptor {
UserDefaults.standard.defaultPlace
}
init() { }
}
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)
}
}
}
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)
}
}
// DON'T make your model conform to AppEntity
struct Book: AppEntity { // Bad - couples model to intents
var id: UUID
var title: String
// ...
}
// 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
}
}
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
}
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)")
}
}
| 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 |
| 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 |
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
)
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()
}
}
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()
}
}
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")
}
}
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
}
Return a SwiftUI view showing the outcome of an intent:
func perform() async throws -> some IntentResult {
return .result(view: Text("Order placed!").font(.title))
}
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()
}
}
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.
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.
AttributedString type for text parameters to preserve formattingModels 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())"
)
}
{
"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"
}
}
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.
struct CreateNoteIntent: AppIntent {
@Parameter(title: "Content")
var content: String // Loses formatting from model
}
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()
}
}
Bear app's Create Note accepts AttributedString, allowing diary templates from ChatGPT to include:
When Use Model output connects to another action, the runtime automatically converts types:
// 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.
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
IndexedEntity dramatically reduces boilerplate by auto-generating Find actions from your Spotlight integration. Instead of manually implementing EntityQuery and EntityPropertyQuery, adopt IndexedEntity to get:
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"
}
// 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?
@Property(title: "Notes", customIndexingKey: "eventNotes")
var notes: String?
@Property(title: "Attendee Count", customIndexingKey: "attendeeCount")
var attendeeCount: Int
With IndexedEntity conformance, users get this Find action automatically:
Find Events where:
- Title contains "Team"
- Start Date is today
- Location is "San Francisco"
EnumerableEntityQuery protocolEntityPropertyQuery protocolWith IndexedEntity Just add indexing keys, done!
Enable string-based search by implementing EntityStringQuery:
extension EventEntityQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [EventEntity] {
return try await EventService.shared.search(query: string)
}
}
Or rely on IndexedEntity + Spotlight for automatic search.
For entities that need custom searchable attributes or manual index management:
extension LandmarkEntity {
var searchableAttributes: CSSearchableItemAttributeSet {
let attributes = CSSearchableItemAttributeSet()
attributes.title = name
attributes.namedLocation = regionDescription
attributes.keywords = activities
attributes.latitude = NSNumber(value: coordinate.latitude)
attributes.longitude = NSNumber(value: coordinate.longitude)
attributes.supportsNavigation = true
return attributes
}
}
// Add entities to index
func indexLandmarks() async {
let landmarks = await fetchLandmarks()
try await CSSearchableIndex.default().indexAppEntities(landmarks, priority: .normal)
}
// Remove from index when deleted
func deleteLandmark(_ landmark: LandmarkEntity) async {
await dataStore.delete(landmark)
try await CSSearchableIndex.default().deleteAppEntities(
identifiedBy: [landmark.id],
ofType: LandmarkEntity.self
)
}
Apple's sample code (App Intents Travel Tracking App) demonstrates IndexedEntity:
struct TripEntity: AppEntity, IndexedEntity {
var id: UUID
@Property(title: "Name", indexingKey: \.title)
var name: String
@Property(title: "Start Date", indexingKey: \.startDate)
var startDate: Date
@Property(title: "End Date", indexingKey: \.endDate)
var endDate: Date
@Property(title: "Destination", customIndexingKey: "destination")
var destination: String
// Auto-generated Find Trips action with filters for all properties
}
Spotlight on Mac (macOS Sequoia+) allows users to run your app's intents directly from system search. Intents that work in Shortcuts automatically work in Spotlight with proper configuration.
Key principle Spotlight is all about running things quickly. To do that, people need to be able to provide all the information your intent needs to run directly in Spotlight.
The parameter summary, which is what people will see in Spotlight UI, must contain all required parameters that don't have a default value.
struct CreateEventIntent: AppIntent {
static var title: LocalizedStringResource = "Create Event"
@Parameter(title: "Title")
var title: String
@Parameter(title: "Start Date")
var startDate: Date
@Parameter(title: "End Date")
var endDate: Date
@Parameter(title: "Notes") // Required, no default
var notes: String
static var parameterSummary: some ParameterSummary {
Summary("Create '\(\.$title)' from \(\.$startDate) to \(\.$endDate)")
// Missing 'notes' parameter!
}
}
@Parameter(title: "Notes")
var notes: String? // Optional - can omit from summary
@Parameter(title: "Notes")
var notes: String = "" // Has default - can omit from summary
static var parameterSummary: some ParameterSummary {
Summary("Create '\(\.$title)' from \(\.$startDate) to \(\.$endDate)") {
\.$notes // All required params included
}
}
Intents hidden from Shortcuts won't appear in Spotlight:
// ❌ Hidden from Spotlight
static var isDiscoverable: Bool = false
// ❌ Hidden from Spotlight
static var assistantOnly: Bool = true
// ❌ Hidden from Spotlight
// Intent with no perform() method (widget configuration only)
Make parameter filling quick with suggestions:
struct EventEntityQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [EventEntity] {
return try await EventService.shared.fetchEvents(ids: identifiers)
}
// Provide upcoming events, not all past/present events
func suggestedEntities() async throws -> [EventEntity] {
return try await EventService.shared.upcomingEvents(limit: 10)
}
}
struct TimezoneQuery: EnumerableEntityQuery {
func allEntities() async throws -> [TimezoneEntity] {
// Small list - provide all
return TimezoneEntity.allTimezones
}
}
Use suggested entities when List is large or unbounded (calendar events, notes, contacts) Use all entities when List is small and bounded (timezones, priority levels, categories)
Suggest currently active content:
// In your detail view controller
func showEventDetail(_ event: Event) {
let activity = NSUserActivity(activityType: "com.myapp.viewEvent")
activity.persistentIdentifier = event.id.uuidString
// Spotlight suggests this event for parameters
activity.appEntityIdentifier = event.id.uuidString
userActivity = activity
}
For more details on on-screen content tagging, see the "Exploring New Advances in App Intents" session.
Basic filtering (automatic): If you provide suggestions, Spotlight automatically filters them as user types.
Deep search (requires implementation): For searching beyond suggestions:
extension EventQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [EventEntity] {
return try await EventService.shared.search(query: string)
}
}
struct EventEntity: AppEntity, IndexedEntity {
// Spotlight search automatically supported
}
// Background intent - runs without opening app
struct CreateEventIntent: AppIntent {
static var openAppWhenRun: Bool = false
@Parameter(title: "Title")
var title: String
@Parameter(title: "Start Date")
var startDate: Date
func perform() async throws -> some IntentResult {
let event = try await EventService.shared.createEvent(
title: title,
startDate: startDate
)
// Optionally open app to view created event
return .result(
value: EventEntity(from: event),
opensIntent: OpenEventIntent(event: EventEntity(from: event))
)
}
}
// Foreground intent - opens app to specific event
struct OpenEventIntent: AppIntent {
static var openAppWhenRun: Bool = true
@Parameter(title: "Event")
var event: EventEntity
func perform() async throws -> some IntentResult {
await MainActor.run {
EventCoordinator.shared.showEvent(id: event.id)
}
return .result()
}
}
Enable Spotlight suggestions based on usage patterns:
struct OrderCoffeeIntent: AppIntent, PredictableIntent {
static var title: LocalizedStringResource = "Order Coffee"
@Parameter(title: "Coffee Type")
var coffeeType: CoffeeType
@Parameter(title: "Size")
var size: CoffeeSize
func perform() async throws -> some IntentResult {
// Order logic
return .result()
}
}
Spotlight learns when/how user runs this intent and surfaces suggestions proactively.
Personal Automations arrive on macOS (macOS Sequoia+) with Mac-specific triggers:
Example use case Invoice processing shortcut runs automatically every time a new invoice is added to ~/Documents/Invoices folder.
As long as your intent is available on macOS, they will also be available to use in Shortcuts to run as a part of Automations on Mac. This includes iOS apps that are installable on macOS.
No additional code required — your existing intents work in automations automatically.
struct ProcessInvoiceIntent: AppIntent {
static var title: LocalizedStringResource = "Process Invoice"
// Available on macOS automatically
// Also works: iOS apps installed on Mac (Catalyst, Mac Catalyst)
@Parameter(title: "Invoice")
var invoice: FileEntity
func perform() async throws -> some IntentResult {
// Extract data, add to spreadsheet, etc.
return .result()
}
}
With automations, your intents are now accessible from:
Apple provides pre-built schemas for common app categories:
import AppIntents
import BooksIntents
struct OpenBookIntent: BooksOpenBookIntent {
@Parameter(title: "Book")
var target: BookEntity
func perform() async throws -> some IntentResult {
await MainActor.run {
BookReader.shared.open(book: target)
}
return .result()
}
}
Add intent to Shortcuts:
Test parameter resolution:
Test with Siri:
// In your app target, not tests
#if DEBUG
extension OrderSoupIntent {
static func testIntent() async throws {
let intent = OrderSoupIntent()
intent.soup = SoupEntity(id: "1", name: "Tomato", price: 8.99)
intent.quantity = 2
let result = try await intent.perform()
print("Result: \(result)")
}
}
#endif
// ❌ Problem: isDiscoverable = false or missing
struct MyIntent: AppIntent {
// Missing isDiscoverable
}
// ✅ Solution: Make discoverable
struct MyIntent: AppIntent {
static var isDiscoverable: Bool = true
}
// ❌ Problem: Missing defaultQuery
struct ProductEntity: AppEntity {
var id: String
// Missing defaultQuery
}
// ✅ Solution: Add query
struct ProductEntity: AppEntity {
var id: String
static var defaultQuery = ProductQuery()
}
// ❌ Problem: Accessing MainActor from background
func perform() async throws -> some IntentResult {
UIApplication.shared.open(url) // Crash! MainActor only
return .result()
}
// ✅ Solution: Use MainActor or openAppWhenRun
func perform() async throws -> some IntentResult {
await MainActor.run {
UIApplication.shared.open(url)
}
return .result()
}
// ❌ Problem: entities(for:) not implemented
struct BookQuery: EntityQuery {
// Missing entities(for:) implementation
}
// ✅ Solution: Implement required methods
struct BookQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
return try await BookService.shared.fetchBooks(ids: identifiers)
}
func suggestedEntities() async throws -> [BookEntity] {
return try await BookService.shared.recentBooks(limit: 10)
}
}
static var title: LocalizedStringResource = "Do Thing"
static var title: LocalizedStringResource = "Process"
static var title: LocalizedStringResource = "Send Message"
static var title: LocalizedStringResource = "Book Appointment"
static var title: LocalizedStringResource = "Start Workout"
static var parameterSummary: some ParameterSummary {
Summary("Execute \(\.$action) with \(\.$target)")
}
static var parameterSummary: some ParameterSummary {
Summary("Send \(\.$message) to \(\.$contact)")
}
// Siri: "Send 'Hello' to John"
throw MyError.validationFailed("Invalid parameter state")
throw MyError.outOfStock("Sorry, this item is currently unavailable")
func suggestedEntities() async throws -> [TaskEntity] {
return try await TaskService.shared.allTasks() // Could be thousands!
}
func suggestedEntities() async throws -> [TaskEntity] {
return try await TaskService.shared.recentTasks(limit: 10)
}
func perform() async throws -> some IntentResult {
let data = URLSession.shared.synchronousDataTask(url) // Blocks!
return .result()
}
func perform() async throws -> some IntentResult {
let data = try await URLSession.shared.data(from: url)
return .result()
}
struct StartWorkoutIntent: AppIntent {
static var title: LocalizedStringResource = "Start Workout"
static var description: IntentDescription = "Starts a new workout session"
static var openAppWhenRun: Bool = true
@Parameter(title: "Workout Type")
var workoutType: WorkoutType
@Parameter(title: "Duration (minutes)")
var duration: Int?
static var parameterSummary: some ParameterSummary {
Summary("Start \(\.$workoutType)") {
\.$duration
}
}
func perform() async throws -> some IntentResult {
let workout = Workout(
type: workoutType,
duration: duration.map { TimeInterval($0 * 60) }
)
await MainActor.run {
WorkoutCoordinator.shared.start(workout)
}
return .result(
dialog: "Starting \(workoutType.displayName) workout"
)
}
}
enum WorkoutType: String, AppEnum {
case running
case cycling
case swimming
case yoga
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Workout Type"
static var caseDisplayRepresentations: [WorkoutType: DisplayRepresentation] = [
.running: "Running",
.cycling: "Cycling",
.swimming: "Swimming",
.yoga: "Yoga"
]
var displayName: String {
switch self {
case .running: return "running"
case .cycling: return "cycling"
case .swimming: return "swimming"
case .yoga: return "yoga"
}
}
}
struct AddTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Add Task"
static var description: IntentDescription = "Creates a new task"
static var isDiscoverable: Bool = true
@Parameter(title: "Title")
var title: String
@Parameter(title: "List")
var list: TaskListEntity?
@Parameter(title: "Due Date")
var dueDate: Date?
static var parameterSummary: some ParameterSummary {
Summary("Add '\(\.$title)'") {
\.$list
\.$dueDate
}
}
func perform() async throws -> some IntentResult {
let task = try await TaskService.shared.createTask(
title: title,
list: list?.id,
dueDate: dueDate
)
return .result(
value: TaskEntity(from: task),
dialog: "Task '\(title)' added"
)
}
}
struct TaskListEntity: AppEntity {
var id: UUID
var name: String
var color: String
static var typeDisplayRepresentation: TypeDisplayRepresentation = "List"
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
image: .init(systemName: "list.bullet")
)
}
static var defaultQuery = TaskListQuery()
}
struct TaskListQuery: EntityQuery, EntityStringQuery {
func entities(for identifiers: [UUID]) async throws -> [TaskListEntity] {
return try await TaskService.shared.fetchLists(ids: identifiers)
}
func suggestedEntities() async throws -> [TaskListEntity] {
// Provide user's favorite lists
return try await TaskService.shared.favoriteLists(limit: 5)
}
func entities(matching string: String) async throws -> [TaskListEntity] {
return try await TaskService.shared.searchLists(query: string)
}
}
isDiscoverable appear in ShortcutsopenAppWhenRun = truedisplayRepresentation shows meaningful infoWWDC: 2025-244, 2025-275, 2025-260
Docs: /appintents, /appintents/appintent, /appintents/appentity, /Updates/AppIntents
Skills: axiom-app-shortcuts-ref, axiom-core-spotlight-ref, axiom-app-discoverability
Remember App Intents are how users interact with your app through Siri, Shortcuts, and system features. Well-designed intents feel like a natural extension of your app's functionality and provide value across Apple's ecosystem.
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.