-
Discover new capabilities in the App Intents framework
Level up your App Intents adoption with advanced features to make it faster, more flexible, and more relevant. Find out how ValueRepresentation and RelevantEntities make your content more discoverable and allow it to travel across apps, EntityCollection improves performance, and SyncableEntity let you scale across devices. Explore richer parameter types including union values and long-running intents that handle cancellation gracefully.
Chapters
- 0:00 - Introduction
- 2:40 - Share entities across apps with ValueRepresentation
- 3:45 - Register relevant entities with RelevantEntities
- 7:05 - Handle entities efficiently with EntityCollection
- 8:55 - Use entities across devices with SyncableEntity
- 11:01 - Richer parameter types
- 12:38 - Union value parameters
- 13:26 - Extend execution with LongRunningIntent
- 15:27 - Target the right process with ExecutionTargets
- 17:14 - Next steps
Resources
-
Search this video…
Hi, my name is Moe, an engineer on the App Intents team. I'm excited to share with you, the new App Intents capabilities we're introducing with our 2027 releases. App Intents is the framework that lets you express your app's actions and content to other parts of the system in ways that feel natural and deeply integrated.
From Siri and the Shortcuts app to Spotlight and Widgets, App Intents has been the engine behind some of the most delightful experiences on Apple platforms.
And is now a key pillar of Apple Intelligence. In our 2027 releases, driven by your feature requests, we're bringing more control, more flexibility, and a significantly smoother developer experience.
Today, we'll explore three areas. We'll start with entities. I'll show you new ways to share them across apps, tell the system when they're relevant, and process them at scale.
Then, I'll walk you through new support for native types and union values with full Shortcuts integration.
And finally, I'll show you how your intents can run longer, handle cancellation gracefully, and target the right process for execution.
I'm going to focus on new features today. But, if you're new to App Intents or need a refresher on the basics, I'd recommend checking out "Get to know App Intents" from WWDC25.
That video introduces the Landmarks Travel Tracking app. I'll be building on it with the APIs we cover today. You can download the sample code and follow along.
Let's dive into what's new. Your entities, your app's content, like a landmark or a playlist, live inside your app. But the people using your app don't. They move between apps all the time. Let's go through a pair of examples using Mail and Maps with my travel tracking app. I built a shortcut to share trip ideas with friends. It finds a nearby landmark and sends it along with a message. My entity conforms to transferable from the CoreTransferable framework. So the shortcut can share it in a format Mail can use. And that works great.
But what if instead of sending it to a friend, I want to get directions to that landmark? Well, that won't work. Maps needs some structured information — a coordinate, an address, or something it can navigate to. But that kind of data doesn't have an associated data format that can be put in a file or data.
The existing file and data representations work great for known formats like PDFs or images — but not for structured types that don't have any.
This is where ValueRepresentation comes in. It's a new representation type that lets you share structured types that the system already understands.
Here's my LandmarkEntity — it represents a place in the travel tracking app. It already conforms to Transferable, so I just need to add a ValueRepresentation alongside any existing representations. Inside my ValueRepresentation, I export my landmark's coordinate and name, as a PlaceDescriptor from the GeoToolbox framework. PlaceDescriptor carries coordinates and other metadata that Maps needs to navigate.
If my entity already has a PlaceDescriptor @Property, I can skip the closure entirely and use a key-path. Same result, much less code.
So, going back to my shortcut, I tap run — my landmark flows to Maps as a PlaceDescriptor, and Maps opens with directions to the landmark.
Your entities now have more ways to travel across apps. Now, let's talk about helping the system suggest them when they are relevant. Suppose you're building a music app like CosmoTunes, the sample app from the video "Explore advanced App Intents features for Siri and Apple Intelligence".
Your app has a brand new, high-tempo playlist, that's perfect for running. When someone sets up a running workout in the Fitness app, they get a list of suggested playlists.
How do you get your playlist into those suggestions? Today, you have two ways to make your content available to the system. The first is to index your content with Spotlight. This makes it available to people searching for your content in the Spotlight UI, including semantic search. This is also the primary way Siri is able to retrieve your content.
The second approach is interaction donation. When people take actions in your app, you donate those interactions to the system through the IntentDonationManager API. Over time, the system learns patterns and can suggest similar actions in the future. Siri also uses these interactions to deliver a more personalized experience.
But what about that new playlist? Nobody's searched for it in Spotlight since they don't know it exists.
And since nobody's played it, there's no interaction to donate either. You need a way to tell the system this playlist is relevant so it can surface it at the right moment.
Introducing RelevantEntities. With RelevantEntities, you can suggest entities to the system and provide context about when and why they're relevant. Here's how this works.
You start by identifying the relevant entities — in this case, your running playlists. Next, you create a context to tell the system these playlists are relevant when someone starts a run. Then you call updateEntities to register them.
The system surfaces these playlists as suggestions in the right context — even if they were never played before.
Entities stay registered until you remove them. You can removeAllEntities for a specific context, remove specific entities from a context, or clear all your entities across all contexts.
Now you have more options for helping people discover your content. How do you choose between them? Use Spotlight when you want your content to be searchable and retrievable by Siri.
Use interaction donation to teach Siri and the system how people use your app — so it can identify patterns and suggest actions people may want to repeat. And use RelevantEntities to hint to the system which content is relevant in specific situations — so the system can suggest it at the right moment.
For more on these topics, check out our new documentation on Spotlight and interaction donation.
Your entities are shared and the system knows when they're relevant. Now, let's make them more efficient.
Back in the travel tracking app. The app has landmark photos, but I also wanted to let people save their own travel photos.
So I added a photo album view — and to make the photos available to the system, I defined a PhotoEntity with an app schema for photos. This gives the system the context it needs to work with my photos across Siri, Shortcuts, and Spotlight.
I also created an intent to tag my photos by keyword so people could organize and find them easily.
As the photo library grew, I noticed something. Tagging a lot of photos at once was slower than expected.
Let's walk through the code to understand why.
The intent is very simple, it just adds a keyword to my photos. It takes a list of photo entities and a tag as @Parameter.
And the perform method just applies the tag to each photo item. So why was that, actually a problem? Well, it has to do with how app intents resolve parameters.
Before an intent runs, the system resolves every entity. That means calling the entity query to populate all of its properties, so the intent has everything it may need. For most intents, that's exactly what you want. But in my case, this meant resolving hundreds or thousands of photo entities, even though my code only needs the entity ID to update my data model. So, how do I fix this? EntityCollection fixes this. It's a new type that stores an array of entity identifiers, instead of the fully resolved entities.
When you use EntityCollection as your parameter type, the system passes just the identifiers to the intent's perform method, without resolving the full entities. Here's the updated code. I changed my @Parameter type to EntityCollection, and passed the identifiers directly to my tagging method. And that's all it took.
To confirm the fix worked, I built a Shortcut to find and tag 1000 photos. First, with a regular array of photo entities.
Then with EntityCollection, which was almost instant.
The code change is small, but the performance difference is significant.
Now, what happens when the same entity needs to work on multiple devices? With our 2027 releases, Siri can continue conversations across devices — and your entities can be part of those conversations.
If your app runs on multiple devices, people might start a conversation with Siri on one device and continue on another. But there's a challenge. If I ask Siri on my iPhone to add a photo to an album, then switch to my other device and ask Siri to tag that photo — Siri might not be able to find that photo. To understand why, let's think about how entities are identified. Every entity needs an ID, that's how the system finds it. Your entity's ID might be generated locally on each device. Local IDs work great on the device they were created on. But each device generates its own local IDs.
So the same entity can end up with a different ID on each device.
For Siri to reference your entities across devices, it needs a stableID that's the same everywhere. That could come from your server, or from CloudKit record IDs. Then, you need a way to tell the system your entity's ID is stable.
That's what SyncableEntity does — it declares to the system that your entity's ID is stable and can be used across devices. Here's how to adopt it.
I start by adding the SyncableEntity protocol to my entity. Then, I need to provide the stable ID.
If your entity already uses an ID that's the same across all devices — like a server-assigned UUID or a CloudKit record ID — no more change is needed.
But if you use local identifiers, like CoreData row IDs, you need both: a local ID and a stable one. SyncableEntityIdentifier pairs them into a single ID. On-device, your code uses the local ID. And across devices, the system uses the stable one.
So far, we've focused on entities. Now, let's talk about the intents that use them.
Your intents take parameters — the inputs people provide, like a date, a name, or an address.
When you declare a @Parameter, the system gives you a native picker, Siri understanding, and localization for free. We're extending that same support to more native types.
We're adding native support for Duration, so no more building custom time pickers. And PersonNameComponents for structured name input instead of a plain string.
And more.
Each one gets a native picker and works everywhere your intent does — Siri, Shortcuts and Widgets.
Those are individual types — one type per @Parameter. But sometimes a parameter needs to accept more than one type.
A union value is a Swift enum where each case wraps a different type, letting a single parameter represent one of several options.
Now that I have both landmarks and travel photos in the app, I wanted a widget that shows photos from either a photo album or a landmark collection. With @UnionValue supporting input parameters, I can use one widget for both.
Here's the code. I define my union value as an enum, the @UnionValue macro. And each case wraps a different entity type — one for landmark collections, and one for photo albums.
The macro generates everything the system needs — type information, case metadata, and picker support.
I also configure how each option appears in the picker.
typeDisplayRepresentation is the label for the overall type and caseDisplayRepresentations maps each case to the name shown in the picker.
And this isn't limited to Widgets — @UnionValue parameters work everywhere your intent does, including the Shortcuts app.
To learn more, check out the travel tracking sample code project and its corresponding article.
Everything we've covered so far makes your entities and parameters more expressive and efficient. Now, let's talk about execution.
When your intent runs — from Siri, Shortcuts, or any system surface — it only has 30 seconds to finish. That works for most everyday actions. But not every intent is that quick. Now that my app supports tagging and organizing photos, I wanted to let people share their travel photos — uploading them to a shared album without opening the app. So I created an upload intent and added a button to my widget to trigger it. But with large photos, the upload takes time — and the intent kept failing because it couldn't finish within the 30-second limit. LongRunningIntent fixes this. It lets your intent run beyond the 30-second limit — and manages the background task lifecycle of your app. And as your intent runs, progress updates appear automatically as a Live Activity. Now, lets check out the code.
Here's the intent I wrote to upload my photos. I'm conforming to LongRunningIntent.
I take a photo file as input.
Then I wrap my work in performBackgroundTask for extended execution.
LongRunningIntent requires the intent to report progress, so the system knows it's still working and hasn't stalled. And because it builds on ProgressReportingIntent, I get a built-in progress object to track my work.
I calculate the number of chunks for the file and set the total count, then upload each chunk and update the progress as I go.
Here's what happens when my intent runs. It can run longer and there's a stop button right on the Live Activity, so the person can cancel it at anytime.
Though, It'd be great if my intent got a heads-up before being stopped.
CancellableIntent lets your intent clean up gracefully when cancelled — whether the person tapped cancel, the system timed out or needed to reclaim resources.
Here's how I can add cancellation support.
I add CancellableIntent and implement the onCancel handler. When cancellation reason happens, handler gives me the reason, and I can use it to cleanup partial uploads or cancel in-flight requests.
LongRunningIntent also supports background GPU access on supported devices — for tasks like photo processing or on-device inference. Just make sure to add GPU access to your app's entitlement. To learn more about the mechanics of running tasks in the background, check out this video from WWDC25. So far we've covered how long your intent runs and what happens when it stops. Let's talk about which process runs it.
As your app grows, you may move some intents into a Widget extension, or an App Intents extension.
Lightweight, separate processes that can handle requests without launching your app.
You may also create a shared Swift package or framework where your intents and entities live, and import it into your app and extensions. In fact, that's exactly what I did with the travel tracking app — all my intents live in a shared package, imported by both the main app and the widget extension.
When your intents, entities, and queries live in a shared package like this — linked by your app and extensions — the system has to decide, which process runs each intent when a request comes in. It picks a target based on heuristics like if the app is already running, it prefers the app. and if not, it launches the extension. But sometimes that's not the right choice.
For example, I wanted to add a favorite button to my widget so people can mark a photo as favorite right from the Home Screen.
My widget shares the data model with the app — but having two processes write to the same data store can cause conflicts.
So I gave the widget read-only access and the main app handles all the writes. When someone taps that button, the intent needs to run in the main app. ExecutionTargets lets you tell the system exactly which process should run your intent. Here's how.
You can target the main app, an appIntentsExtension, a widgetKitExtension, or any combination. With ExecutionTargets, you override the system's heuristics and control exactly which process handles your intent. That wraps up the new features I wanted to share.
As next steps: add ValueRepresentation to your entities so they can carry structured data across apps. Register relevant content with the system — so it gets surfaced at the right moment.
Adopt EntityCollection to make your intents faster when working with large numbers of entities. And add LongRunningIntent to any intent that needs more than 30 seconds to finish.
To build your app's Siri experience step by step, check out "Code-along: Make your app available to Siri". And to test your intents with the new AppIntentsTesting framework, check out "Validate your App Intents adoption with AppIntentsTesting".
I can't wait to see what you build and thanks for watching!
-
-
0:01 - Share structured entities with ValueRepresentation
struct LandmarkEntity: AppEntity, Transferable { var id: Int var landmark: Landmark // contains CLLocationCoordinate2D static var transferRepresentation: some TransferRepresentation { ValueRepresentation( exporting: { entity in PlaceDescriptor( representations: [.coordinate(entity.landmark.locationCoordinate)], commonName: entity.landmark.name ) } ) } } // If the entity already has a PlaceDescriptor property, use a key-path — much less code: struct LandmarkEntity: AppEntity, Transferable { var id: Int @Property var placeDescriptor: PlaceDescriptor static var transferRepresentation: some TransferRepresentation { ValueRepresentation(exporting: \.placeDescriptor) } } -
5:18 - Register relevant entities with RelevantEntities
// Suggest playlists for the workout session let playlistEntities = [dailyRun, runningMix] let workoutContext = AppEntityContext.audio(.workout(activityType: .running)) try await RelevantEntities.shared.updateEntities( playlistEntities, for: workoutContext ) // Clear all entities for a context try await RelevantEntities.shared.removeAllEntities(for: workoutContext) // Remove specific entities from a context try await RelevantEntities.shared.removeEntities(playlistEntities, from: workoutContext) // Or remove all entities across all contexts try await RelevantEntities.shared.removeAllEntities() -
7:15 - Handle large entity sets with EntityCollection
struct TagPhotosIntent: AppIntent { static let title: LocalizedStringResource = "Tag Travel Photos" @Parameter var photos: EntityCollection<PhotoEntity> // was: [PhotoEntity] @Parameter var tag: String func perform() async throws -> some IntentResult { modelData.tagPhotos(ids: photos.identifiers, tag: tag) // was: tagPhotos(photos, tag: tag) return .result() } } -
10:14 - Make entity IDs stable with SyncableEntity
// If your ID is already stable across devices (server UUID, CloudKit record ID): struct PhotoEntity: AppEntity, SyncableEntity { var id: Int // Already stable across devices — that's it } // If you use local IDs, pair a local and a stable ID: struct PhotoEntity: AppEntity, SyncableEntity { var id: SyncableEntityIdentifier<String, String> init(localID: String, stableID: String) { self.id = SyncableEntityIdentifier(local: localID, stable: stableID) } } -
11:58 - Accept multiple types with @UnionValue
@UnionValue enum TravelGalleryContent { case landmarkCollection(LandmarkCollectionEntity) case photoAlbum(PhotoAlbumEntity) static let typeDisplayRepresentation: TypeDisplayRepresentation = "Travel Gallery" static let caseDisplayRepresentations: [Cases: DisplayRepresentation] = [ .landmarkCollection: "Landmark Collection", .photoAlbum: "Photo Album" ] } -
13:41 - Run beyond 30 s with LongRunningIntent + CancellableIntent
struct UploadPhotoIntent: LongRunningIntent, CancellableIntent { static let title: LocalizedStringResource = "Upload Photo" @Parameter var photo: IntentFile func perform() async throws -> some IntentResult & ProvidesDialog { let result = try await performBackgroundTask { let chunks = calculateChunks(for: photo) progress.totalUnitCount = Int64(chunks) for chunk in 1...chunks { try Task.checkCancellation() try await uploadChunk(chunk) progress.completedUnitCount = Int64(chunk) } return "Upload complete!" } onCancel: { reason in cleanup(for: reason) } return .result(dialog: "\(result)") } } -
16:54 - Control which process runs your intent with ExecutionTargets
// Write operation — needs the main app struct UpdateFavoriteIntent: AppIntent { static var allowedExecutionTargets: ExecutionTargets { .main } } // Standalone download — runs in the extension struct DownloadPhotoIntent: AppIntent { static var allowedExecutionTargets: ExecutionTargets { .appIntentsExtension } } // Display-only — runs in the widget extension struct GetLandmarkStatusIntent: AppIntent { static var allowedExecutionTargets: ExecutionTargets { .widgetKitExtension } } // Works in either — lets the system choose struct TagPhotosIntent: AppIntent { static var allowedExecutionTargets: ExecutionTargets { [.main, .appIntentsExtension] } }
-
-
- 0:00 - Introduction
The 2027 App Intents updates — more control, flexibility, and a smoother developer experience across Siri, Shortcuts, Spotlight, Widgets, and Apple Intelligence. Three areas: entity enhancements, richer parameters, and intent execution, built on the Landmarks Travel Tracking sample.
- 2:40 - Share entities across apps with ValueRepresentation
Beyond Transferable's File and Data representations, the new ValueRepresentation shares structured types the system understands, for example exporting a landmark as a PlaceDescriptor (GeoToolbox) so it flows to Maps for directions. Use a key-path if the entity already has the property.
- 3:45 - Register relevant entities with RelevantEntities
Spotlight indexing and interaction donation can't surface never-seen, never-used content. RelevantEntities lets you suggest entities with a context (such as running playlists when a workout starts) via updateEntities, and remove them by context, by entity, or entirely.
- 7:05 - Handle entities efficiently with EntityCollection
Resolving every entity before an intent runs is costly at scale (tagging thousands of photos). EntityCollection passes just identifiers to perform() without full resolution, a one-line parameter-type change that made tagging 1000 photos nearly instant.
- 8:55 - Use entities across devices with SyncableEntity
Siri conversations now continue across devices, but local IDs differ per device. SyncableEntity declares a stable ID (server UUID or CloudKit record ID); when you only have local IDs, SyncableEntityIdentifier pairs a local and a stable ID so on-device code uses local and the system uses stable.
- 11:01 - Richer parameter types
Declaring a @Parameter gives a native picker, Siri understanding, and localization for free, now extended to more native types like Duration (no custom time pickers) and PersonNameComponents, working across Siri, Shortcuts, and Widgets.
- 12:38 - Union value parameters
A @UnionValue enum lets one parameter accept multiple types, for example a single widget showing photos from either a landmark collection or a photo album. The macro generates type info, case metadata, and picker support (typeDisplayRepresentation, caseDisplayRepresentations), and works everywhere including Shortcuts.
- 13:26 - Extend execution with LongRunningIntent
Intents normally have 30 seconds; LongRunningIntent runs beyond it, manages the background task lifecycle, and shows progress as a Live Activity. Wrap work in performBackgroundTask and report progress (it builds on ProgressReportingIntent). Add CancellableIntent's onCancel to clean up gracefully; it also supports background GPU access.
- 15:27 - Target the right process with ExecutionTargets
When intents live in a shared package linked by the app and extensions, the system picks a process by heuristics, not always right (for example a widget favorite button needs the writing main app). ExecutionTargets overrides this to target the main app, an App Intents extension, a WidgetKit extension, or any combination.
- 17:14 - Next steps
Add ValueRepresentation to carry structured data, register relevant content, adopt EntityCollection for large entity sets, and add LongRunningIntent for work over 30 seconds. See "Code-along: Make your app available to Siri" and "Validate your App Intents adoption with AppIntentsTesting."