
App Intents Code Review
- 106 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
Helps with ai & agent building tasks.
About
app-intents-code-review is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- app-intents-code-review
- AI & Agent Building
- AI-coding skill
App Intents Code Review by the numbers
- 106 all-time installs (skills.sh)
- Ranked #4,152 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill app-intents-code-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 106 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
Helps with ai & agent building tasks.
Files
App Intents Code Review
Quick Reference
| Issue Type | Reference |
|---|---|
| AppIntent protocol, perform(), return types | references/intent-structure.md |
| AppEntity, EntityQuery, identifiers | references/entities.md |
| AppShortcutsProvider, phrases, discovery | references/shortcuts.md |
| @Parameter, validation, dynamic options | references/parameters.md |
Review Checklist
- [ ]
perform()marked with@MainActorif accessing UI/main thread resources - [ ]
perform()completes within 30-second timeout (no heavy downloads/processing) - [ ] Custom errors conform to
CustomLocalizedStringResourceConvertible - [ ]
EntityQuery.entities(for:)handles missing identifiers gracefully - [ ]
EntityStringQueryused if Siri voice input needed (not plainEntityQuery) - [ ]
suggestedEntities()returns reasonable defaults for disambiguation - [ ]
AppShortcutphrases include.applicationNameparameter - [ ] Non-optional
@Parameterhas sensible defaults or usesrequestValue() - [ ]
@IntentParameterDependencynot used on iOS 16 targets (crashes) - [ ] Phrases localized in
AppShortcuts.strings, notLocalizable.strings - [ ] App Intents defined in app bundle, not Swift Package (pre-iOS 17)
- [ ]
isDiscoverable = falsefor internal/widget-only intents
When to Load References
- AppIntent protocol implementation -> intent-structure.md
- Entity queries, identifiers, Spotlight -> entities.md
- App Shortcuts, phrases, discovery -> shortcuts.md
- Parameter validation, dynamic options -> parameters.md
Review Questions
1. Does perform() handle timeout limits for long-running operations? 2. Are entity queries self-contained (no @Dependency injection in Siri context)? 3. Do phrases read naturally and include the app name? 4. Are SwiftData models passed by persistentModelID, not directly? 5. Would migrating from SiriKit break existing user shortcuts?
Hard gates (before reporting)
Complete in order for each finding you intend to report. Do not advance until the pass condition is satisfied.
1. Location artifact — The finding includes [FILE:LINE] (or a line range) copied from the current file contents; the path resolves in this repo. 2. Scope read — You read the full surrounding type: the AppIntent / AppEntity / EntityQuery / AppShortcutsProvider (or equivalent) that contains the flagged code, not only a diff hunk or snippet. 3. Platform or integration claim (only if the finding depends on minimum iOS, Swift Package vs app target, @IntentParameterDependency availability, SiriKit migration, or isDiscoverable / extension placement) — You name one concrete artifact you inspected (for example IPHONEOS_DEPLOYMENT_TARGET or target membership in the Xcode project, Package.swift platforms, entitlements, or where the intent file lives) or you drop or downgrade the finding to an open question. 4. Protocol — Pre-report steps in review-verification-protocol are satisfied for this item (no finding if they are not).
Use the issue format [FILE:LINE] ISSUE_TITLE for each reported finding. Hard gate 4 is the full pre-report checklist for this skill’s review type.
Entities and Queries
AppEntity Protocol
Represents domain objects for use in App Intents:
struct BookEntity: AppEntity, Identifiable {
var id: UUID // Must be stable and persistent
@Property(title: "Title")
var title: String
@Property(title: "Author")
var author: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(title)", subtitle: "\(author)")
}
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book"
static var defaultQuery = BookQuery()
}| Requirement | Purpose |
|---|---|
id | Stable identifier for persistence across sessions |
displayRepresentation | How entity appears in Siri/Shortcuts UI |
typeDisplayRepresentation | Human-readable type name ("Book", "Task") |
defaultQuery | Associated query for lookups |
EntityQuery Protocol
Basic lookup by identifier:
struct BookQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
identifiers.compactMap { Database.shared.book(for: $0) }
}
}EntityStringQuery Protocol
Required for Siri voice input with free-form search:
struct BookQuery: EntityStringQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
identifiers.compactMap { Database.shared.book(for: $0) }
}
func suggestedEntities() async throws -> [BookEntity] {
Database.shared.recentBooks // Shown in picker UI
}
func entities(matching string: String) async throws -> [BookEntity] {
Database.shared.books.filter { $0.title.localizedCaseInsensitiveContains(string) }
}
}Critical: Using plain EntityQuery with Siri causes infinite parameter request loops. Use EntityStringQuery for voice-driven disambiguation.
EnumerableEntityQuery Protocol (iOS 17+)
For small datasets, return all entities and let system filter:
struct ShelfQuery: EnumerableEntityQuery {
func allEntities() async throws -> [ShelfEntity] {
Shelf.allCases.map { ShelfEntity($0) }
}
}Best for: Enums, small fixed sets (<100 items)
EntityPropertyQuery Protocol
For large datasets with property-based filtering:
struct BookQuery: EntityPropertyQuery {
static var sortingOptions = SortingOptions {
SortableBy(\BookEntity.$title)
SortableBy(\BookEntity.$dateAdded)
}
static var properties = QueryProperties {
Property(\BookEntity.$title) { EqualTo, Contains }
Property(\BookEntity.$author) { EqualTo, Contains }
}
func entities(matching predicates: QueryPredicate<BookEntity>,
mode: ComparatorMode,
sortedBy: [EntitySortingOptions<BookEntity>],
limit: Int?) async throws -> [BookEntity] {
// Build NSPredicate from QueryPredicate
}
}iOS 18 Entity Features
IndexedEntity: Spotlight semantic search
struct BookEntity: AppEntity, IndexedEntity {
var attributeSet: CSSearchableItemAttributeSet {
let attrs = CSSearchableItemAttributeSet()
attrs.displayName = title
attrs.contentDescription = summary
return attrs
}
}URLRepresentable: Deep linking
struct BookEntity: AppEntity, URLRepresentable {
static var urlRepresentation: URLRepresentation {
"myapp://book/\(.id)"
}
}SwiftData Integration
// BAD: Passing model directly (not Sendable)
func perform() async throws -> some IntentResult {
let book = fetchBook() // @Model type
processBook(book) // Data race risk
}
// GOOD: Pass by ID, fetch in context
func perform() async throws -> some IntentResult {
let bookID = persistentModelID
let context = ModelContext(container)
let book = context.model(for: bookID) as? Book
}Dependency Injection Limitation
@Dependency and @AppDependency do not work in Siri/Shortcuts context. Queries must be self-contained:
// BAD: Dependency injection in query
struct BookQuery: EntityQuery {
@Dependency var database: Database // nil in Siri
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
database.books(for: identifiers) // Crashes
}
}
// GOOD: Self-contained query
struct BookQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
Database.shared.books(for: identifiers) // Singleton access
}
}Critical Anti-Patterns
// BAD: Plain EntityQuery with Siri voice input
struct BookQuery: EntityQuery { ... } // Infinite prompt loop
// GOOD: EntityStringQuery for voice input
struct BookQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [BookEntity] { ... }
}// BAD: Empty suggested entities
func suggestedEntities() async throws -> [BookEntity] { [] }
// GOOD: Provide reasonable defaults
func suggestedEntities() async throws -> [BookEntity] {
Database.shared.recentBooks.prefix(10)
}// BAD: Non-persistent ID
struct BookEntity: AppEntity {
var id = UUID() // New ID each time!
}
// GOOD: Stable ID from data source
struct BookEntity: AppEntity {
let id: UUID // From database record
}Review Questions
1. Is `EntityStringQuery` used for Siri voice input? Plain EntityQuery causes infinite loops. 2. Does `suggestedEntities()` return useful defaults? Empty results break disambiguation. 3. Are entity IDs stable and persistent? New IDs each instantiation break continuity. 4. Is the query self-contained? @Dependency fails in Siri/Shortcuts context. 5. Is `EnumerableEntityQuery` used only for small sets? Large sets should use EntityPropertyQuery.
Intent Structure
AppIntent Protocol
Required conformance for all App Intents:
struct OpenCurrentlyReading: AppIntent {
static var title: LocalizedStringResource = "Open Currently Reading"
static var openAppWhenRun: Bool = true // Optional: default false
@MainActor
func perform() async throws -> some IntentResult {
Navigator.shared.openShelf(.currentlyReading)
return .result()
}
}| Property | Required | Default | Purpose |
|---|---|---|---|
title | Yes | - | Localized display name |
openAppWhenRun | No | false | Launch app before execution |
isDiscoverable | No | true | Show in Shortcuts app (iOS 17+) |
Return Types
perform() returns some IntentResult with optional protocol conformances:
| Protocol | Purpose | Example |
|---|---|---|
ReturnsValue<T> | Pass data to next intent | .result(value: book) |
ProvidesDialog | Siri voice/text response | .result(dialog: "Done!") |
ShowsSnippetView | SwiftUI visual feedback | .result(view: SuccessView()) |
OpensIntent | Chain to another intent | .result(opensIntent: NextIntent()) |
// Combined return type
func perform() async throws -> some IntentResult & ReturnsValue<BookEntity> & ProvidesDialog {
return .result(value: book, dialog: "Added \(book.title) to Library!")
}Threading
perform()runs on arbitrary background queue by default- Mark with
@MainActorfor UI operations or main thread access - Long operations must complete within ~30 seconds or time out
Error Handling
Custom errors must provide localized messages:
enum BookIntentError: Error, CustomLocalizedStringResourceConvertible {
case notFound
case networkError(String)
var localizedStringResource: LocalizedStringResource {
switch self {
case .notFound: return "Book not found"
case .networkError(let msg): return "Network error: \(msg)"
}
}
}Use AppIntentError for standard cases:
.insufficientAccount- Needs sign-in.entityNotFound- Entity missing.needsValue- Parameter required
iOS 17+ Protocols
ForegroundContinuableIntent: Continue in app with custom UI
throw needsToContinueInForegroundError() // Stop and require user action
try await requestToContinueInForeground() // Continue with user inputProgressReportingIntent: Long-running operations
func perform() async throws -> some IntentResult {
progress.totalUnitCount = 100
for i in 0..<100 {
progress.completedUnitCount = Int64(i)
// ... work
}
return .result()
}Critical Anti-Patterns
// BAD: Heavy work without timeout consideration
func perform() async throws -> some IntentResult {
let data = try await downloadLargeFile() // May exceed 30s limit
return .result()
}
// GOOD: Open app for long operations
static var openAppWhenRun = true
func perform() async throws -> some IntentResult {
// App handles long operation with proper UI
}// BAD: Generic error without localization
throw NSError(domain: "app", code: 1, userInfo: nil)
// GOOD: Localized error message
throw BookIntentError.notFound// BAD: UI work without @MainActor
func perform() async throws -> some IntentResult {
UIApplication.shared.open(url) // Crashes
}
// GOOD: Mark for main thread
@MainActor
func perform() async throws -> some IntentResult {
UIApplication.shared.open(url)
}Review Questions
1. Does `perform()` complete within 30 seconds? Long downloads/processing should open app. 2. Is `@MainActor` used for UI operations? Intents run on background queues by default. 3. Do custom errors provide localized messages? Raw Error gives poor Siri feedback. 4. Is `openAppWhenRun` set appropriately? Background-capable intents should stay false. 5. Is `isDiscoverable = false` for internal intents? Widget-only intents shouldn't clutter Shortcuts.
Parameters
@Parameter Property Wrapper
Declares user-configurable inputs:
struct OpenBook: AppIntent {
@Parameter(title: "Book")
var book: BookEntity
@Parameter(title: "Page", default: 1)
var page: Int
@Parameter(title: "Read Aloud")
var readAloud: Bool? // Optional = not required
}| Option | Purpose |
|---|---|
title | Localized display name (required) |
default | Default value for parameter |
description | Help text for parameter |
requestValueDialog | Prompt when requesting value |
Supported Types
- Primitives:
Int,Double,Bool,String,Date,URL - Collections:
[T]where T is supported - Enums: Must conform to
AppEnum - Entities: Must conform to
AppEntity - Files:
IntentFilefor file handling
AppEnum for Fixed Values
enum Priority: String, AppEnum {
case low, medium, high
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Priority"
static var caseDisplayRepresentations: [Priority: DisplayRepresentation] = [
.low: "Low",
.medium: "Medium",
.high: "High"
]
}ParameterSummary
Natural language description with embedded parameters:
static var parameterSummary: some ParameterSummary {
Summary("Open \(\.$book) at page \(\.$page)")
}iOS 17+: Conditional summaries based on widget family:
static var parameterSummary: some ParameterSummary {
When(\.$includeDetails, .equalTo, true) {
Summary("Show \(\.$book) with details")
} otherwise: {
Summary("Show \(\.$book)")
}
}Dynamic Options
Provide runtime-computed options:
struct BookParameter: DynamicOptionsProvider {
func results() async throws -> [BookEntity] {
Database.shared.availableBooks
}
func defaultResult() async -> BookEntity? {
Database.shared.lastOpenedBook
}
}
@Parameter(title: "Book", optionsProvider: BookParameter())
var book: BookEntity@IntentParameterDependency (iOS 17+)
Access other parameters in options provider:
struct ChapterParameter: DynamicOptionsProvider {
@IntentParameterDependency<OpenBook>(\.book)
var bookDependency
func results() async throws -> [ChapterEntity] {
guard let book = bookDependency?.book else { return [] }
return book.chapters
}
}Warning: @IntentParameterDependency crashes on iOS 16. Guard with availability:
if #available(iOS 17, *) {
// Use dependency
}User Interaction
Request values or disambiguation during perform():
func perform() async throws -> some IntentResult {
// Request missing value
let book = try await $book.requestValue("Which book?")
// Disambiguation from options
let chapter = try await $chapter.requestDisambiguation(
among: book.chapters,
dialog: "Which chapter?"
)
// Confirmation
let confirmed = try await $book.requestConfirmation(
for: book,
dialog: "Open \(book.title)?"
)
}Note: User cancellation throws an error - handle gracefully.
Validation
Validate parameters before use:
func perform() async throws -> some IntentResult {
guard page > 0 && page <= book.pageCount else {
throw BookIntentError.invalidPage
}
// ...
}For complex validation, use requestValue() with specific prompts.
Critical Anti-Patterns
// BAD: Non-optional parameter without default
@Parameter(title: "Count")
var count: Int // Required with no default - user must always provide
// GOOD: Optional or has default
@Parameter(title: "Count", default: 10)
var count: Int// BAD: @IntentParameterDependency on iOS 16 target
@IntentParameterDependency<MyIntent>(\.param)
var dependency // Crashes on iOS 16
// GOOD: Guard with availability
@available(iOS 17, *)
@IntentParameterDependency<MyIntent>(\.param)
var dependency// BAD: Ignoring requestConfirmation cancellation
func perform() async throws -> some IntentResult {
try await $action.requestConfirmation(for: action) // Throws on cancel
performAction() // Runs even if canceled?
}
// GOOD: Handle cancellation
func perform() async throws -> some IntentResult {
do {
try await $action.requestConfirmation(for: action)
performAction()
} catch {
// User canceled - graceful exit
return .result()
}
}// BAD: Missing defaultResult in DynamicOptionsProvider
struct BookParameter: DynamicOptionsProvider {
func results() async throws -> [BookEntity] { ... }
// No defaultResult - non-optional params fail without explicit selection
}
// GOOD: Provide default
struct BookParameter: DynamicOptionsProvider {
func results() async throws -> [BookEntity] { ... }
func defaultResult() async -> BookEntity? {
Database.shared.lastOpenedBook
}
}Review Questions
1. Do non-optional parameters have defaults or use `requestValue()`? 2. Is `@IntentParameterDependency` guarded for iOS 17+? Crashes on iOS 16. 3. Are user cancellations from `requestConfirmation` handled? They throw errors. 4. Does `DynamicOptionsProvider` implement `defaultResult()`? Required for non-optional params. 5. Are parameter summaries written as natural sentences?
Shortcuts Integration
AppShortcutsProvider
Registers intents for automatic discovery in Shortcuts app and Siri:
struct LibraryAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: OpenCurrentlyReading(),
phrases: [
"Open Currently Reading in \(.applicationName)",
"Show my reading list in \(.applicationName)"
],
shortTitle: "Open Reading List",
systemImageName: "books.vertical.fill"
)
}
}| Property | Required | Purpose |
|---|---|---|
intent | Yes | The AppIntent instance to invoke |
phrases | Yes | Siri trigger phrases (must include app name) |
shortTitle | Yes | Brief description for UI |
systemImageName | Yes | SF Symbol for visual display |
Phrase Requirements
Critical: Every phrase MUST include .applicationName:
// BAD: Missing app name
phrases: ["Open my books", "Show reading list"] // Won't be discoverable
// GOOD: Includes app name
phrases: [
"Open my books in \(.applicationName)",
"Show reading list with \(.applicationName)"
]Limits:
- Maximum 1,000 total phrases per app (including parameter variations)
- Use natural language that reads well when spoken
Localization
Phrases must be in AppShortcuts.strings (or AppShortcuts.xcstrings for iOS 18+):
// AppShortcuts.strings
"Open Currently Reading in ${applicationName}" = "Open Currently Reading in ${applicationName}";Critical: Using Localizable.strings for phrases does NOT work.
Parameterized Phrases
Include parameters using \(.$parameterName):
AppShortcut(
intent: OpenBook(),
phrases: [
"Open \(\.$book) in \(.applicationName)",
"Read \(\.$book) with \(.applicationName)"
],
shortTitle: "Open Book",
systemImageName: "book"
)Warning: Custom AppEntity parameters in phrases may prevent shortcuts from appearing. Test thoroughly.
iOS 17+ Extensions
Define AppShortcutsProvider in App Intents extensions (not main app) for faster startup:
// In App Intents Extension target
struct BookShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] { ... }
}Extensions skip UI, analytics, and non-critical initialization.
Discovery Issues
Common reasons shortcuts don't appear:
| Issue | Solution |
|---|---|
| Missing in Shortcuts app | Check Project Target > General > Supported Intents |
| Xcode version mismatch | Try Xcode beta or release; use xcode-select |
| App Intents in Swift Package | Move to main app bundle (pre-iOS 17) |
| Release build issues | Mark all App Intents as public |
| Metadata processor failure | Simplify custom types; check build logs |
Migration from SiriKit
When migrating from INIntent to AppIntent:
struct OpenBookIntent: AppIntent {
// Conform for migration
static var intentClassName: String? = "OpenBookIntent"
}Warning: CustomIntentMigratedAppIntent conformance breaks iOS 16 even with availability annotations.
Multilingual Considerations
- App names in different languages than Siri's language cause recognition failures
- Test with Siri language matching app language settings
- Consider region-specific phrase variations
Critical Anti-Patterns
// BAD: Phrase without app name
AppShortcut(
intent: OpenBook(),
phrases: ["Open my book"], // Not discoverable by Siri
...
)
// GOOD: App name included
AppShortcut(
intent: OpenBook(),
phrases: ["Open my book in \(.applicationName)"],
...
)// BAD: Localization in wrong file
// Localizable.strings - WRONG FILE
"Open book" = "Open book";
// GOOD: Use AppShortcuts.strings
// AppShortcuts.strings - CORRECT FILE
"Open book in ${applicationName}" = "Open book in ${applicationName}";// BAD: Complex entity parameter in phrase (may fail)
AppShortcut(
intent: ProcessBook(),
phrases: ["Process \(\.$complexEntity) in \(.applicationName)"],
...
)
// GOOD: Simple parameters or none
AppShortcut(
intent: ProcessBook(),
phrases: ["Process current book in \(.applicationName)"],
...
)Review Questions
1. Do all phrases include `.applicationName`? Required for Siri discovery. 2. Are phrases in `AppShortcuts.strings`? Localizable.strings doesn't work. 3. Is the app bundle correct? Swift Package intents won't appear (pre-iOS 17). 4. Are custom entity parameters tested in phrases? Complex entities may break discovery. 5. Is migration handled carefully? CustomIntentMigratedAppIntent breaks iOS 16.