2026 App ServicesAI & Machine Learning
WWDC26 · 18 min · App Services / AI & Machine Learning
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.
Watch at developer.apple.com ↗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
Code shown on screen · 7 snippets
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
var placeDescriptor: PlaceDescriptor
static var transferRepresentation: some TransferRepresentation {
ValueRepresentation(exporting: \.placeDescriptor)
}
} 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() Handle large entity sets with EntityCollection
struct TagPhotosIntent: AppIntent {
static let title: LocalizedStringResource = "Tag Travel Photos"
var photos: EntityCollection<PhotoEntity> // was: [PhotoEntity]
var tag: String
func perform() async throws -> some IntentResult {
modelData.tagPhotos(ids: photos.identifiers, tag: tag) // was: tagPhotos(photos, tag: tag)
return .result()
}
} 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)
}
} Accept multiple types with @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"
]
} Run beyond 30 s with LongRunningIntent + CancellableIntent
struct UploadPhotoIntent: LongRunningIntent, CancellableIntent {
static let title: LocalizedStringResource = "Upload Photo"
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)")
}
} 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] }
}