App Intents and Siri Shortcuts: Advanced iOS Automation 2026
Complete guide to App Intents and Siri Shortcuts for iOS 18+. Build custom Siri actions, integrate Apple Intelligence, and automate your Swift app in 2026.

In 2026, with Apple Intelligence and the App Intents framework, iOS apps enter an era where user intent matters more than UI. Apps that don't expose intents become invisible in an AI-first OS. App Intents form the foundation enabling Siri, Spotlight, widgets, and the Action Button to interact with app functionality.
This article presents the complete creation of App Intents and Siri Shortcuts for iOS 18+, from fundamental concepts through Apple Intelligence integration and App Intent Domains.
Understanding the App Intents Framework
The App Intents framework, introduced with iOS 16, modernizes intent construction in Swift by replacing the older SiriKit Intents framework. This declarative architecture enables creating system-discoverable actions: Spotlight, Shortcuts app, Siri, and the Action Button.
import AppIntents
// An AppIntent represents an action users can perform
struct CreateTaskIntent: AppIntent {
// Title displayed in Shortcuts and Siri
static var title: LocalizedStringResource = "Create a task"
// Description for accessibility and suggestions
static var description = IntentDescription(
"Creates a new task in the application."
)
// Parameter with automatic validation
@Parameter(title: "Task title")
var taskTitle: String
// Optional parameter with default value
@Parameter(title: "Priority", default: .medium)
var priority: TaskPriority
// Action execution
func perform() async throws -> some IntentResult & ReturnsValue<TaskEntity> {
// Create task via service
let task = TaskService.shared.createTask(
title: taskTitle,
priority: priority
)
// Return created entity for chaining
return .result(value: TaskEntity(task: task))
}
}The intent declares parameters using the @Parameter property wrapper, allowing Siri to request missing values. The perform() method executes business logic and returns a typed result.
Defining App Entities for Data
App Entities represent the "nouns" of the application: objects on which intents operate. They enable Siri to understand and manipulate app data.
import AppIntents
// Internal data model
struct Task: Identifiable, Codable {
let id: UUID
var title: String
var priority: TaskPriority
var isCompleted: Bool
var dueDate: Date?
}
// Entity exposed to the system
struct TaskEntity: AppEntity {
// Required unique identifier
var id: UUID
// Displayable properties
var title: String
var priority: TaskPriority
var isCompleted: Bool
// Display configuration in the system
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Task"
// Visual representation of the instance
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(priority.rawValue)",
image: .init(systemName: isCompleted ? "checkmark.circle.fill" : "circle")
)
}
// Default query for searching entities
static var defaultQuery = TaskEntityQuery()
// Initializer from internal model
init(task: Task) {
self.id = task.id
self.title = task.title
self.priority = task.priority
self.isCompleted = task.isCompleted
}
}
// Query for searching and filtering entities
struct TaskEntityQuery: EntityQuery {
// Search by identifiers
func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
TaskService.shared.fetchTasks()
.filter { identifiers.contains($0.id) }
.map { TaskEntity(task: $0) }
}
// Suggestions displayed in the interface
func suggestedEntities() async throws -> [TaskEntity] {
TaskService.shared.fetchTasks()
.filter { !$0.isCompleted }
.prefix(5)
.map { TaskEntity(task: $0) }
}
}The EntityQuery defines how the system searches and suggests entities. The entities(for:) and suggestedEntities() methods feed Siri and Shortcuts interfaces.
Use AppEnum for types with a fixed set of values (priority, status), and AppEntity for dynamic user-created types (tasks, notes, contacts).
Creating App Enums for Fixed Values
App Enums expose enumerated types to the system, enabling Siri to offer contextual choices.
import AppIntents
// Enum exposed to the system
enum TaskPriority: String, AppEnum, Codable {
case low
case medium
case high
// Type name displayed
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Priority"
// Representation of each case
static var caseDisplayRepresentations: [TaskPriority: DisplayRepresentation] = [
.low: DisplayRepresentation(
title: "Low",
image: .init(systemName: "arrow.down.circle")
),
.medium: DisplayRepresentation(
title: "Medium",
image: .init(systemName: "minus.circle")
),
.high: DisplayRepresentation(
title: "High",
image: .init(systemName: "exclamationmark.circle")
)
]
}
// Enum for task status
enum TaskStatus: String, AppEnum, Codable {
case pending
case inProgress
case completed
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Status"
static var caseDisplayRepresentations: [TaskStatus: DisplayRepresentation] = [
.pending: "Pending",
.inProgress: "In Progress",
.completed: "Completed"
]
}Visual representations (SF Symbols icons) enrich display in Shortcuts and Siri suggestions.
Implementing AppShortcutsProvider
The AppShortcutsProvider exposes App Shortcuts to the system, making them immediately available without user configuration. These shortcuts appear in Spotlight, Siri, and the Action Button.
import AppIntents
// Provider declaring all app shortcuts
struct TaskAppShortcutsProvider: AppShortcutsProvider {
// Maximum 10 shortcuts per application
@AppShortcutsBuilder
static var appShortcuts: [AppShortcut] {
// Shortcut to create a task
AppShortcut(
intent: CreateTaskIntent(),
phrases: [
// The .applicationName placeholder is REQUIRED
"Create a task with \(.applicationName)",
"New task in \(.applicationName)",
"Add a task to \(.applicationName)"
],
shortTitle: "Create Task",
systemImageName: "plus.circle"
)
// Shortcut to list tasks
AppShortcut(
intent: ListTasksIntent(),
phrases: [
"Show my tasks in \(.applicationName)",
"Display tasks from \(.applicationName)",
"What are my tasks \(.applicationName)"
],
shortTitle: "My Tasks",
systemImageName: "list.bullet"
)
// Shortcut with dynamic parameter
AppShortcut(
intent: CompleteTaskIntent(),
phrases: [
"Complete \(\.$taskName) in \(.applicationName)",
"Mark \(\.$taskName) as done with \(.applicationName)"
],
shortTitle: "Complete Task",
systemImageName: "checkmark.circle"
)
}
}Voice phrases must include the \(.applicationName) placeholder for Siri to identify the target app. Dynamic parameters like \(\.$taskName) enable contextual commands.
An application can declare a maximum of 10 App Shortcuts. Prioritize the most frequent and useful actions for users.
Intents with Complex Parameters
Intents can accept complex parameters including entities and configuration options.
import AppIntents
struct CompleteTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Complete a task"
static var description = IntentDescription(
"Marks a task as completed."
)
// Entity parameter with automatic search
@Parameter(title: "Task")
var task: TaskEntity
// Optional parameter with date
@Parameter(title: "Completion date")
var completionDate: Date?
// Siri dialog configuration
static var parameterSummary: some ParameterSummary {
Summary("Complete \(\.$task)") {
\.$completionDate
}
}
func perform() async throws -> some IntentResult & ProvidesDialog {
// Update the task
TaskService.shared.completeTask(
id: task.id,
completionDate: completionDate ?? Date()
)
// Voice feedback for Siri
return .result(
dialog: "Task \(task.title) has been marked as completed."
)
}
}
// Intent returning a list of entities
struct ListTasksIntent: AppIntent {
static var title: LocalizedStringResource = "List tasks"
// Optional filter
@Parameter(title: "Status", default: nil)
var statusFilter: TaskStatus?
@Parameter(title: "Priority", default: nil)
var priorityFilter: TaskPriority?
func perform() async throws -> some IntentResult & ReturnsValue<[TaskEntity]> {
var tasks = TaskService.shared.fetchTasks()
// Apply filters
if let status = statusFilter {
tasks = tasks.filter {
switch status {
case .completed: return $0.isCompleted
case .pending, .inProgress: return !$0.isCompleted
}
}
}
if let priority = priorityFilter {
tasks = tasks.filter { $0.priority == priority }
}
let entities = tasks.map { TaskEntity(task: $0) }
return .result(value: entities)
}
}The ParameterSummary defines how Siri presents the intent during voice execution, providing natural feedback.
Ready to ace your iOS interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Apple Intelligence Integration
iOS 18 introduces App Intent Domains, collections of APIs designed for specific functionality. These domains enable Apple Intelligence to understand and execute actions with increased accuracy.
import AppIntents
// Conforming to Bookmarks domain for Apple Intelligence integration
struct SaveBookmarkIntent: AppIntent {
static var title: LocalizedStringResource = "Save a bookmark"
// Automatically validated URL parameter
@Parameter(title: "URL")
var url: URL
@Parameter(title: "Title", default: nil)
var title: String?
@Parameter(title: "Folder", default: nil)
var folder: BookmarkFolderEntity?
// Open app if necessary
static var openAppWhenRun: Bool = false
func perform() async throws -> some IntentResult & ProvidesDialog {
let bookmark = BookmarkService.shared.save(
url: url,
title: title,
folder: folder?.id
)
return .result(
dialog: "Bookmark saved: \(bookmark.title)"
)
}
}
// Intent with screen content awareness (iOS 18.4+)
struct AnalyzeScreenContentIntent: AppIntent {
static var title: LocalizedStringResource = "Analyze content"
// Screen context access via Apple Intelligence
@Parameter(title: "Context")
var screenContext: String?
func perform() async throws -> some IntentResult & ProvidesDialog {
guard let context = screenContext else {
return .result(dialog: "No content to analyze.")
}
// Process content extracted by Apple Intelligence
let analysis = ContentAnalyzer.analyze(context)
return .result(dialog: analysis.summary)
}
}Predefined domains (Books, Camera, Spreadsheets) enable Siri to respond to requests accurately thanks to models trained on these specific tasks.
Siri Actions with User Confirmation
For sensitive actions, the system can request confirmation before execution.
import AppIntents
struct DeleteTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Delete a task"
@Parameter(title: "Task")
var task: TaskEntity
// Requires user confirmation
static var isDiscoverable: Bool = true
func perform() async throws -> some IntentResult & ProvidesDialog {
// Request confirmation via dialog
try await requestConfirmation(
result: .result(
dialog: "Are you sure you want to delete \(task.title)?"
)
)
// Delete after confirmation
TaskService.shared.deleteTask(id: task.id)
return .result(
dialog: "Task \(task.title) has been deleted."
)
}
}
// Intent with multiple dialog steps
struct ScheduleTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Schedule a task"
@Parameter(title: "Task")
var task: TaskEntity
@Parameter(title: "Date")
var scheduledDate: Date
@Parameter(title: "Reminder", default: true)
var setReminder: Bool
static var parameterSummary: some ParameterSummary {
When(\.$setReminder, .equalTo, true) {
Summary("Schedule \(\.$task) for \(\.$scheduledDate) with reminder")
} otherwise: {
Summary("Schedule \(\.$task) for \(\.$scheduledDate)")
}
}
func perform() async throws -> some IntentResult & ProvidesDialog {
TaskService.shared.schedule(
taskId: task.id,
date: scheduledDate,
reminder: setReminder
)
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .short
let formattedDate = dateFormatter.string(from: scheduledDate)
return .result(
dialog: "Task scheduled for \(formattedDate)."
)
}
}The requestConfirmation method pauses execution until user validation, protecting against accidental actions.
Intents in Interactive Widgets
App Intents integrate naturally with WidgetKit to create iOS 17+ interactive widgets.
import AppIntents
import WidgetKit
// Widget-optimized intent (fast execution)
struct ToggleTaskFromWidgetIntent: AppIntent {
static var title: LocalizedStringResource = "Toggle task"
@Parameter(title: "Task ID")
var taskID: String
init() {}
init(taskID: UUID) {
self.taskID = taskID.uuidString
}
// No dialog for widgets
func perform() async throws -> some IntentResult {
guard let uuid = UUID(uuidString: taskID) else {
return .result()
}
TaskService.shared.toggleCompletion(taskId: uuid)
// Immediate widget refresh
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}
// Widget view with interactive button
import SwiftUI
struct TaskWidgetView: View {
let task: Task
var body: some View {
Button(intent: ToggleTaskFromWidgetIntent(taskID: task.id)) {
HStack {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(task.isCompleted ? .green : .secondary)
Text(task.title)
.strikethrough(task.isCompleted)
}
.padding()
}
.buttonStyle(.plain)
}
}Widgets use the Button(intent:) syntax to directly connect interaction to the App Intent, without opening the application.
Action Button Configuration
The Action Button on iPhone 15 Pro and later can trigger App Shortcuts directly.
import AppIntents
// Action Button optimized intent
struct QuickCaptureIntent: AppIntent {
static var title: LocalizedStringResource = "Quick capture"
static var description = IntentDescription(
"Quickly creates a task with a title."
)
// Opens app for input
static var openAppWhenRun: Bool = true
func perform() async throws -> some IntentResult & OpensIntent {
// Notification to open quick capture screen
NotificationCenter.default.post(
name: .quickCaptureTriggered,
object: nil
)
return .result(opensIntent: ShowQuickCaptureViewIntent())
}
}
// Declare in AppShortcutsProvider for Action Button
extension TaskAppShortcutsProvider {
@AppShortcutsBuilder
static var appShortcuts: [AppShortcut] {
// ... other shortcuts
AppShortcut(
intent: QuickCaptureIntent(),
phrases: [
"Quick capture \(.applicationName)",
"Quick note \(.applicationName)"
],
shortTitle: "Capture",
systemImageName: "bolt.circle"
)
}
}Users can configure the Action Button to trigger this shortcut via Settings > Action Button.
iOS 18 introduces @DeferredProperty and @ComputedProperty macros to reduce boilerplate. App Intents can also reside in Swift Packages for cross-platform reuse.
Intents in Swift Packages
App Intents can be defined in Swift Packages for sharing between iOS, macOS, and watchOS.
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "TaskIntents",
platforms: [
.iOS(.v17),
.macOS(.v14),
.watchOS(.v10)
],
products: [
.library(name: "TaskIntents", targets: ["TaskIntents"])
],
dependencies: [],
targets: [
.target(
name: "TaskIntents",
dependencies: []
)
]
)
// Sources/TaskIntents/SharedIntents.swift
import AppIntents
// Cross-platform shared intent
public struct SharedCreateTaskIntent: AppIntent {
public static var title: LocalizedStringResource = "Create a task"
@Parameter(title: "Title")
public var taskTitle: String
public init() {}
public func perform() async throws -> some IntentResult {
// Shared logic
await TaskRepository.shared.create(title: taskTitle)
return .result()
}
}
// Platform-specific extension in app
#if os(iOS)
extension SharedCreateTaskIntent {
// iOS-specific behavior
static var openAppWhenRun: Bool = false
}
#endifThis architecture maintains a single codebase for intents while adapting behavior per platform.
Testing App Intents
App Intents can be unit tested like any Swift code.
import XCTest
import AppIntents
@testable import TaskApp
final class TaskIntentTests: XCTestCase {
override func setUp() {
super.setUp()
// Reset service for isolated tests
TaskService.shared.reset()
}
func testCreateTaskIntent() async throws {
// Given
var intent = CreateTaskIntent()
intent.taskTitle = "Test Task"
intent.priority = .high
// When
let result = try await intent.perform()
// Then
let tasks = TaskService.shared.fetchTasks()
XCTAssertEqual(tasks.count, 1)
XCTAssertEqual(tasks.first?.title, "Test Task")
XCTAssertEqual(tasks.first?.priority, .high)
}
func testCompleteTaskIntent() async throws {
// Given
let task = TaskService.shared.createTask(
title: "Task to complete",
priority: .medium
)
var intent = CompleteTaskIntent()
intent.task = TaskEntity(task: task)
// When
_ = try await intent.perform()
// Then
let updatedTask = TaskService.shared.fetchTask(id: task.id)
XCTAssertTrue(updatedTask?.isCompleted ?? false)
}
func testEntityQuery() async throws {
// Given
let task1 = TaskService.shared.createTask(title: "Task 1", priority: .low)
let task2 = TaskService.shared.createTask(title: "Task 2", priority: .high)
let query = TaskEntityQuery()
// When
let entities = try await query.entities(for: [task1.id, task2.id])
// Then
XCTAssertEqual(entities.count, 2)
}
}Tests verify intent behavior independently of the system interface.
Best Practices and Optimizations
Several patterns ensure performant App Intents and optimal user experience.
import AppIntents
// 1. Localized phrases
struct LocalizedTaskIntent: AppIntent {
static var title: LocalizedStringResource = "task.create.title"
static var description = IntentDescription(
"task.create.description",
categoryName: "task.category"
)
@Parameter(title: "task.parameter.title")
var taskTitle: String
func perform() async throws -> some IntentResult {
// ...
return .result()
}
}
// 2. Proper error handling
enum TaskIntentError: Error, CustomLocalizedStringResourceConvertible {
case taskNotFound
case invalidInput
case serviceUnavailable
var localizedStringResource: LocalizedStringResource {
switch self {
case .taskNotFound:
return "Task not found."
case .invalidInput:
return "Invalid data."
case .serviceUnavailable:
return "Service temporarily unavailable."
}
}
}
struct RobustTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Robust intent"
@Parameter(title: "ID")
var taskId: String
func perform() async throws -> some IntentResult & ProvidesDialog {
guard let uuid = UUID(uuidString: taskId) else {
throw TaskIntentError.invalidInput
}
guard let task = TaskService.shared.fetchTask(id: uuid) else {
throw TaskIntentError.taskNotFound
}
return .result(dialog: "Task found: \(task.title)")
}
}
// 3. EntityQuery optimization
struct OptimizedTaskQuery: EntityStringQuery {
// Optimized text search
func entities(matching string: String) async throws -> [TaskEntity] {
// Service-side search with limit
TaskService.shared.search(query: string, limit: 10)
.map { TaskEntity(task: $0) }
}
// Limited suggestions for performance
func suggestedEntities() async throws -> [TaskEntity] {
TaskService.shared.fetchRecentTasks(limit: 5)
.map { TaskEntity(task: $0) }
}
}
// 4. Focus Filter for Focus modes
struct TaskFocusFilter: SetFocusFilterIntent {
static var title: LocalizedStringResource = "Filter tasks"
@Parameter(title: "Show high priority only")
var showHighPriorityOnly: Bool
func perform() async throws -> some IntentResult {
TaskService.shared.setFocusFilter(highPriorityOnly: showHighPriorityOnly)
return .result()
}
}These patterns ensure smooth system integration while maintaining optimal performance.
Conclusion
App Intents and Siri Shortcuts transform how users interact with iOS applications. In 2026, with Apple Intelligence, exposing intents is no longer optional but essential for delivering a modern and intuitive experience.
App Intents iOS 18+ Checklist
- ✅ Create AppIntents for the main app actions
- ✅ Define AppEntities for manipulable data
- ✅ Use AppEnum for enumerated types
- ✅ Implement AppShortcutsProvider with voice phrases
- ✅ Respect the 10 App Shortcuts maximum limit
- ✅ Include
\(.applicationName)in all phrases - ✅ Configure ParameterSummary for Siri feedback
- ✅ Integrate Apple Intelligence domains if applicable
- ✅ Test intents with unit tests
- ✅ Localize titles and descriptions
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

WidgetKit iOS 17+: Building Interactive Widgets with App Intents
Complete guide to creating interactive iOS widgets with WidgetKit and App Intents. Buttons, toggles, animations, and best practices for iOS 17+ in 2026.

Combine vs async/await in Swift: Progressive Migration Patterns
Complete guide to migrating from Combine to async/await in Swift: progressive strategies, bridging patterns, and paradigm coexistence in iOS codebases.

iOS Accessibility Interview Questions in 2026: VoiceOver and Dynamic Type
Prepare for iOS interviews with key accessibility questions: VoiceOver, Dynamic Type, semantic traits, and accessibility audits.