7.9 — AppIntents & Shortcuts

Opening scenario

Your app has a “Start workout” button. A power user wants to say “Hey Siri, start my morning run” without opening the app. The PM wants the same action available from a Lock Screen widget, the Action Button on iPhone 15 Pro, the Shortcuts app, Spotlight search, the Apple Watch, and — as of iOS 18 — Apple Intelligence. AppIntents is the framework that unifies all of these surfaces. Write the action once; it appears everywhere.

ContextWhat it usually means
Reads “AppIntent”Has defined a basic intent
Reads “AppShortcutsProvider”Has registered Siri shortcuts
Reads “AppEntity”Has modeled custom domain objects
Reads “IntentParameter / dynamic options”Has built parameterized intents
Reads “AppIntentVocabulary”Has tuned Siri recognition

Concept → Why → How → Code

Concept

AppIntents replaces the old INIntent / SiriKit / Intents Extension / Intents UI Extension stack with a single, Swift-native, code-only framework. Three core protocols:

  • AppIntent — an action your app exposes. Has parameters, performs work, returns a result.
  • AppEntity — a domain object your app understands (Workout, Recipe, Habit). Can be passed as a parameter or returned.
  • AppShortcutsProvider — registers AppShortcut definitions with Siri at install time (no user setup required).

The result: one Swift file defines a feature that’s invokable from Siri, the Shortcuts app, Lock Screen widgets (Buttons with intent:), Action Button, Spotlight, Apple Watch’s Smart Stack, the Action menu on Vision Pro, and Apple Intelligence’s tool-use surface.

Why

  • Free surface coverage — write once, appear in 8+ system places.
  • No extension target — runs in-process or out-of-process based on the platform; you don’t manage that.
  • Type-safe — parameters are Swift enums, structs, entities. No NSDictionary plumbing.
  • Apple Intelligence integration — iOS 18+ uses your AppIntent metadata as tool definitions for the on-device LLM.

How — a basic AppIntent

import AppIntents

struct StartWorkoutIntent: AppIntent {
    static var title: LocalizedStringResource = "Start Workout"
    static var description = IntentDescription("Begin a workout in MyFitnessApp.")

    @Parameter(title: "Activity")
    var activity: ActivityType

    static var parameterSummary: some ParameterSummary {
        Summary("Start a \(\.$activity) workout")
    }

    @MainActor
    func perform() async throws -> some IntentResult & ProvidesDialog {
        try await WorkoutService.shared.start(activity: activity)
        return .result(dialog: "Started your \(activity.localizedName).")
    }
}

enum ActivityType: String, AppEnum {
    case running, cycling, yoga, strength

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Activity"
    static var caseDisplayRepresentations: [ActivityType: DisplayRepresentation] = [
        .running: "Run",
        .cycling: "Cycle",
        .yoga: "Yoga",
        .strength: "Strength"
    ]

    var localizedName: String {
        Self.caseDisplayRepresentations[self]?.title.key ?? rawValue
    }
}

That single struct now appears in:

  • The Shortcuts app under your app.
  • Siri (after user has used it once, or always with an AppShortcutsProvider — see below).
  • Lock Screen widgets via Button(intent: StartWorkoutIntent(activity: .running)) { ... }.

AppShortcutsProvider — zero-setup Siri

struct MyAppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: StartWorkoutIntent(activity: .running),
            phrases: [
                "Start a run in \(.applicationName)",
                "Begin running with \(.applicationName)",
                "\(.applicationName) start run"
            ],
            shortTitle: "Start Run",
            systemImageName: "figure.run"
        )

        AppShortcut(
            intent: ViewTodayHabitsIntent(),
            phrases: ["Show my habits in \(.applicationName)"],
            shortTitle: "Today's Habits",
            systemImageName: "checklist"
        )
    }
}

AppShortcutsProvider is auto-registered at install time — the user never has to “enable” the shortcut. They just say “Start a run in MyFitnessApp” and it works. Limit: 10 AppShortcuts per app.

