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.

App Intents and Siri Shortcuts for advanced iOS automation with Swift and Apple Intelligence

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.

What this article covers

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.

TaskIntent.swiftswift
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.

TaskEntity.swiftswift
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.

App Enums vs App Entities

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.

TaskPriority.swiftswift
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.

ShortcutsProvider.swiftswift
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.

10 Shortcuts Limit

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.

CompleteTaskIntent.swiftswift
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.

BookmarkDomainIntents.swiftswift
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.

DeleteTaskIntent.swiftswift
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.

TaskWidgetIntents.swiftswift
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.

ActionButtonIntent.swiftswift
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+ Macros

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.

Package.swiftswift
// 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
}
#endif

This 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.

TaskIntentTests.swiftswift
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.

BestPractices.swiftswift
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

#app-intents
#siri-shortcuts
#ios
#swift
#apple-intelligence

Share

Related articles