Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

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 ↗

Transcript all transcripts

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 swift · at 0:01 ↗
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)
      }
  }
Register relevant entities with RelevantEntities swift · at 5:18 ↗
// 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 swift · at 7:15 ↗
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()
      }
  }
Make entity IDs stable with SyncableEntity swift · at 10:14 ↗
// 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 swift · at 11:58 ↗
@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 swift · at 13:41 ↗
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)")
      }
  }
Control which process runs your intent with ExecutionTargets swift · at 16:54 ↗
// 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] }
  }

Resources