AppEntity — exposing domain objects

struct HabitEntity: AppEntity {
    let id: UUID
    var name: String

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Habit"
    var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(name)") }

    static var defaultQuery = HabitQuery()
}

struct HabitQuery: EntityQuery {
    func entities(for identifiers: [HabitEntity.ID]) async throws -> [HabitEntity] {
        await HabitStore.shared.habits.filter { identifiers.contains($0.id) }
    }
    func suggestedEntities() async throws -> [HabitEntity] {
        await HabitStore.shared.habits
    }
}

Now an intent can take a HabitEntity parameter and Siri/Shortcuts will offer a list picker:

struct CompleteHabitIntent: AppIntent {
    static var title: LocalizedStringResource = "Complete Habit"

    @Parameter(title: "Habit") var habit: HabitEntity

    func perform() async throws -> some IntentResult {
        await HabitStore.shared.complete(id: habit.id)
        return .result()
    }
}

Dynamic options

@Parameter(
    title: "Workout",
    optionsProvider: WorkoutOptionsProvider()
)
var workout: String

struct WorkoutOptionsProvider: DynamicOptionsProvider {
    func results() async throws -> [String] {
        try await WorkoutCatalog.shared.availableNames()
    }
}

Returning results — ReturnsValue, OpensIntent, ProvidesDialog, ShowsSnippetView

func perform() async throws -> some IntentResult & ReturnsValue<Int> & ProvidesDialog {
    let count = await HabitStore.shared.todayCount
    return .result(value: count, dialog: "You've completed \(count) habits today.")
}

For Spotlight/widget snippets:

func perform() async throws -> some IntentResult & ShowsSnippetView {
    .result(view: HabitSummaryView())
}

Where intents run

  • In-process when the host UI needs to update (Lock Screen Button tap, widget Button tap).
  • Out-of-process (background) for Siri/Shortcuts execution. You don’t pick — set static var openAppWhenRun = true if your intent absolutely requires app foreground.
  • ForegroundContinuableIntent — for intents that might need to open the app: start in background, escalate if needed.

Spotlight donation

CSSearchableItem integration with AppEntity is automatic in iOS 17+: registered AppEntities appear in Spotlight, and tapping one runs your default intent. No NSUserActivity boilerplate.

iOS 18 — Apple Intelligence tool use

When the user invokes Apple Intelligence and asks “What habits did I complete today?”, the system can use your CompleteHabitIntent and a GetHabitsTodayIntent as tools, parameterized by the conversation context. Your AppIntents become LLM-callable functions. No additional code beyond well-named intents with clear description strings.

This is why naming and descriptions matter enormously now. description: "Returns the user's habits completed today, with completion timestamps" is better than description: "Habits intent".

In the wild

  • Shortcuts (built-in) — the canonical AppIntents host.
  • Bear, Things 3, Drafts — extensive intent coverage for “Create note,” “Add task,” “Open inbox.”
  • Carrot Weather — donated intents for “Show forecast for [location]” feeding Siri and Lock Screen.
  • Toolbox for HomeKit, Home+ 6AppEntity-based intents for “Turn on [room].”
  • Apple’s own apps (Reminders, Calendar, Notes, Mail) — all migrated from INIntent to AppIntent.

Common misconceptions

  1. “AppShortcuts need user setup.” They don’t. Define them in AppShortcutsProvider and Siri recognizes the phrases at install time.
  2. “AppIntents are only for Siri.” They power Shortcuts, widgets, Action Button, Spotlight, Apple Intelligence, Smart Stack, and more. Siri is one of many surfaces.
  3. “I can put any complex UI in an intent result.” Snippet views must be lightweight SwiftUI — no heavy interaction, no scroll views with full app state. Treat them as widgets.
  4. “AppEntity is just a struct that conforms to a protocol.” It also needs an EntityQuery for ID-based lookup and ideally suggestedEntities for picker UX. Without those, the entity isn’t useful in dynamic intents.
  5. “AppIntents replace NSUserActivity for handoff.” They do for the action surface, but NSUserActivity still exists for Handoff between devices, Continuity, and SiriKit donations on older OSes.

