Highlight
App Intents adds APIs for custom dialog responses, interaction donation, entity ownership, screen awareness, and system entity annotation, helping apps provide more natural and personalized experiences in Siri and Apple Intelligence.
Core Content
In the past, developers had limited control when letting an app perform actions through Siri. Siri generated responses automatically, and developers could not match the app’s distinctive voice or visual style. When users performed actions inside the app, Apple Intelligence could not learn from those behavior patterns. App content also was not transparent enough to Siri: the user could see what was on screen, while Siri might not be able to “see” it.
At WWDC26, Apple introduced advanced App Intents features that address these limitations.
Developers can now customize Siri’s dialog responses so replies match the app’s language and tone. Apps can use interaction donation APIs to tell Apple Intelligence what users do in the UI, helping the system understand preferences. Entity ownership declarations tell Siri which content is public or shared, so it can decide whether confirmation is needed. Screen awareness APIs connect on-screen content to entities Siri can understand. System-level entity annotations let Siri understand app content in notifications, Now Playing, alarms, and other system experiences.
Together, these capabilities make collaboration between apps and Siri deeper and make the user experience more coherent.
Details
Custom dialog responses (02:00)
The ProvidesDialog protocol lets an intent return custom dialog. Siri can display the supporting text and read the full text aloud on voice-first devices.
@AppIntent(schema: .audio.addToPlaylist)
struct AddToPlaylistIntent {
func perform() async throws -> some IntentResult & ProvidesDialog {
// Adds song to playlist and responds
return .result(
dialog: IntentDialog(
full: """
Added \(song.title) to the \
\(playlist.title) mix tape.
""",
supporting: "Added"
)
)
}
}
Key points:
- The
ProvidesDialogprotocol declares that an intent provides custom dialog - The
fullfield inIntentDialogcontains the full description for voice devices - The
supportingfield is a shorter version that can be shown in the UI
During intent execution, you can use $label.requestValue() to ask the user a question.
@AppIntent(schema: .clock.createTimer)
struct CreateTimerIntent {
var duration: Duration
var label: String?
var isSleepTimer: Bool
func perform() async throws -> some ReturnsValue<TimerEntity> {
// Checks active timers and requests label parameter
label = try await $label.requestValue(
"""
You already have a timer running. \
What should we call this one?
"""
)
return .result(value: timerEntity)
}
}
Key points:
$label.requestValue()requests user input during execution- It fits situations that need clarification, such as avoiding duplicate names
Intent responses can also return a custom SwiftUI view by using the ShowsSnippetView protocol.
@AppIntent(schema: .audio.addToPlaylist)
struct AddToPlaylistIntent {
var audioEntity: AudioEntity
var playlist: PlaylistEntity
func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {
// Adds to playlist and shows dialog and snippet
let view = PlaylistSnippetView(
playlist: updatedEntity,
tracks: updated.tracks
)
return .result(dialog: dialog, view: view)
}
}
Entity display representation (04:26)
Entities can define their visual presentation in the system with DisplayRepresentation.
@AppEntity(schema: .audio.song)
struct SongEntity {
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(artistName)",
image: artworkImage
)
}
}
Key points:
DisplayRepresentationdefines how an entity appears in Siri, Spotlight, and Shortcuts- It includes three components:
title,subtitle, andimage - It is used in entity pickers, confirmation dialogs, and similar experiences
Interaction donation (07:00)
Use IntentDonationManager to tell Apple Intelligence about actions the user takes in your app UI, so the system can learn user preferences.
@ModelActor
actor ModelManager {
func sendMessage(_ /* ... */, donateIntent: Bool = false) async throws -> [Message.ID] {
// Donate intent with parameters and result so Siri can learn user preferences
if donateIntent {
let intent = SendMessageIntent()
intent.destination = .recipients(conversation.recipients.map(\.entity))
let result = messages.map(\.entity)
Task {
try await IntentDonationManager.shared.donate(
intent: intent,
result: .result(value: result)
)
}
}
}
}
Key points:
- Donation is needed only for UI interactions; Siri-triggered intents are already recorded automatically
- Pass both the intent parameters and the execution result
- The system uses this information to infer preferences, such as which app a user tends to use for a specific contact
Excessive donation will be ignored by the system, so donate only when the action truly represents user behavior.
Entity ownership declarations (10:03)
The OwnershipProvidingEntity protocol tells Siri whether an entity is public or shared, which affects confirmation behavior.
@AppEntity(schema: .calendar.event)
struct EventEntity: OwnershipProvidingEntity {
var ownership: EntityOwnership {
// isShared used to compute ownership state: .shared, .public, or .unknown
attendees.isEmpty ? .unknown : .shared
}
}
Key points:
- Actions on public or shared entities are more likely to trigger confirmation
- The default is
.unknown, and the system assumes the entity is private - Return the value dynamically based on entity state
Semantic indexing and structured search (11:30)
Use IndexedEntity and CSSearchableIndex.indexAppEntities() to add entities to Spotlight’s semantic index.
struct EntityIndexingHelper {
// Indexes playlist entities
func indexPlaylist(_ playlist: Playlist) async throws {
let entity = PlaylistEntity(playlist: playlist)
try await CSSearchableIndex(name: indexName)
.indexAppEntities([entity])
}
}
Key points:
- Once indexed, entities can be searched by name through Siri
- Spotlight can also find these entities
- Some domains support semantic search, not just keyword matching
For content that is not indexed in advance, use IntentValueQuery to provide structured search.
struct AudioIntentValueQuery: IntentValueQuery {
func values(for input: AudioSearch) async throws -> [AudioEntity] {
switch input.criteria {
case .searchQuery(let query):
return try await searchResults(for: query)
case .unspecified:
return try await likedSongResults()
// ... also a .url case
}
}
}
Key points:
IntentValueQueryreceives structured search input- It can return multiple entity types through
UnionValue - It supports conditions such as
.searchQuery,.unspecified, and.url
In-app search (14:49)
The .system.searchInApp schema lets Siri perform a search inside your app instead of displaying results directly.
@AppIntent(schema: .system.searchInApp)
struct SearchAudioLibraryIntent {
var criteria: StringSearchCriteria
func perform() async throws -> some IntentResult {
// Perform in-app search with Siri search string
navigation.searchText = criteria.term
navigation.selectedTab = .library
return .result()
}
}
Key points:
- This fits carefully designed in-app search experiences
- It works even if entities are not indexed
- Pass the search string into the app’s navigation system
Screen awareness (16:27)
Screen awareness lets Siri understand entities currently displayed on screen. There are three main APIs.
Use NSUserActivity for a single primary entity.
struct NowPlayingView: View {
@Environment(PlaybackController.self) private var playback
var body: some View {
VStack {
// Player UI
}
.userActivity("cosmotunes.nowPlaying", isActive: playback.currentTrack) { activity in
activity.title = playback.currentTrack?.title
activity.appEntityIdentifier = EntityIdentifier(
for: SongEntity.self,
identifier: playback.currentTrack.id
)
}
}
}
Key points:
- Use this when the whole screen presents one entity
appEntityIdentifierconnects the activity to a specific entity
Use View Entity annotation for multiple entities in a list.
struct AlbumView: View {
private var header: some View {
VStack(alignment: .leading, spacing: 6) {
// ...
}
.appEntityIdentifier(
EntityIdentifier(for: AlbumEntity.self, identifier: session.id.uuidString)
)
}
}
Key points:
- Add an annotation to each view that represents an entity
- This fits screens with a small number of entities
Use Collection annotation for large sets of entities.
struct PlaylistDetailView: View {
var body: some View {
List {
ForEach(playlist.tracks) { track in
PlaylistTrackRow(track: track)
}
}
.appEntityIdentifier(forSelectionType: GeneratedTrack.ID.self) { trackID in
EntityIdentifier(for: SongEntity.self, identifier: trackID)
}
}
}
Key points:
- Avoid annotating every list item individually
- The system fetches entity identifiers on demand
- It supports entities that have scrolled off screen
For custom canvas views, use custom canvas view annotation. See the sample code for details.
System entity annotations (21:07)
Annotate entities in system experiences such as notifications, Now Playing, and alarms.
// User notifications
func scheduleNotification(message: Message, author: Contact, conversation: Conversation) {
let content = UNMutableNotificationContent()
content.title = author.name
content.body = message.body
content.appEntityIdentifiers = [
EntityIdentifier(for: MessageEntity.self, identifier: message.id)
]
}
// Now Playing
final class CosmoTunesMediaSession: MediaSessionRepresentable {
var content: (any MediaContentRepresentable)? {
var content = MusicContent(id: track.id.uuidString, songTitle: track.title /* ... */)
content.appEntityIdentifiers = [
EntityIdentifier(for: SongEntity.self, identifier: track.id),
EntityIdentifier(for: ArtistEntity.self, identifier: track.session.artistName),
EntityIdentifier(for: PlaylistEntity.self, identifier: currentPlaylist.id),
]
return content
}
}
// AlarmKit
func scheduleAlarm(_ alarm: Alarm) async throws {
let configuration = AlarmManager.AlarmConfiguration<CosmoTunesAlarmMetadata>.alarm(
schedule: schedule,
attributes: attributes,
appEntityIdentifier: EntityIdentifier(for: AlarmEntity.self, identifier: alarm.id),
stopIntent: DismissAlarmIntent(),
secondaryIntent: SnoozeAlarmIntent(),
sound: sound
)
}
Key points:
- All three experiences use the same annotation pattern
- Arrange entity identifiers from most specific to most general
- You cannot use
TransientAppEntity, because a persistent identifier is required
Key Takeaways
1. Personalized dialog responses
What to do: Add custom dialog to your app’s key intents so Siri’s responses sound like your app.
Why it is worth doing: Siri’s automatically generated responses are generic and cannot match an app’s distinctive tone. Custom dialog makes users feel that Siri truly understands the app. Start with high-frequency actions such as adding songs or creating timers for the best return on effort.
How to start: Have the intent’s perform() return some IntentResult & ProvidesDialog, and use IntentDialog(full:supporting:) to define both full and short replies. Entry point: ProvidesDialog protocol + IntentDialog
2. UI donation
What to do: Add intent donation to core app workflows, such as sending a message, starting navigation, or playing music.
Why it is worth doing: Apple Intelligence needs to understand what users actually do inside your app before it can provide personalized suggestions. Donation tells the system, “This user tends to use this app to message this person,” so Siri can prioritize it next time.
How to start: After the action completes, call IntentDonationManager.shared.donate(intent:result:), passing an intent with complete parameters and the execution result. Donate only for real UI interactions, because Siri calls are already recorded automatically. Entry point: IntentDonationManager.shared.donate(intent:result:)
3. Entity ownership awareness
What to do: If your app has public or shared content, implement OwnershipProvidingEntity.
Why it is worth doing: Actions on public or shared entities are more likely to trigger Siri confirmation, helping users avoid accidental changes to shared data. Clear ownership state tells Siri when to ask one more question and improves trust in your app.
How to start: Make the entity conform to OwnershipProvidingEntity and implement the ownership property, returning .shared, .public, or .unknown dynamically based on entity state. Entry point: OwnershipProvidingEntity protocol + var ownership: EntityOwnership
4. Semantic indexing
What to do: Add relatively stable content entities in your app to the Spotlight index.
Why it is worth doing: Users may forget which app contains an item, but remember its name. Once added to Spotlight’s semantic index, that content can be opened directly from system search, reducing discovery friction.
How to start: Make the entity conform to IndexedEntity, then call CSSearchableIndex.indexAppEntities() to add it to the index. For content that is not pre-indexed, provide structured search with IntentValueQuery. Entry point: IndexedEntity + CSSearchableIndex.indexAppEntities()
5. Screen awareness
What to do: Add entity annotations to your detail and list screens so Siri understands what is currently visible.
Why it is worth doing: Once users can see something on screen, they can say “play the third one” or “share that” without speaking the full name. This removes the gap where the user sees an item but Siri does not, making voice interaction more natural.
How to start: Use .userActivity with appEntityIdentifier for a single entity, annotate views with .appEntityIdentifier for a small number of entities, and use .appEntityIdentifier(forSelectionType:) Collection annotation for large lists. Entry point: appEntityIdentifier + EntityIdentifier(for:identifier:)
Related Sessions
- What’s new in App Intents - An overview of App Intents fundamentals and new capabilities
- Code-along: Make your app available to Siri - A hands-on walkthrough for implementing App Intents
- App Intents Testing - Tools and techniques for testing App Intents
- Secure your app: Mitigate risks to agentic features - Security considerations for agentic apps
- Apple Intelligence and Siri AI - An overview of Apple Intelligence and Siri AI
Comments
GitHub Issues · utterances