App Intents in iOS 27: Background, Sync, Spotlight
App Intents shipped in iOS 16 as Apple’s typed, structured-action API for Shortcuts, Siri, and Spotlight; iOS 17 extended it into App Intents-driven widgets; iOS 18 made it the contract for Apple Intelligence’s action surface; iOS 26 pushed it into Visual Intelligence and interactive snippets. iOS 27 changes the shape of the bet again, and the change is mechanical rather than cosmetic: an intent can now run past the 30-second background limit, an entity can carry an identity that survives the trip across a user’s devices, and a query can repair its own Spotlight index when the system asks. iOS 27 adds capability, not sugar.1
Every prior release widened who could call your intents. iOS 27 widens what your intents can do once called. A sync intent that operates on a few thousand records used to race a 30-second timer and lose; now it asks the system for more runway and reports progress while it works. An entity that meant one thing on iPhone and something else on Mac now resolves to the same object on both. The post walks the iOS 27 surface against Apple’s documentation, with the same frame as the rest of the cluster: what an app that already ships App Intents adds to gain each new capability.
TL;DR
LongRunningIntentextends an intent’s background runtime past the system’s 30-second limit. You wrap the work inperformBackgroundTask(options:operation:)and passLongRunningTaskOptions; the protocol refinesProgressReportingIntent, so progress reporting is a requirement, not an option. Live Activities renders that progress automatically.234SyncableEntitydeclares that anAppEntitycarries an identifier consistent across a user’s devices, which lets the system refer to the same object on iPhone, Mac, and Watch (Siri uses it to hand a conversation from one device to another).5IndexedEntityQueryadds Spotlight reindexing support to anEntityQuery, so when the system flags a problem with your app’s index it can ask your query to re-donate the affected entities.6AppUnionValueandAppUnionValueCasesProviding(generated by the@UnionValuemacro) let one parameter accept several distinct entity types with proper picker UI and parameter summaries.78OwnershipProvidingEntity,EntityOwnership, andEntityCollectioncover ownership-aware confirmation and bulk efficiency;RunSystemShortcutIntentandIntentExecutionTargetscover widget-launched system actions and which process runs an intent.910111213
The 30-Second Wall: LongRunningIntent
The background execution limit has been the quiet ceiling on what an App Intent could do. When the system performs an intent in the background (the user asks Siri to sync, then locks the phone and pockets it), it traditionally grants roughly 30 seconds to finish.2 For logging a glass of water that is generous. For synchronizing a library, running on-device inference, or processing a large file, 30 seconds is a guillotine: the system kills the task mid-write and the user gets a half-finished result.
iOS 27 introduces LongRunningIntent, a protocol an intent adopts to ask the system for an extended background window.2 Apple names the use cases directly in the documentation: file operations, data synchronization, machine learning inference, and data processing over a large enough dataset. The declaration tells you the most important constraint before you write a line:
protocol LongRunningIntent : ProgressReportingIntent
LongRunningIntent refines ProgressReportingIntent.2 You cannot adopt the long-running protocol without also reporting progress, by design. The extended runtime is a privilege the system grants conditionally, and the condition is that you keep telling it how far along you are. Stop reporting and the system can revoke the extension and end the task early.3
The work goes inside performBackgroundTask(options:operation:):
@discardableResult
func performBackgroundTask<T>(
options: LongRunningTaskOptions = [],
operation: @escaping () async throws -> T
) async throws -> T
You call the method from your intent’s perform() body and put the expensive code in the operation closure. The method automatically extends your runtime past the standard 30-second limit on platforms that impose it; you do not start a separate background task or manage a UIBackgroundTaskIdentifier yourself.3 A library-sync intent looks like this:
import AppIntents
struct SyncLibraryIntent: LongRunningIntent {
static var title: LocalizedStringResource = "Sync Library"
func perform() async throws -> some IntentResult {
try await performBackgroundTask(options: []) {
let records = try await server.fetchPendingRecords()
for (offset, record) in records.enumerated() {
try await store.apply(record)
progress.completedUnitCount = Int64(offset + 1)
progress.totalUnitCount = Int64(records.count)
}
return ()
}
return .result()
}
}
Two things earn explanation, because tutorials skip them.
The progress property is the contract, not telemetry. Apple is explicit: while your operation runs, update the Progress from the ProgressReportingIntent conformance regularly, and if you don’t, the system can cancel the runtime extension and end your task prematurely.3 Progress reporting on a normal intent is a nicety. On a LongRunningIntent it is the heartbeat that keeps the extension alive.
LongRunningTaskOptions declares resource requirements. The options value (an OptionSet-style struct that defaults to []) tells the system about additional resource needs for the task, which it factors into the runtime it grants.4 An empty set is the common case. You reach for explicit options when the work needs more than the default profile.
The payoff beyond surviving past 30 seconds: Live Activities renders the progress for free. The documentation states that Live Activities displays the progress of the intent’s task using information it receives automatically from performBackgroundTask, drawing the title and subtitle and a progress bar from the values your code reports.3 A long sync started by voice shows up on the Lock Screen as a live progress bar without you building a single Live Activity view for it. The intent reports, the system renders.
Consistent Identity: SyncableEntity
An AppEntity has an id. On a single device that identifier only has to be unique within the app. The trouble starts the moment a user owns more than one device, which in Apple’s ecosystem is the default. The “Project Atlas” the user discussed with Siri on their iPhone has to be recognizably the same “Project Atlas” when they pick the conversation back up on the Mac. If the iPhone’s local identifier differs from the Mac’s, the system has two unrelated objects and no way to connect them.
SyncableEntity is iOS 27’s answer:5
protocol SyncableEntity : AppEntity
Adopting it declares that your entity’s identifier is the same across devices. The presence of the protocol tells the system it can refer to your entity consistently from one device to another. Apple gives the concrete payoff: Siri uses the capability to transfer a conversation from one device to another.5
The adoption cost depends entirely on where your identifiers come from. If your entities already use a stable cross-device identifier (a server-issued UUID, an iCloud record name), you adopt SyncableEntity with no other changes, because the value you already store is the value the system needs.5
import AppIntents
struct ProjectEntity: SyncableEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Project"
static var defaultQuery = ProjectQuery()
// A UUID issued by the backend and identical on every device.
var id: UUID
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
@Property(title: "Name") var name: String
}
The trap is the app that mints a fresh local identifier on each device (an autoincrement row ID, a per-install UUID). Those identifiers are unique locally and meaningless across devices. Apple’s guidance for that case: adopt the protocol and key your identity off the value that is actually stable, so the system has something durable to anchor on.5 Sync was the hard problem teams kept solving by hand with their own iCloud plumbing. SyncableEntity moves the cross-device-identity declaration into the framework where Siri and the rest of the system can act on it.
Self-Healing Search: IndexedEntityQuery
iOS 16 let you donate IndexedEntity instances to Spotlight so individual entities became searchable. The gap was repair. Indexes drift, get corrupted, or fall behind after a migration, and until iOS 27 the system’s only recourse was to lean on your app’s CSSearchableIndex delegate or its CSImportExtension.
IndexedEntityQuery closes the gap by letting the system ask your query to reindex:6
protocol IndexedEntityQuery : EntityQuery where Self.Entity : IndexedEntity
The where clause is the precondition: the query’s entity has to conform to IndexedEntity, because reindexing only makes sense for entities you donate to Spotlight in the first place.6 When the system hits a problem with an app’s index, it calls this protocol’s methods if your query type adopts it; if your query doesn’t, Spotlight keeps asking your CSSearchableIndex object (or your CSImportExtension, if you donated by associating the entity with that type) to do the work instead.6 You implement the methods to fetch the requested entities and donate them again through your preferred searchable index.
import AppIntents
import CoreSpotlight
struct PhotoQuery: IndexedEntityQuery {
func entities(for identifiers: [Photo.ID]) async throws -> [Photo] {
try await library.photos(matching: identifiers)
}
func suggestedEntities() async throws -> [Photo] {
try await library.recentPhotos(limit: 20)
}
// Called by the system during reindexing. Fetch the requested
// entities and donate them again to Spotlight.
func entities(matching string: String) async throws -> [Photo] {
try await library.photos(matchingText: string)
}
}
The value is operational. An app that gets IndexedEntityQuery right participates in Spotlight’s recovery loop: the system notices the index is wrong and the app supplies fresh data on demand, instead of the user quietly losing search results until the next full re-donation. The cluster’s foundational App Intents post covered bare IndexedEntity exposure for making entries searchable; IndexedEntityQuery is the maintenance layer on top.
One Parameter, Several Types: AppUnionValue
Plenty of real intents take a parameter that is legitimately one of several types. “Share this” where “this” is a photo, a document, or a link. The pre-iOS-27 workarounds were ugly: separate intents per type, or a string discriminator plus optional parameters that the picker UI could not render cleanly.
iOS 27 adds AppUnionValue for typed union parameters:7
protocol AppUnionValue : TypeDisplayRepresentable
A union value conforming to the protocol works as a Shortcuts parameter with rich metadata, so the system can present an appropriate picker and a sensible parameter summary across the member types.7 You don’t write the conformance by hand. The @UnionValue macro generates it, and the same macro generates a nested Cases enum that conforms to AppUnionValueCasesProviding:78
protocol AppUnionValueCasesProviding : AppEnum
AppUnionValueCasesProviding is conformed to automatically by the Cases enum the macro emits.8 It bridges the cases enum back to the union value type and inherits metadata through its AppEnum conformance, which is what gives each case its display name in the picker.8 In practice you write the union and annotate it:
import AppIntents
@UnionValue
enum ShareTarget {
case photo(PhotoEntity)
case document(DocumentEntity)
case link(URL)
}
struct ShareIntent: AppIntent {
static var title: LocalizedStringResource = "Share Item"
@Parameter(title: "Item")
var target: ShareTarget
func perform() async throws -> some IntentResult {
// Switch over the concrete case and act accordingly.
return .result()
}
}
The @UnionValue macro handles the AppUnionValue and AppUnionValueCasesProviding conformances; if you want custom metadata beyond the defaults, you implement the protocol requirements in an extension.7 One parameter, three valid types, a picker that knows how to show all three.
Ownership And Efficiency
Two distinct iOS 27 concerns share this section because both protect the system from acting carelessly on your data: ownership-aware confirmation, and bulk-operation efficiency.
Confirming Destructive Actions: OwnershipProvidingEntity
When your app passes entities into intents and returns them from results, Apple Intelligence, Siri, and custom shortcuts can act on those entities across apps. For destructive or sensitive actions (deleting an entity, updating a shared one), you want a confirmation that carries the right context. OwnershipProvidingEntity supplies it:9
protocol OwnershipProvidingEntity : AppEntity
Conform your entity to it and the system prompts for confirmation, with appropriate context in the dialog, when an intent acts on shared or publicly accessible entities.9 The ownership state itself is an EntityOwnership value, a flag-based struct where you specify a single state or combine several with an OptionSet:10
import AppIntents
struct AlbumEntity: OwnershipProvidingEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Album"
static var defaultQuery = AlbumQuery()
var id: UUID
var isSharedWithFamily: Bool
var isPublished: Bool
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
@Property(title: "Name") var name: String
// Reflect how the user has shared this album so the system can
// calibrate its confirmation dialog.
var ownership: EntityOwnership {
var state: EntityOwnership = []
if isSharedWithFamily || isPublished {
state = .shared
}
return state
}
}
The mechanism matters most for apps with shared content: a photo album you published or shared with family should produce a more cautious confirmation than a private one, and OwnershipProvidingEntity is how the entity tells the system which is which.9
Bulk Without the Memory Hit: EntityCollection
Resolving entities is not free. When an intent takes hundreds of entities as a parameter, forcing the system to resolve every identifier into a full instance during parameter resolution can cost meaningful time and memory at a bad moment. EntityCollection is the fix:11
struct EntityCollection<Entity> where Entity : AppEntity
The collection stores only the identifier for each entity initially and offers an option to fetch the full instances later if you need them.11 Use it as a variable type when you hold many identifiers, and use it as a parameter type when an intent operates over a large set:
import AppIntents
struct DisableNotificationsIntent: AppIntent {
static var title: LocalizedStringResource = "Disable Notifications"
// Hundreds of conversations resolve lazily, not all at once.
@Parameter(title: "Conversations")
var conversations: EntityCollection<ConversationEntity>
func perform() async throws -> some IntentResult {
return .result()
}
}
For a parameter holding hundreds of entities, skipping per-identifier resolution saves time and memory exactly when the user is waiting on the action to start.11
Where The Intent Runs: RunSystemShortcutIntent And IntentExecutionTargets
Two smaller iOS 27 additions round out the surface. RunSystemShortcutIntent is a widget-only intent for launching another app or running an App Shortcut, custom shortcut, or system action from a widget button:12
struct RunSystemShortcutIntent
You use it only to initialize a Button with the system-shortcut initializer and place that button in a widget; it does nothing useful outside that context.12 When the user configures the widget they choose the button’s action, and the intent supplies the metadata the system needs for the configuration UI. It does not hand your widget access to a shortcut’s actions, parameters, or implementation. If the chosen shortcut needs to prompt for input, the system may open the Shortcuts app to perform it.12
IntentExecutionTargets answers a question that surfaces once you share intents and entities across your app, widget extension, and App Intents extension through a Swift package or framework: which process runs the intent?13
struct IntentExecutionTargets
By default the system performs an intent or entity query using any available target.13 You use IntentExecutionTargets to constrain that. Apple’s example is a browser: adding a bookmark can happen while the app isn’t visible, so the App Intents extension is fine, but opening a new tab only makes sense when the app is visible, which requires the app’s own process.13 You declare the valid targets and the system honors the constraint.
Adoption Path
An app that already ships App Intents adds iOS 27 capabilities incrementally; none of them rewrites the core model.
- Find your slowest intent. Any intent doing file I/O, sync, on-device inference, or large data processing is a
LongRunningIntentcandidate. Adopt the protocol, move the work intoperformBackgroundTask(options:operation:), and reportprogressthroughout. You get past-30-second runtime and free Live Activities progress.23 - Audit your entity identifiers. If they’re already stable across devices (server UUID, iCloud record name), conform the relevant entities to
SyncableEntityand ship. If they’re per-device, fix the identity first, then conform.5 - Add
IndexedEntityQueryto queries whose entities areIndexedEntity. It is purely additive: the methods only get called when the system needs a reindex, and your search results stay correct through index drift.6 - Collapse multi-type parameters with
@UnionValue. Anywhere you faked a union with separate intents or a discriminator string, the macro gives you one clean parameter.7 - Mark shared entities with
OwnershipProvidingEntity, and switch large-set parameters toEntityCollection. The first improves confirmation safety, the second improves resolution performance.911
FAQ
How long can a LongRunningIntent run in the background?
Apple documents the floor it lifts, not a fixed ceiling. The system traditionally gives a background task roughly 30 seconds to finish, and LongRunningIntent (via performBackgroundTask(options:operation:)) automatically extends that window past the standard limit on platforms that impose it.23 The extension is conditional: you have to keep updating the Progress from the ProgressReportingIntent conformance, and if you stop, the system can cancel the extension and end your task early.3 Treat progress reporting as the price of the extra runtime.
Do I have to report progress to use LongRunningIntent?
Yes. LongRunningIntent is declared as protocol LongRunningIntent : ProgressReportingIntent, so adopting it requires the ProgressReportingIntent conformance and its Progress.2 Beyond satisfying the compiler, regular progress updates keep the background runtime extension alive and feed the title, subtitle, and progress bar that Live Activities renders automatically from performBackgroundTask.3
What does SyncableEntity actually change at runtime?
It declares that your entity’s identifier is identical across a user’s devices, which lets the system treat the object as one entity everywhere instead of separate per-device objects.5 The concrete capability Apple names: Siri can transfer a conversation about that entity from one device to another. If your identifiers are already cross-device-stable you adopt the protocol with no other changes; if they are per-device, you re-anchor identity on a stable value first.5
When does the system call IndexedEntityQuery?
When it encounters a problem with your app’s Spotlight index and your query type adopts IndexedEntityQuery (with its entity conforming to IndexedEntity).6 The system calls the protocol’s methods to have you fetch the affected entities and donate them again to Spotlight. If your query doesn’t adopt the protocol, Spotlight falls back to asking your CSSearchableIndex object, or your CSImportExtension if you donated through that type.6
Why use EntityCollection instead of a plain array of entities?
EntityCollection<Entity> stores only each entity’s identifier up front and fetches full instances later if needed.11 As an intent parameter it stops the system from forcing every identifier to resolve into a full instance during parameter resolution, which for a parameter holding hundreds of entities saves time and memory at a potentially critical moment.11 A plain [Entity] array resolves everything eagerly.
Is RunSystemShortcutIntent usable outside widgets?
No. It exists solely to initialize a Button with the system-shortcut initializer for placement in a widget, and it provides no functionality in other contexts.12 It surfaces metadata for the widget’s configuration UI and represents the user’s chosen action; it does not give your widget or app access to the underlying shortcut’s actions, parameters, or implementation.12
The full Apple Ecosystem cluster: typed App Intents; the iOS 26 additions; the routing question against MCP tools; Foundation Models; the new Foundation Models tool-calling control; the runtime vs tooling LLM distinction; three surfaces; the single source of truth pattern; MCP servers alongside an app; Live Activities; the watchOS runtime; SwiftUI internals; SwiftData schema discipline; Liquid Glass patterns; multi-platform shipping; the platform matrix; Vision framework; @Observable internals; Accessibility as platform. The hub is at the Apple Ecosystem Series. For broader iOS-with-AI-agents context, see the iOS Agent Development guide.
References
-
Apple Developer Documentation: App Intents. The framework reference covering
AppIntent,AppEntity, queries, parameters, and the iOS 27 additions. ↩ -
Apple Developer Documentation:
LongRunningIntent(iOS 27.0 beta). “An interface you use to extend the background execution time of an app intent that performs a long-running task.” Declared asprotocol LongRunningIntent : ProgressReportingIntent; the system traditionally gives background tasks up to 30 seconds. ↩↩↩↩↩↩↩ -
Apple Developer Documentation:
performBackgroundTask(options:operation:)(iOS 27.0 beta). Runs an operation in the background with extended time past the standard 30-second limit; requires regular progress updates or the system can cancel the extension; Live Activities renders the progress automatically. ↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation:
LongRunningTaskOptions(iOS 27.0 beta). Options for configuring long-running tasks; declares additional resource requirements, passed toperformBackgroundTask(options:operation:). ↩↩ -
Apple Developer Documentation:
SyncableEntity(iOS 27.0 beta). “An interface that indicates your entity has an identifier that’s consistent across devices.” Declared asprotocol SyncableEntity : AppEntity; Siri uses it to transfer a conversation between devices. ↩↩↩↩↩↩↩↩ -
Apple Developer Documentation:
IndexedEntityQuery(iOS 27.0 beta). “An interface that adds Spotlight reindexing support to your entity query.” Declared asprotocol IndexedEntityQuery : EntityQuery where Self.Entity : IndexedEntity. ↩↩↩↩↩↩↩ -
Apple Developer Documentation:
AppUnionValue(iOS 27.0 beta). “A protocol that provides nominal type identity and metadata for union values.” Declared asprotocol AppUnionValue : TypeDisplayRepresentable; conformance is generated by the@UnionValuemacro. ↩↩↩↩↩↩ -
Apple Developer Documentation:
AppUnionValueCasesProviding(iOS 27.0 beta). Declared asprotocol AppUnionValueCasesProviding : AppEnum; conformed to automatically by theCasesenum generated by the@UnionValuemacro. ↩↩↩↩ -
Apple Developer Documentation:
OwnershipProvidingEntity(iOS 27.0 beta). “A type that provides the system with ownership and sharing context for an app entity.” Declared asprotocol OwnershipProvidingEntity : AppEntity; prompts for confirmation on shared or publicly accessible entities. ↩↩↩↩↩ -
Apple Developer Documentation:
EntityOwnership(iOS 27.0 beta). “A type that represents the ownership and sharing characteristics of an app entity.” Declared asstruct EntityOwnership; flag-based, combinable with anOptionSet. ↩↩ -
Apple Developer Documentation:
EntityCollection(iOS 27.0 beta). “An array of entity identifiers that you use to improve the efficiency of operations involving large numbers of entities.” Declared asstruct EntityCollection<Entity> where Entity : AppEntity; stores identifiers initially and resolves full instances lazily. ↩↩↩↩↩↩↩ -
Apple Developer Documentation:
RunSystemShortcutIntent(iOS 27.0 beta). “An app intent you use in widgets to open another app or perform an App Shortcut, custom shortcut, or system action.” Declared asstruct RunSystemShortcutIntent; usable only to initialize a widgetButton. ↩↩↩↩↩↩ -
Apple Developer Documentation:
IntentExecutionTargets(iOS 27.0 beta). “A set of options that describes which process performs an intent or entity query.” Declared asstruct IntentExecutionTargets; constrains execution to the app, App Intents extension, or any available target. ↩↩↩↩