iOS 27 brought a major update to the App Intents framework in WWDC26's Session 345: new APIs like LongRunningIntent, CancellableIntent, EntityCollection, and SyncableEntity now make Siri integrations both more resilient and consistent across devices. In this guide I walk through iOS 27 App Intents LongRunningIntent SyncableEntity Siri integration, drawing on WWDC26 Session 345 and Apple's official API documentation, showing what actually changed as of the beta period (June 2026).
💡 Pro Tip: When migrating an existingAppIntenttoLongRunningIntent, first wrap yourperform()body in aperformBackgroundTaskblock — testing that refactor on its own before adding the protocol makes it much easier to tell which error came from which change.
Table of Contents
- Why App Intents matters again on iOS 27
- Breaking the 30-second wall with LongRunningIntent
- Writing progress to Live Activity: using the progress object correctly
- CancellableIntent: the natural partner of iOS 26.4, not iOS 27
- Scaling large data sets with EntityCollection
- SyncableEntity and cross-device Siri conversations
- Contextual suggestions with RelevantEntities
- ExecutionTargets: which operation runs in which process
- The order to follow when migrating existing AppIntents to iOS 27
- Table: Which API solves which problem
- Table: Migration prioritization checklist
- FAQ
- How do you write an intent that runs longer than 30 seconds on iOS 27?
- What is SyncableEntity, and why can't Siri find the entity across devices?
- How do you process thousands of records quickly with EntityCollection?
- How do I make my app visible in iOS 27 Siri?
- Is using CancellableIntent mandatory, or is LongRunningIntent alone enough?
- Can I build a general "suggested content" list with RelevantEntities?
- Update (September 2026)
- Conclusion
- Sources
Why App Intents matters again on iOS 27
The core promise of App Intents hasn't changed: making your app's content and actions discoverable for system experiences like Apple Intelligence, Siri, Spotlight, Shortcuts, and widgets. WWDC26 Session 345 covers the additions from what it calls "our 2027 releases" under eight headings; the documentation lists the same APIs under the "iOS 27.0" tag. I'm focusing on three main axes: long-running work finishing without interruption, no performance loss on large data sets, and entities staying consistent across devices — mapped to LongRunningIntent, EntityCollection, and SyncableEntity respectively; together with CancellableIntent and RelevantEntities I cover these as five core APIs, and the final section is devoted to ExecutionTargets and @UnionValue.
If you haven't touched App Intents fundamentals yet, I'd recommend reading App Intents and Shortcuts integration first — this guide doesn't repeat those fundamentals, it focuses directly on the iOS 27 delta.
Breaking the 30-second wall with LongRunningIntent
According to Apple's official documentation, LongRunningIntent is "an interface you use to extend the background execution time of an app intent that performs a long-running task." The underlying problem: the system traditionally gives a normal intent only up to 30 seconds of run time ("the system traditionally gives it up to 30 seconds to finish its task"). Work like batch photo processing, large file transfers, or a multi-step automation may not finish in that window — in the session, Apple's team describes it exactly as "the intent kept failing because it couldn't finish within the 30-second limit."
On the Swift side, LongRunningIntent is a protocol that extends ProgressReportingIntent (protocol LongRunningIntent : ProgressReportingIntent). ProgressReportingIntent isn't actually a new API — it's been around since iOS 17.0; iOS 27's contribution is combining that progress-reporting infrastructure with LongRunningIntent, which extends background run time.
1import AppIntents2 3struct SyncPhotoLibraryIntent: LongRunningIntent {4 static var title: LocalizedStringResource = "Fotoğrafları Senkronize Et"5 6 func perform() async throws -> some IntentResult & ReturnsValue<String> {7 let batches = loadBatches()8 9 let summary = try await performBackgroundTask {10 progress.totalUnitCount = Int64(batches.count)11 for (index, batch) in batches.enumerated() {12 try Task.checkCancellation()13 try await syncBatch(batch)14 progress.completedUnitCount = Int64(index + 1)15 }16 return "\(batches.count) grup senkronize edildi"17 }18 19 return .result(value: summary)20 }21 22 private func loadBatches() -> [[String]] { [] }23 private func syncBatch(_ batch: [String]) async throws {}24}While the work is running, the system automatically shows a Live Activity as progress reports come in, and the user can stop the work from that Live Activity; what's new in iOS 27 is that this Live Activity is triggered automatically by App Intents. If you want to see the mechanism itself in more detail, you can check out interactive widgets and Live Activity.
Writing progress to Live Activity: using the progress object correctly
The most practical thing about LongRunningIntent is that you don't need a manual ActivityKit implementation. What makes that possible is the progress object inherited from ProgressReportingIntent: update totalUnitCount and completedUnitCount, and the system reflects those values in the Live Activity automatically.
Watch how often you update progress. Instead of updating after every single record, update at meaningful stage boundaries (e.g. after each batch) — this avoids needless redraws and gives the user a smoother progress experience:
1import AppIntents2 3struct ProcessRecordsIntent: LongRunningIntent {4 static var title: LocalizedStringResource = "Kayıtları İşle"5 6 func perform() async throws -> some IntentResult & ReturnsValue<Int> {7 let items = Array(1...250)8 9 let processedCount = try await performBackgroundTask {10 progress.totalUnitCount = Int64(items.count)11 var processed = 012 13 for start in stride(from: 0, to: items.count, by: 25) {14 let chunk = items[start..<min(start + 25, items.count)]15 try await process(Array(chunk))16 processed += chunk.count17 // Update every 25 records, not every record.18 progress.completedUnitCount = Int64(processed)19 }20 return processed21 }22 23 return .result(value: processedCount)24 }25 26 private func process(_ chunk: [Int]) async throws {}27}When the work is cancelled, the onCancel block I'll cover in the CancellableIntent section takes over.
CancellableIntent: the natural partner of iOS 26.4, not iOS 27
According to Apple's official API metadata, CancellableIntent was introduced across all platforms not with iOS 27, but with iOS 26.4. It resurfaced in WWDC26 Session 345 as the natural complement to LongRunningIntent — a long-running task also needed to become cancellable.
The official definition is: "An interface to support the graceful cancellation of your app intent's task." You use it by wrapping the main body of the work with the withIntentCancellationHandler(operation:onCancel:isolation:) method. The method's onCancel parameter is of type (IntentCancellationReason) -> Void, meaning it hands you the cancellation reason directly:
1import AppIntents2 3struct SyncLibraryIntent: AppIntent, ProgressReportingIntent, CancellableIntent {4 static var title: LocalizedStringResource = "Kütüphaneyi Senkronize Et"5 6 func perform() async throws -> some IntentResult & ProvidesDialog {7 let batchCount = 88 9 return try await withIntentCancellationHandler {10 progress.totalUnitCount = Int64(batchCount)11 for index in 0..<batchCount {12 try await syncBatch(index)13 progress.completedUnitCount = Int64(index + 1)14 }15 return .result(dialog: "\(batchCount) grup senkronize edildi")16 } onCancel: { reason in17 // Clean up based on the reason.18 switch reason {19 case .timeout: cleanupPartialSync(reason: "timeout")20 case .userCancelled: cleanupPartialSync(reason: "user_cancelled")21 default: cleanupPartialSync(reason: "unknown")22 }23 }24 }25 26 private func syncBatch(_ index: Int) async throws {}27 private func cleanupPartialSync(reason: String) {}28}Per the WWDC26 session, cancellation can come from three different sources: "whether the person tapped cancel, the system timed out or needed to reclaim resources." You don't need to keep a separate tracker in your own state to tell the reason apart; as the documentation puts it, "If you want to know the reason for cancellation, use the withIntentCancellationHandler(operation:onCancel:isolation:) method that this protocol offers instead." The IntentCancellationReason value passed to the handler carries that information, and you can distinguish between .timeout and .userCancelled as shown above.
Scaling large data sets with EntityCollection
When you use a normal @Parameter array, the system fully resolves every entity before the intent runs — calling the relevant query and filling in all of its properties. With hundreds or thousands of records, that creates a noticeable delay.
EntityCollection is "an array of entity identifiers that you use to improve the efficiency of operations involving large numbers of entities," per Apple's docs. Its signature is struct EntityCollection<Entity> where Entity : AppEntity, arriving with iOS 27.0. Switch the parameter type to this, and the system only carries the entity identifiers, leaving the full resolve to the intent itself:
1struct BatchTagPhotosIntent: AppIntent {2 static var title: LocalizedStringResource = "Fotoğrafları Etiketle"3 4 @Parameter(title: "Fotoğraflar")5 var photos: EntityCollection<PhotoEntity>6 7 func perform() async throws -> some IntentResult {8 // photos here is a list of identifiers that aren't fully resolved yet;9 // you resolve only the subset you actually need yourself.10 for identifier in photos.identifiers {11 try await tagPhoto(id: identifier, tag: "iOS27")12 }13 return .result()14 }15}In the WWDC26 session, Apple's team showed this with a Shortcut demo that finds and tags 1,000 photos, describing the result with EntityCollection as "almost instant"; no exact millisecond or percentage figure was shared.
SyncableEntity and cross-device Siri conversations
As Apple defines it, SyncableEntity is "an interface that indicates your entity has an identifier that's consistent across devices." Its signature is protocol SyncableEntity : AppEntity, arriving with iOS 27.0. Its purpose is clear: "Siri uses this capability to transfer a conversation from one device to another."
The problem: if your entity ID is generated per-device (say, a local database row identifier), the same photo gets a different ID on iPhone and iPad. When Siri wants to continue on one device a conversation that started on another, it can't resolve that ID, and the conversation breaks. If the identifier is already consistent across devices — say, a server-issued UUID — you can add the protocol, as the docs put it, "without any additional changes." If you keep the local and stable identifiers separate, make the entity's id property type SyncableEntityIdentifier:
1import AppIntents2 3struct PhotoEntity: AppEntity, SyncableEntity {4 // The local ID (CoreData row identifier) and the stable ID (CloudKit5 // record name) are matched into a single identifier.6 var id: SyncableEntityIdentifier<String, String>7 var title: String8 9 init(localID: String, stableID: String, title: String) {10 self.id = SyncableEntityIdentifier(local: localID, stable: stableID)11 self.title = title12 }13 14 static var typeDisplayRepresentation: TypeDisplayRepresentation = "Fotoğraf"15 static var defaultQuery = PhotoQuery()16 17 var displayRepresentation: DisplayRepresentation {18 DisplayRepresentation(title: "\(title)")19 }20}21 22struct PhotoQuery: EntityQuery {23 func entities(24 for ids: [SyncableEntityIdentifier<String, String>]25 ) async throws -> [PhotoEntity] { [] }26}Contextual suggestions with RelevantEntities
The framing of RelevantEntities isn't the same width across the two primary sources, so use it knowing that. The session's definition is broad: "With RelevantEntities, you can suggest entities to the system and provide context about when and why they're relevant." The official doc's summary is narrower: "a type you use to donate your app's songs, albums, artists, and other media items to play during workouts." As of the beta, the only documented context generator on AppEntityContext is audio(_:), meaning you tag your donation with an AudioContext; the session's example ties running playlists to a workout context:
1import AppIntents2 3func donateRunningPlaylists(4 dailyRun: PlaylistEntity,5 runningMix: PlaylistEntity6) async throws {7 let playlistEntities = [dailyRun, runningMix]8 let workoutContext = AppEntityContext.audio(.workout)9 10 try await RelevantEntities.shared.updateEntities(11 playlistEntities,12 for: workoutContext13 )14 15 // Remove specific entries:16 try await RelevantEntities.shared.removeEntities(17 playlistEntities,18 from: workoutContext19 )20 // Or remove all of them:21 try await RelevantEntities.shared.removeAllEntities()22}If you want to narrow the context, there are the workout(activityType:) and workout(intensityLevel:) type methods; the session's example writes .workout(activityType: .running).
The record persists until you call removeAllEntities(), removeAllEntities(for:), removeEntities(_:), or removeEntities(_:from:). For a non-media "contextual suggestion" scenario, the distinction the session suggests is this: use Spotlight for making content searchable, interaction donation for the system learning usage patterns, and RelevantEntities for telling the system which content is relevant in specific situations — these are covered in more detail in the FAQ section below.
ExecutionTargets: which operation runs in which process
When intents live in a package shared by the app and its extensions, the system decides which process runs them using heuristic rules — and that isn't always the right process. Apple's team defines it in the session as: "ExecutionTargets lets you tell the system exactly which process should run your intent." You can choose the main app, an App Intents extension, a WidgetKit extension, or any combination — for example, keeping a data-writing widget button's intent in the main app.
Likewise, @UnionValue also comes from the session: "A Swift enum where each case wraps a different type, letting a single parameter represent one of several options." The macro lets a parameter accept one of several entity types; its use isn't limited to widgets either: "this isn't limited to Widgets — @UnionValue parameters work everywhere your intent does, including the Shortcuts app." As the session puts it, what the macro produces is the type information, case metadata, and picker support the system needs.
1@UnionValue2enum MediaSelection {3 case song(SongEntity)4 case album(AlbumEntity)5 case podcast(PodcastEntity)6 7 static let typeDisplayRepresentation: TypeDisplayRepresentation = "Medya"8 static let caseDisplayRepresentations: [Cases: DisplayRepresentation] = [9 .song: "Şarkı",10 .album: "Albüm",11 .podcast: "Podcast"12 ]13}14 15struct PlayMediaIntent: AppIntent {16 @Parameter(title: "Medya")17 var selection: MediaSelection18 19 func perform() async throws -> some IntentResult {20 switch selection {21 case .song(let song): try await play(song)22 case .album(let album): try await play(album)23 case .podcast(let podcast): try await play(podcast)24 }25 return .result()26 }27}The order to follow when migrating existing AppIntents to iOS 27
The order below is a practical implementation sequence I derived from the API contracts above:
- Measure the duration first. If your
perform()body regularly exceeds 30 seconds, it's aLongRunningIntentcandidate; if it doesn't, you don't need to change anything. Measure on a real device with real data volume — simulator timings can be misleading. - Wrap it in `performBackgroundTask`. Move it into this closure without changing your existing logic, and fill in the
progressfields. Testing this step on its own makes it easier to isolate errors that show up in the next step when you addCancellableIntent. - Add `CancellableIntent` and write a real `onCancel`. Don't leave it empty — whether your app stays consistent when the user stops the task depends on this block. Decide here whether you'll roll back partially processed records or leave them as they are.
- Review your array parameters. Any
@Parameterarray carrying more than a few dozen records is anEntityCollectioncandidate; first clarify whether your intent needs the entity's full property set or just its identifier. - Add `SyncableEntity` to entities used across devices, especially in scenarios where Siri conversations start on one device and finish on another. If your local ID is already stable (say, a server-assigned UUID), you may not need any additional mapping; you only need a
SyncableEntityIdentifiermapping if you have device-specific IDs. - Evaluate `RelevantEntities` within its documented context. If you're after a general "suggested content" experience, also put Spotlight indexing and the
IntentDonationManagerdonation flow on the table.
Table: Which API solves which problem
API | Problem it solves | iOS version |
|---|---|---|
LongRunningIntent | 30-second background run-time limit | iOS 27.0 |
CancellableIntent | Graceful cancellation and cleanup | iOS 26.4 |
EntityCollection | Full-resolve cost on large parameter arrays | iOS 27.0 |
SyncableEntity | Unstable entity ID across devices | iOS 27.0 |
RelevantEntities | Context-based content suggestions | iOS 27.0 |
ExecutionTargets | Which process the intent runs in | iOS 27.0 |
@UnionValue | Multiple entity types in a single parameter | iOS 27.0 |
Table: Migration prioritization checklist
Symptom | Recommended API | Priority |
|---|---|---|
Intent frequently hits the 30-second timeout | LongRunningIntent + CancellableIntent | High |
Parameter array carries hundreds/thousands of records | EntityCollection | High |
Same content gives a different Siri result on different devices | SyncableEntity | Medium |
Missing content suggestions in a workout/audio context | RelevantEntities | Medium |
Unclear or needlessly heavy process placement for the intent | ExecutionTargets | Low |
GOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
Reader Reward
I've put together a practical checklist to go through before you start your iOS 27 App Intents migration. This list summarizes the order in which to evaluate the five APIs covered in the sections above, and the steps most often skipped during migration; you can go through it in order before writing a new intent or migrating an existing one.
FAQ
How do you write an intent that runs longer than 30 seconds on iOS 27?
Conform your intent to LongRunningIntent, which also builds on ProgressReportingIntent. Put your work inside a performBackgroundTask block and update progress at regular intervals; per the docs, if you don't update it regularly, the system may cancel the background extension and end your task early. While it runs, an automatic Live Activity appears with a stop option.
What is SyncableEntity, and why can't Siri find the entity across devices?
By default, your entity ID might be generated per device (say, a local database row identifier); the same content gets a different ID on iPhone and iPad. When Siri tries to continue on another device a conversation that started on one, it can't resolve that ID. Add SyncableEntity and provide a stable identifier (server-assigned, or a CloudKit record ID) — making the id property's type SyncableEntityIdentifier if you keep local and stable identifiers separate — and Siri recognizes the entity the same way on every device.
How do you process thousands of records quickly with EntityCollection?
With a normal array parameter, the system fully resolves every entity before the intent runs; with thousands of records this creates a serious delay. Switch the type to EntityCollection and the system only passes identifiers, skipping the full resolve. In Apple's own demo with 1,000 photos, this produced an "almost instant" result; no exact millisecond figure was shared.
How do I make my app visible in iOS 27 Siri?
The distinction the session draws points to three complementary paths. First, indexing your content in Spotlight — that's how Siri finds and surfaces your content. Second, donating user actions with IntentDonationManager; the system learns patterns and suggests similar actions. Third, for content that's never been searched or played, giving the "this content is relevant in this situation" hint with RelevantEntities.
Is using CancellableIntent mandatory, or is LongRunningIntent alone enough?
Not mandatory, but strongly recommended. LongRunningIntent only extends run time; without CancellableIntent, your app gets no chance to clean up when the user stops the work, and can end up half-finished. In the WWDC26 session, the two protocols are presented as a natural pair.
Can I build a general "suggested content" list with RelevantEntities?
The two sources speak at different widths, so be careful. The session describes the API broadly ("suggest entities to the system and provide context about when and why they're relevant"), while the official doc's summary talks about media content like songs, albums, artists, playlists, and podcasts. For a general, non-media suggestion experience, factor in Spotlight indexing and IntentDonationManager donation too.
Update (September 2026)
This guide was written during the beta period. Apple released iOS 27.0 (24A437) and Xcode 27 (27A266a) on September 14, 2026; the APIs above are no longer in beta, they're in general availability. As of September 21, 2026, iOS 27.2 beta 2 (24B5089g) is in distribution.
Conclusion
iOS 27's App Intents update isn't a single big feature, it's a set of five complementary APIs: LongRunningIntent and CancellableIntent make long-running work reliable, EntityCollection solves scale, SyncableEntity provides cross-device consistency, and RelevantEntities handles context-based content suggestions. If you have an existing App Intents integration, it's enough to first identify your duration and scale issues and then follow the migration order above.
If you're not yet familiar with the core App Intents concepts, start with App Intents and Shortcuts integration. To see the Live Activity mechanism in depth, interactive widgets and Live Activity will be useful. If you're curious about Siri's conversation-based side, you can check out voice integration with SiriKit. To see what's broken on the SwiftUI side of iOS 27, you can read State macro and ContentBuilder breaks on iOS 27, and for a general developer perspective, the iOS 27 and Xcode 27 developer guide.
Sources
- WWDC26 Session 345 — Discover new capabilities in the App Intents framework — the official introduction of LongRunningIntent, CancellableIntent, EntityCollection, SyncableEntity, RelevantEntities, ExecutionTargets, and @UnionValue.
- LongRunningIntent — Apple Developer Documentation — the official definition of the protocol that extends the 30-second background run-time limit.
- CancellableIntent — Apple Developer Documentation — graceful cancellation support and using withIntentCancellationHandler(operation:onCancel:isolation:).
- EntityCollection — Apple Developer Documentation — the type that reduces the full-resolve cost for large entity sets.
- SyncableEntity — Apple Developer Documentation — the cross-device stable entity identifier interface and the SyncableEntityIdentifier example.
- RelevantEntities — Apple Developer Documentation — contextual suggestion donation and the updateEntities(_:for:) API.
- ProgressReportingIntent — Apple Developer Documentation — the progress-reporting infrastructure that's existed since iOS 17.
- Releases — Apple Developer — release dates for iOS 27.0 (24A437), Xcode 27 (27A266a), and iOS 27.2 beta 2 (24B5089g).
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
We respect your privacy. You can unsubscribe at any time.

