Home / Blog / App Intents and Shortcuts: putting actions in the user’s hand

App Intents and Shortcuts: putting actions in the user’s hand

I’ve shipped App Intents on three projects. Done right, users get their job done without opening the app, triggered from the Shortcuts app, Siri, Spotlight, and even Focus mode filters. Done wrong, it stays an invisible feature nobody uses. A basic intent: adding a task import AppIntents struct AddTaskIntent: AppIntent { static var title: LocalizedStringResource […]

I’ve shipped App Intents on three projects. Done right, users get their job done without opening the app, triggered from the Shortcuts app, Siri, Spotlight, and even Focus mode filters. Done wrong, it stays an invisible feature nobody uses.

A basic intent: adding a task

import AppIntents

struct AddTaskIntent: AppIntent {
    static var title: LocalizedStringResource = "Add Task"
    static var description = IntentDescription("Quickly add a task to the list")
    @Parameter(title: "Task") var text: String
    func perform() async throws -> some IntentResult & ProvidesDialog {
        try await TaskStore.shared.add(text)
        return .result(dialog: "Added: (text)")
    }
}

Even an intent this small shows up in the Shortcuts app and responds to Siri with “AppName, add task X”. No Info.plist entry or storyboard required.

AppShortcutsProvider: the visibility switch

Declaring the intent isn’t enough, the user needs to be able to discover it through AppShortcutsProvider:

struct MyAppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: AddTaskIntent(),
            phrases: [
                "Add a task to (.applicationName)",
                "Create task in (.applicationName)"
            ],
            shortTitle: "Add Task",
            systemImageName: "plus.circle"
        )
    }
}

The phrases are the part that matters. applicationName is a placeholder for your app name, so if the app is called “Notes”, the user says “Add a task to Notes”.

Parametrized intents

If you want priority as a parameter:

enum Priority: String, AppEnum {
    case low, medium, high
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Priority")
    static var caseDisplayRepresentations: [Priority: DisplayRepresentation] = [
        .low: "Low", .medium: "Medium", .high: "High"
    ]
}

struct AddTaskIntent: AppIntent {
    @Parameter(title: "Task") var text: String
    @Parameter(title: "Priority") var priority: Priority
    func perform() async throws -> some IntentResult {
        try await TaskStore.shared.add(text, priority: priority)
        return .result()
    }
}

The Shortcuts app renders the parameter as a dropdown. On a Siri invocation, missing parameters get asked for.

EntityQuery: working with entities

For a “complete task” intent, you need to let the user pick which task. You do that with AppEntity and EntityQuery:

struct TaskEntity: AppEntity {
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Task")
    static var defaultQuery = TaskQuery()
    let id: UUID
    let title: String
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "(title)")
    }
}

struct TaskQuery: EntityQuery {
    func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
        try await TaskStore.shared.fetch(ids: identifiers)
    }
    func suggestedEntities() async throws -> [TaskEntity] {
        try await TaskStore.shared.fetchOpen(limit: 10)
    }
}

Focus filter: a real use case I built

Only work tasks in the Work focus, personal ones in the Personal focus. With SetFocusFilterIntent, the user sees my app’s filter in the Focus mode settings:

struct WorkFocusFilter: SetFocusFilterIntent {
    static var title: LocalizedStringResource = "Work Tasks"
    @Parameter(title: "Tag") var tag: String
    func perform() async throws -> some IntentResult {
        UserDefaults.standard.set(tag, forKey: "focusTag")
        NotificationCenter.default.post(name: .focusChanged, object: nil)
        return .result()
    }
}

When the main app wakes up, it reads focusTag and filters the list accordingly.

Wiring into interactive widgets

In iOS 17 widgets, a button or toggle can fire an App Intent directly. On my todo widget, when the checkbox gets tapped:

Button(intent: ToggleTaskIntent(taskId: task.id)) {
    Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
}

The widget refresh happens automatically, no manual timeline reload.

Pitfall: returning a dialog from async

If you want to show a dialog after an async operation, conform to ProvidesDialog and return .result(dialog:). If you only return .result(), Siri stays silent and the user doesn’t know what happened.

Localization

Wherever you use LocalizedStringResource, the String Catalog (.xcstrings) integration is automatic. Be careful with interpolation parameters in phrases though; some languages have grammar rules that break the English fixed-template assumption.

Final advice

Three good intents beat thirty half-done ones. If users can reach three fast actions from your app, that alone is enough to make it part of their daily flow. The features people actually keep using come out of that kind of restraint.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch