Loading...
Loading...
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+
npx skill4agent add charleswiltgen/axiom axiom-app-intents-refstruct 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()
}
}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()
}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
}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
}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")
}
}AttributedStringstruct 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
}static var typeDisplayRepresentation: TypeDisplayRepresentation = "Calendar Event"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"
}
}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()
}
}// 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 foldertruefalse// 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 appEntityQueryEntityPropertyQuerystruct 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: IntFind Events where:
- Title contains "Team"
- Start Date is today
- Location is "San Francisco"EnumerableEntityQueryEntityPropertyQueryEntityStringQueryextension EventEntityQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [EventEntity] {
return try await EventService.shared.search(query: string)
}
}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
}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 summarystatic var parameterSummary: some ParameterSummary {
Summary("Create '\(\.$title)' from \(\.$startDate) to \(\.$endDate)") {
\.$notes // All required params included
}
}// ❌ 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)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
}
}// 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
}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()
}
}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()
}
}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()
}
}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()
}
}// 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)
}
}isDiscoverableopenAppWhenRun = truedisplayRepresentation