Seasoned engineer’s take

AppIntents is the highest-leverage code in your app. Three intents, well-named with clear descriptions, can light up your app on every Apple surface: home screen widget, Lock Screen, Watch face, Spotlight, Action Button, Apple Intelligence. The cost is one Swift file per intent. The competition is people who haven’t shipped intents at all and whose features only exist when the app is open.

Two practices that pay off:

  1. Make your description strings narrative. They are read by the on-device LLM as tool descriptions. “Returns the user’s three most recently completed habits with their completion times” is parsed and matched far better than “Get habits.”
  2. Avoid optional parameters when you can. Siri’s dialog UX for “What workout?” is much smoother than “Workout? (optional) Activity? (optional).” Provide good defaults and require only what’s necessary.

For migrations: if you’re on legacy INIntent extensions, the AppIntents migration is one of the highest-ROI refactors available. You delete entire extension targets, lose ~1000 lines of plist+Swift+ObjC plumbing, and gain Apple Intelligence support for free.

TIP: Use IntentDonationManager to donate intents the user has performed even if they weren’t invoked via Siri. The OS uses these to rank your AppShortcuts in Spotlight and the Suggestions widget.

WARNING: Don’t let an AppIntent perform a destructive action without confirmation. Provide parameterSummary with clear language, and for “Delete X” intents, set openAppWhenRun = true (or use Confirmation flow) so the user sees the consequence.

Interview corner

Junior: “How do you add a ‘Start workout’ Siri shortcut to your app?”

Define an AppIntent with a perform() method. Register it in an AppShortcutsProvider with one or more invocation phrases. Build and run; Siri recognizes the phrase at install time, no user setup needed.

Mid: “How would you let the user say ‘Complete my habit’ and have Siri ask which one?”

Define a HabitEntity (with EntityQuery that returns the user’s habits). Define CompleteHabitIntent with @Parameter var habit: HabitEntity. Siri sees the entity has a query, runs suggestedEntities(), and presents a picker dialog. Once selected, perform() runs against the chosen habit’s ID. Donate completed intents via IntentDonationManager so frequent habits appear higher in the picker.

Senior: “Design the AppIntent layer for a recipe app that exposes search, view, save, and cook actions to Siri, widgets, Lock Screen, and Apple Intelligence.”

Four AppIntents: SearchRecipesIntent(query: String) -> RecipeEntity[], ShowRecipeIntent(recipe: RecipeEntity) with openAppWhenRun = true, SaveRecipeIntent(recipe: RecipeEntity), StartCookingIntent(recipe: RecipeEntity) (background, posts a UNNotification with a “Next step” action). One RecipeEntity with an EntityQuery backed by SwiftData. Register the top three in AppShortcutsProvider with natural phrases (“Search recipes for (parameter), Show me (parameter), Start cooking (parameter)”). Rich description strings on every intent so Apple Intelligence can invoke them as tools: “Searches the user’s saved recipes by ingredient or dish name and returns up to five matches.” Lock Screen widget uses Button(intent: SearchRecipesIntent(query: "dinner")) { ... }. Spotlight surfaces individual RecipeEntity instances automatically; tapping one runs ShowRecipeIntent as the default. Test on iOS 18 device with Apple Intelligence enabled to verify tool invocation in chat.

Red flag: “We still ship an Intents Extension and an Intents UI Extension for our SiriKit shortcuts.”

In 2026 this is legacy code unless you specifically need pre-iOS 16 support. Migrating to AppIntents deletes two extension targets, removes the Intents framework code, and gives you the full modern surface (widgets, Action Button, Apple Intelligence) for free.

Lab preview

Lab 7.2 — Widget extension builds a parameterized AppIntent and surfaces it via an interactive widget button — the cleanest possible demonstration of “write the intent once, run it from a widget tap.”


Next: 7.10 — watchOS & WatchKit