home / skills / mintuz / claude-plugins / app-intent-driven-development
npx playbooks add skill mintuz/claude-plugins --skill app-intent-driven-developmentReview the files below or copy the command above to add this skill to your agents.
---
name: app-intent-driven-development
description: WHEN designing features App Intent-first so Siri/Shortcuts, widgets, and SwiftUI share logic; NOT UI-first; reuse intents across app.
---
# App Intent–First Driven Development
Design features as App Intents first, then reuse those intents across Shortcuts, widgets, and SwiftUI views so automation and UI stay in lockstep.
## Core Ideas
- **Entities first**: model the data users act on (events, categories, records) as `AppEntity` so intents, widgets, and the app share one source of truth.
- **Intent-first feature**: build the App Intent + entity query before UI; SwiftUI screens call those intents instead of duplicating service code.
- **Single action, single intent**: keep intents focused; avoid mega-intents that are hard to compose in Shortcuts.
- **Predictable UI**: supply `DisplayRepresentation`, `typeDisplayRepresentation`, and icons so Siri/Shortcuts can render rich cards without opening the app.
- **Fast queries**: `EntityQuery` must be quick and cancellable; avoid blocking the main actor.
- **Reuse business logic**: intents call the same services your views use; do not fork logic inside the intent.
## Minimal Entity Blueprint
```swift
import AppIntents
struct TaskEntity: AppEntity, Identifiable {
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Task")
static let defaultQuery = TaskQuery()
let id: UUID
let title: String
let isComplete: Bool
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: title,
subtitle: isComplete ? "Completed" : "Open",
image: .init(systemName: isComplete ? "checkmark.circle.fill" : "circle")
)
}
}
struct TaskQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
try await TaskStore.shared.fetch(ids: identifiers) // fast path
}
func suggestedEntities() async throws -> [TaskEntity] {
try await TaskStore.shared.fetchRecent()
}
}
```
**Key points:** stable identifier, meaningful representation, and fast queries that avoid launching heavy app flows.
## Intent Pattern
```swift
import AppIntents
struct CompleteTaskIntent: AppIntent {
static let title: LocalizedStringResource = "Complete Task"
static let description = IntentDescription("Marks a task as done and returns the updated item.")
@Parameter(title: "Task", requestValueDialog: "Which task should I complete?")
var task: TaskEntity
// Used so we can call the intent from SwiftUI using .perform()
init(task: TaskEntity) { self.task = task }
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<TaskEntity> {
let updated = try await TaskStore.shared.complete(task.id)
return .result(value: updated)
}
static var parameterSummary: some ParameterSummary {
Summary("Complete \(\.$task)")
}
}
```
- **Parameters**: keep them few; provide `requestValueDialog` to make Siri prompts natural.
- **Results**: return entities when possible; system surfaces render them nicely.
- **Isolation**: mark with `@MainActor` only if you must touch UI-bound objects; otherwise keep work off the main actor.
## Reusing Intents in SwiftUI
- Prefer calling intents from UI so automation and in-app flows share one path.
- Use `AppIntentButton` to invoke intents directly from views.
- Translate entity selections into view state so widgets/Shortcuts and in-app pickers present the same objects.
```swift
import AppIntents
import SwiftUI
struct EventRow: View {
let event: EventEntity
var body: some View {
HStack {
Text(event.name)
Spacer()
AppIntentButton(intent: UndoLastEventOccuranceIntent(event: event)) {
Label("Undo", systemImage: "arrow.uturn.backward")
}
}
}
}
```
- For more control, invoke intents imperatively with `perform` (e.g., to show progress or handle errors):
```swift
import AppIntents
import SwiftUI
struct EventRow: View {
@Environment(\.intentExecutor) private var executor
@State private var isWorking = false
@State private var error: Error?
let event: EventEntity
var body: some View {
HStack {
Text(event.name)
Spacer()
Button {
Task {
isWorking = true
defer { isWorking = false }
do {
try await executor.perform(UndoLastEventOccuranceIntent(event: event))
} catch {
self.error = error
}
}
} label: {
if isWorking {
ProgressView()
} else {
Label("Undo", systemImage: "arrow.uturn.backward")
}
}
}
.alert("Undo failed", isPresented: .init(
get: { error != nil },
set: { if !$0 { error = nil } }
)) {
Button("OK", role: .cancel) { error = nil }
} message: {
Text(error?.localizedDescription ?? "Unknown error")
}
}
}
```
- Keep the intent signature identical between Shortcuts and SwiftUI usage.
- Avoid reimplementing service calls in views; route through the intent to keep analytics, validation, and side effects consistent.
## Development Flow
1. Model the domain type as `AppEntity` with `DisplayRepresentation` and `EntityQuery`.
2. Implement a focused `AppIntent` that calls shared services; avoid duplicate data access layers inside the intent.
3. Add previews in Shortcuts or the App Intents preview panel; ensure suggested entities show immediately.
4. Expose the same entity in widgets/Live Activities to keep automation and UI consistent.
5. Localize strings early (`LocalizedStringResource`) to keep Siri responses natural in all supported languages.
## Quick Checklist
- [ ] Entity has stable `id`, `typeDisplayRepresentation`, and rich `displayRepresentation`.
- [ ] Queries are fast, cancellable, and return suggestions without opening the app.
- [ ] Intent reuses shared domain services; no duplicated business logic.
- [ ] Parameters are minimal and well phrased with `requestValueDialog`.
- [ ] Results return entities when possible for better system rendering.
- [ ] Strings are localized; tests cover queries and perform paths.