This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.
Expo AppIntents
A library for exposing Apple App Intents, Siri, Shortcuts, Spotlight, and Apple Intelligence entry points from Expo apps.
This library is currently in alpha and will frequently experience breaking changes.
expo-app-intents exposes Apple App Intents from your app. It keeps the intent declarations in Swift inline modules so Apple's build-time metadata extraction can find them. The package provides the JavaScript API, native invocation queue, entity storage, config plugin, and a starter app-intents directory for native inline module code.
You cannot create App Intent types dynamically from JavaScript at runtime. Declare the AppIntent types you need in Swift. Parameterized intents also require AppEntity and EntityQuery types. If your app exposes shortcut phrases, you usually declare an AppShortcutsProvider. You can leave it out when your app does not use shortcut phrases, such as when it exposes only schema intents. Use this package to deliver invocations to JavaScript and keep dynamic entity values in sync.
Apple Intelligence support
With expo-app-intents, you can implement Apple Intelligence features, including on-screen intelligence. On iOS 18.4 and later, you can mark the entity represented by a view so Siri can act on the visible content. Apply the appEntityIdentifier() modifier to @expo/ui views, or wrap React Native views in AppEntityView. Both require the project to be compiled with Xcode 27 or later. In a project built with an earlier Xcode, the views still render normally, but the system cannot associate them with the entity.
Installation
If you are installing this in an existing React Native app, make sure to install expo in your project.
Configuration in app config
The initializer writes the required configuration for you:
Example app.json with config plugin
If you use a directory other than app-intents, pass the same directory name to the config plugin:
Example app.json with config plugin
How it works
App Intents differ from most Expo APIs because Apple discovers them at build time. Compile the Swift types that describe your intents, entities, queries, and shortcut phrases into the app target. Xcode's App Intents metadata processor can then extract them. expo-app-intents uses Expo inline modules for this app-target Swift and provides an Expo package for the JavaScript API, native storage, and event delivery.
Expo inline modules watch the generated app-intents directory, which contains your app-owned Swift code:
- AppShortcuts.swift defines your concrete
AppShortcutsProviderwhen the selected examples use shortcut phrases. Shortcut phrases are static metadata. The build compiles them into the app, and JavaScript cannot add them dynamically. - AppIntentsSetup.swift contains the app-target setup required by the selected features. When the app has an
AppShortcutsProvider, this file connectsAppShortcuts.updateAppShortcutParameters()to theexpo-app-intentsruntime. - The remaining Swift files define concrete
AppIntent,AppEntity, andEntityQuerytypes. They compile into the app target. Expo registers a file as a module only when it contains an Expo module definition.
When an intent runs, complete the native work that Siri or Shortcuts expects in its perform() method. Then dispatch the intent to JavaScript:
await AppIntentDispatcher.shared.dispatch( name: "orderFood", params: ["dishId": .string(dish.id), "dishName": .string(dish.name)] )
AppIntentDispatcher is a Swift actor. It records the invocation in native storage first. If the JavaScript runtime is already observing, it then emits an onIntent event. This process provides at-least-once delivery. Handle each invocation by id, and call removePendingInvocationAsync(id) after applying the action.
If Siri or Shortcuts runs while JavaScript is not running, the invocation stays in the pending queue. When your app starts, useAppIntents() calls your handler with the current pending invocations and sets newIntent to null. Each live invocation calls the handler again with the new invocation. You can also read the queue with getPendingInvocationsAsync().
Parameterized intents use native entities. For example, the restaurant template defines DishEntity and DishQuery in Swift. JavaScript supplies the current dish list with setEntityCatalogAsync('dish', dishes). The native query reads that catalog through AppIntentEntityStore.shared, so Siri and Shortcuts can resolve spoken values by identifier, title, or synonym. Calling setEntityCatalogAsync() replaces the catalog and requests a shortcut parameter refresh. Call it again whenever those values change.
Schema intents, such as the mail example, are also defined in app-target Swift. Apple divides app schema domains into two groups. Apple Intelligence and Siri can discover types that conform to a primary domain, such as mail, photos, or files. Shortcuts-specific domains, such as journal, books, or reader, appear only in the Shortcuts app.
A schema intent does not need a shortcut phrase. Its schema conformance makes it discoverable. Xcode extracts the App Intents metadata at build time, so Siri and the Shortcuts app can use the action without an entry in your AppShortcutsProvider. The mail example therefore contributes no AppShortcut. If none of the examples you select contributes one, the initializer does not write AppShortcuts.swift.
Xcode rejects an AppShortcutsProvider whose appShortcuts body contains no AppShortcut with the error 'AppShortcutsProvider' property 'appShortcuts' requires builder syntax. Add AppShortcuts.swift when you have a phrase to declare.
Add an AppShortcut only when you want a compiled launch phrase such as "Order food in MyApp". You cannot add phrases conditionally, so a provider that references an availability-gated intent must use the same availability. In this case, consider writing a plain wrapper intent.
The system must also be able to resolve a schema entity. Its default query must be an EntityStringQuery, or the entity must be indexed. Xcode rejects a plain EntityQuery when it extracts App Intents metadata.
Native Swift concepts
The initializer provides working Swift examples, but the intent declarations belong to your app. The following sections describe the main native pieces you use to adapt the examples or write your own intents.
Dispatch an invocation to JavaScript
An intent's perform() method produces the result Siri or Shortcuts expects in Swift. Call AppIntentDispatcher.shared.dispatch() to also record an invocation for JavaScript:
import AppIntents internal import ExpoAppIntents struct SaveNoteIntent: AppIntent { static let title: LocalizedStringResource = "Save Note" static let openAppWhenRun = true @Parameter(title: "Text") var text: String @MainActor func perform() async throws -> some IntentResult & ProvidesDialog { await AppIntentDispatcher.shared.dispatch( name: "saveNote", params: ["text": .string(text)] ) return .result(dialog: "Your note was saved.") } }
Dispatching is one-way. It saves the invocation and returns without waiting for JavaScript because the intent may run while your app is closed. Return dialogs, values, or other system-facing results from Swift. Use the JavaScript handler to update your app's state when it runs.
params accepts JSON-compatible AppIntentValue values:
The value type also supports Swift literals, so you can use values such as "note", 1, true, arrays, and dictionaries directly.
Read dynamic entities in Swift
Publish current entity values from JavaScript with setEntityCatalogAsync(). Your Swift EntityQuery reads the same catalog through AppIntentEntityStore.shared:
struct NoteQuery: EntityStringQuery { func entities(for identifiers: [String]) async throws -> [NoteEntity] { return try await AppIntentEntityStore.shared .entities(ofKind: "note", matching: identifiers) .map(NoteEntity.init(record:)) } func suggestedEntities() async throws -> [NoteEntity] { return try await AppIntentEntityStore.shared .entities(ofKind: "note") .map(NoteEntity.init(record:)) } }
The kind string must match the value passed to setEntityCatalogAsync(). Each AppIntentEntityRecord contains an id and a display title. It can also contain a subtitle and synonyms. Convert the record into your concrete AppEntity in an initializer such as init(record:). The device stores catalogs, so keep them compact and publish only the values Siri and Shortcuts need.
Add shortcut phrases when needed
Use an AppShortcutsProvider when you want compiled launch phrases such as "Save a note in MyApp".
Usage
After installing the package, run the initializer from your project root:
The command adds the config plugin, enables inline modules for the top-level app-intents directory, and writes starter Swift files.
The command also asks which examples to include and preselects only minimal. This option writes the setup module without intents, shortcut phrases, or any of the examples below. Pass --examples to choose examples without the prompt. This flag is also the only way to select them in a non-interactive run:
--examples accepts minimal, counter, restaurant, mail, or all. The all option selects the three examples described on this page. The command runs non-interactively when CI is set to 1 or true, EXPO_NONINTERACTIVE has a value, or standard input is not attached to an interactive terminal. In these cases, the command scaffolds minimal without prompting.
init keeps any Swift files that are already in the selected directory. If you run it again to add another example, review the command's warnings before rebuilding. Some existing files may need a small manual update.
Pass --dir to generate the module in a directory other than app-intents. The initializer writes the same directory name to the config plugin and to the watched directories:
Keep the App Intents directory at the project root, like the default app-intents directory. This location keeps the native declarations easy to find and separate from the rest of the app.
Pass --visual-intelligence with the mail example to scaffold its Spotlight integration. This adds native code that makes drafts searchable and lets the system transfer and open a draft. It also registers the mailDraft entity kind so appEntityIdentifier() can identify the draft represented by a view. The generated code extends the base mail types, so the example works the same way with or without this flag.
Then rebuild the native project:
Your app is now set up to use App Intents. Depending on the selected examples, you can add handlers or populate the App Intents from JavaScript:
Connect an on-screen view to an entity
On iOS 18.4 and later, appEntityIdentifier() tells Siri which entity an @expo/ui SwiftUI view represents, and AppEntityView does the same for React Native views. Both require the project to be compiled with Xcode 27 or later, because the Apple APIs they use are not available in earlier SDKs. Use the same entity kind and identifier in the native registration, JavaScript catalog, and view.
The --visual-intelligence mail example registers mailDraft in AppIntentsSetup.swift:
OnCreate { if #available(iOS 18.0, *) { AppEntityIdentifierRegistry.shared.registerIndexed( "mailDraft", as: MailDraftEntity.self ) } }
registerIndexed() makes identifiers available to on-screen intelligence and keeps Spotlight synchronized with the entity catalog. A custom indexed entity must conform to IndexedEntity and AppIntentEntityRecordConvertible. To support on-screen intelligence without Spotlight indexing, register the entity with AppEntityIdentifierRegistry.shared.register() instead.
Publish the entity from JavaScript using the same mailDraft kind:
await AppIntents.setEntityCatalogAsync('mailDraft', [ { id: 'release-notes', title: 'Release notes for review', subtitle: 'The release notes are ready for a final pass.', }, ]);
Then attach the matching kind and identifier to an @expo/ui view:
import { Column, Host, Text } from '@expo/ui'; import * as AppIntents from 'expo-app-intents'; export function MailDraftCard({ draft }: { draft: { id: string; subject: string } }) { return ( <Host matchContents={{ vertical: true }}> <Column modifiers={[AppIntents.appEntityIdentifier('mailDraft', draft.id)]}> <Text>{draft.subject}</Text> </Column> </Host> ); }
To mark React Native views instead, wrap them in AppEntityView with the same kind and identifier:
import { Text } from 'react-native'; import * as AppIntents from 'expo-app-intents'; export function MailDraftCard({ draft }: { draft: { id: string; subject: string } }) { return ( <AppIntents.AppEntityView entity="mailDraft" entityId={draft.id}> <Text>{draft.subject}</Text> </AppIntents.AppEntityView> ); }
Both views still render normally on earlier iOS versions, in a project compiled with an Xcode version earlier than 27, or when the kind is not registered. In these cases, the system cannot associate the view with the entity, and expo-app-intents logs a warning once.
Handle the counter example
The counter example dispatches an increaseCounter invocation. Mount the hook once near the root of your app. It can then process pending invocations captured while JavaScript was not running and live invocations received while the app is running.
Delivery is at-least-once, so the same invocation can reach your handler more than once. An invocation stays in the pending queue until removePendingInvocationAsync(id) completes successfully. Each live invocation delivers a fresh snapshot of that queue.
Keep the identifiers of invocations you have already applied in a ref and skip them. The restaurant and mail examples use the same guard. A ref deduplicates invocations only within one session. If removePendingInvocationAsync() fails, the invocation remains pending on the next launch and your handler applies it again. Persist the handled identifiers when you cannot safely reapply an action. The queue holds at most 100 invocations and drops the oldest to make room. An app that never removes invocations eventually loses them.
import { useRef, useState } from 'react'; import { Text, View } from 'react-native'; import * as AppIntents from 'expo-app-intents'; export function CounterIntentHandler() { const [count, setCount] = useState(0); const [lastIntentId, setLastIntentId] = useState<string | null>(null); const handledIds = useRef(new Set<string>()); AppIntents.useAppIntents(async pendingIntents => { for (const invocation of pendingIntents) { if (invocation.name !== 'increaseCounter' || handledIds.current.has(invocation.id)) { continue; } handledIds.current.add(invocation.id); setCount(value => value + 1); setLastIntentId(invocation.id); await AppIntents.removePendingInvocationAsync(invocation.id); } }); return ( <View> <Text>Counter: {count}</Text> {lastIntentId ? <Text>Last opened by Siri: {lastIntentId}</Text> : null} </View> ); }
Handle the restaurant example
The restaurant example uses a dynamic dish entity catalog. Seed the catalog from JavaScript after the app starts, and then handle orderFood invocations. Calling setEntityCatalogAsync() replaces the native catalog and refreshes shortcut parameters. Siri and Shortcuts can then resolve dishes by identifier, title, or synonym. Handle a rejected promise so a failed catalog update appears in your logs instead of becoming an unhandled rejection.
import { useEffect, useRef, useState } from 'react'; import { Text, View } from 'react-native'; import * as AppIntents from 'expo-app-intents'; const dishes = [ { id: 'margherita-pizza', title: 'Margherita Pizza', synonyms: ['margherita', 'pizza'] }, { id: 'spaghetti-carbonara', title: 'Spaghetti Carbonara', synonyms: ['carbonara'] }, { id: 'tiramisu', title: 'Tiramisu', synonyms: ['dessert'] }, ]; export function RestaurantIntentHandler() { const [latestOrder, setLatestOrder] = useState<string | null>(null); const handledIds = useRef(new Set<string>()); useEffect(() => { AppIntents.setEntityCatalogAsync('dish', dishes).catch(error => console.warn('Could not publish the dish catalog.', error) ); }, []); AppIntents.useAppIntents(async pendingIntents => { for (const invocation of pendingIntents) { if (invocation.name !== 'orderFood' || handledIds.current.has(invocation.id)) { continue; } handledIds.current.add(invocation.id); setLatestOrder(String(invocation.params.dishName || invocation.params.dishId)); await AppIntents.removePendingInvocationAsync(invocation.id); } }); return ( <View> <Text>{latestOrder ? `Latest order: ${latestOrder}` : 'No orders yet.'}</Text> </View> ); }
Handle the mail example
The mail example adopts Apple's mail schema domain and has no shortcut phrases. You can reach both intents through Siri, Apple Intelligence, or the Shortcuts app. createMailDraft sends id, subject, body, recipients, and attachmentCount. deleteMailDrafts sends an ids array. Because deleting is destructive, the schema requires local device authentication and displays a confirmation before the intent runs.
DeleteDraftIntent takes an array of MailDraftEntity values, so one invocation can delete several drafts. MailDraftEntityQuery resolves those entities from the mailDraft catalog. Publish the catalog whenever your drafts change. Otherwise, Siri and the Shortcuts app cannot offer a draft, and the delete intent cannot run. Each record maps title to the subject and subtitle to the body. Native storage preserves the catalog across restarts, so do not publish an empty catalog before your drafts load.
Give every record a non-empty title. setEntityCatalogAsync() rejects a catalog when any entity has an empty id or title because Siri cannot resolve the entity. The rejection discards the entire update and keeps the previous catalog. A draft created without a subject provides an empty subject string instead of undefined. Use || instead of ?? for the fallback because ?? preserves the empty string.
import { useEffect, useRef, useState } from 'react'; import { FlatList, Text } from 'react-native'; import * as AppIntents from 'expo-app-intents'; type MailDraft = { id: string; subject: string; body: string; }; export function MailIntentHandler() { const [drafts, setDrafts] = useState<MailDraft[]>([]); const handledIds = useRef(new Set<string>()); const hasPublished = useRef(false); useEffect(() => { // The native catalog outlives the app, but `drafts` starts empty, so publishing on the // first render would clear a catalog written by an earlier launch. Wait for a draft. if (drafts.length === 0 && !hasPublished.current) { return; } hasPublished.current = true; AppIntents.setEntityCatalogAsync( 'mailDraft', drafts.map(draft => ({ id: draft.id, title: draft.subject, subtitle: draft.body })) ).catch(error => console.warn('Could not publish the mail draft catalog.', error)); }, [drafts]); AppIntents.useAppIntents(async pendingIntents => { for (const invocation of pendingIntents) { const isMailIntent = invocation.name === 'createMailDraft' || invocation.name === 'deleteMailDrafts'; if (!isMailIntent || handledIds.current.has(invocation.id)) { continue; } handledIds.current.add(invocation.id); if (invocation.name === 'createMailDraft') { const body = String(invocation.params.body ?? ''); const draft = { id: String(invocation.params.id || invocation.id), // Use `||`, not `??`: a draft with no subject arrives as an empty string. subject: String(invocation.params.subject || body.slice(0, 40) || 'No subject'), body, }; setDrafts(current => [draft, ...current]); } else { const ids = new Set((invocation.params.ids as string[] | undefined) ?? []); setDrafts(current => current.filter(draft => !ids.has(draft.id))); } await AppIntents.removePendingInvocationAsync(invocation.id); } }); return ( <FlatList data={drafts} keyExtractor={draft => draft.id} renderItem={({ item }) => ( <Text> {item.subject}: {item.body} </Text> )} /> ); }
Provide dynamic values for parameterized intents from JavaScript:
await AppIntents.setEntityCatalogAsync('dish', [ { id: 'margherita-pizza', title: 'Margherita Pizza', synonyms: ['margherita', 'pizza'] }, { id: 'spaghetti-carbonara', title: 'Spaghetti Carbonara', synonyms: ['carbonara'] }, ]);
The generated entity queries read these values through await AppIntentEntityStore.shared.entities(ofKind:). Siri and Shortcuts can then offer and resolve them as intent parameters.
Set hideInSpotlight on an entity to keep it out of the Spotlight index while leaving it resolvable. Siri can still offer it as a parameter and open it by identifier:
await AppIntents.setEntityCatalogAsync('mailDraft', [ { id: 'release-notes', title: 'Release notes for review' }, { id: 'salary-review', title: 'Salary review', hideInSpotlight: true }, ]);
The flag only applies to entities registered natively with registerIndexed. Flipping it takes effect the next time the catalog is published, and an entity that was already indexed is removed. To keep an entity away from Siri altogether, leave it out of the catalog instead. An entity that isn't published can't be offered, matched, or resolved.
Classic App Shortcut phrases can interpolate at most one non-array parameter. The generated restaurant order example declares four phrases:
"Place an order in \(.applicationName)", "Order food in \(.applicationName)", "Order \(\.$dish) in \(.applicationName)", "Place an order for \(\.$dish) in \(.applicationName)"
The first two phrases do not interpolate a parameter, so Siri asks which dish you want. The last two interpolate the dish parameter. This parameter lets Siri run a one-shot command such as "Order Tiramisu in MyApp". Parameterized phrases can create pre-filled tiles in Shortcuts. If an older build created a tile that uses stale parameter values, delete it so iOS can recreate it from the current app metadata.
Platform constraints
- The native runtime supports iOS 16.4 and later, tvOS 16.4 and later, and macOS 13.4 and later.
expo-app-intentsis not available in Expo Go. - The initializer, bundled examples, and build commands on this page focus on iOS. Individual App Intents APIs and schema domains can require newer operating-system versions or be unavailable on other Apple platforms.
- App Shortcut phrases are compiled into the app and cannot be added dynamically from JavaScript.
- Every App Shortcut phrase must include
\(.applicationName). - A single App Shortcut phrase can interpolate at most one non-array parameter.
- Apps can define at most 10 App Shortcuts.
- Schema examples require iOS 18 or later and must match one of Apple's supported App Intent domains. Only primary domains are discoverable by Apple Intelligence and Siri.
- Some schemas require a newer iOS version than their domain. For example,
.mail.openDraftand.system.openrequire iOS 27 or later.
Additional resources
Start with the counter example generated by npx expo-app-intents init. It demonstrates the basic connection between the JavaScript and Swift runtimes. Then use the restaurant and mail examples to learn about dynamic entities and schema intents.
Use the following Apple resources to learn more about App Intent development:
- Accelerating app interactions with App Intents
- Code-along: Make your app available to Siri
- Making actions and content discoverable by Apple Intelligence
API
import * as AppIntents from 'expo-app-intents';
Component
Type: React.Element<AppEntityViewProps>
A wrapper that associates its contents with an App Entity so Apple Intelligence and Siri
can understand which entity is visible onscreen. Use it around React Native views; for @expo/ui
views, use the appEntityIdentifier() modifier instead.
Note: The entity association requires iOS 18.4 or later and a project compiled with Xcode 27 or later. The children still render normally when those requirements are not met, on other platforms, or when the native module is unavailable.
Props for a UIKit wrapper that associates its onscreen content with one App Entity.
Hooks
Calls handler once with the pending invocations recorded while JavaScript was not running,
then again for every new invocation received while the component is mounted.
newIntent is null for the initial pending snapshot. Later calls include the current
pending snapshot and the new invocation that triggered the call. The initial call is always
delivered first, and new invocations are delivered one at a time in arrival order.
Pending invocations are not removed automatically. The handler must call
removePendingInvocationAsync(id)
after handling each one. The queue holds at most 100 invocations, and once it is full the oldest
are dropped to make room, so a handler that never removes them does eventually lose invocations.
When App Intents are unavailable, this hook calls the handler with an empty snapshot and does not call it again.
voidMethods
Returns an ExpoUI SwiftUI modifier config that ties a view to an AppEntity identifier.
The entity value must be registered from app-target Swift with
AppEntityIdentifierRegistry.shared.register(_:as:) or
AppEntityIdentifierRegistry.shared.registerIndexed(_:as:).
Note: The entity association requires iOS 18.4 or later and a project compiled with Xcode 27 or later. Otherwise the view renders normally without it.
AppEntityIdentifierModifierRemoves all pending invocations. Does nothing when App Intents are unavailable.
Promise<void>Returns the current entity catalog of the given kind. The returned promise is fulfilled with an empty array when the kind was never published or App Intents are unavailable.
The returned promise is rejected when the stored catalog cannot be read.
Promise<AppIntentEntity[]>Returns invocations that have not been removed from the pending queue yet, oldest first. The returned promise is fulfilled with an empty array when App Intents are unavailable.
The queue keeps at most 100 invocations. An app that never removes them keeps only the newest 100.
The returned promise is rejected when the stored queue cannot be read. In this case, the invocations waiting in the queue are not delivered. The queue starts empty afterward, so a later call succeeds.
Promise<AppIntentInvocation[]>Returns whether App Intents are available on this device.
Returns false on Android and web.
booleanAsks the system to re-evaluate App Shortcut phrases and parameter values.
The returned promise is rejected with UnavailabilityError when App Intents are unavailable. It
is also rejected when the app has no AppShortcutsProvider to refresh. Publishing a catalog with
setEntityCatalogAsync() also refreshes
shortcuts.
Promise<void>Rebuilds the Spotlight index from the stored entity catalog, whether or not the catalog
changed. setEntityCatalogAsync already keeps the index in step, so this is only needed to
recover from an index that no longer matches the catalog: one the system evicted, or one left
stale by an app update that changed how entities describe themselves.
Pass a kind to rebuild one catalog, or omit it to rebuild every kind registered natively with
registerIndexed. Kinds with no indexed registration are ignored.
Rejects when a catalog cannot be read or the index cannot be written, because this is the retry
path and a caller that asked for a rebuild has no other way to learn it did not happen. Every
kind is attempted before the first failure is reported, so one unreadable catalog does not skip
the rest. A kind whose rebuild failed is retried by the next setEntityCatalogAsync, even when
the catalog it publishes is unchanged.
Promise<void>Removes a handled invocation so it is no longer delivered or returned as pending. Does nothing when App Intents are unavailable.
The returned promise is rejected when the stored queue cannot be read or written. A rejection caused by an unreadable queue leaves nothing pending. The native layer sets aside the unreadable data, so it removes every invocation that was waiting in the queue instead of only this one.
Promise<void>Replaces the entity catalog of the given kind and asks the system to retrain
parameterized shortcut phrases against the new values.
Entities registered natively with
registerIndexed also have their Spotlight index rebuilt from the new catalog.
Publishing a catalog that matches the stored catalog does nothing, so apps can safely call this function on every start.
The native store uses UserDefaults, which is best suited to compact catalogs. For large
datasets, such as thousands of contacts or songs, apps should store the full data locally and
publish only the subset that Siri and Shortcuts need.
When kind or an entity is invalid, the returned promise is rejected and the previous catalog
remains available. The kind is invalid when it is empty or contains only whitespace. An entity
is invalid when its id or title is empty or contains only whitespace. An entity is also
invalid when another entity in the catalog has the same id.
Promise<void>Event subscriptions
Adds a listener for live App Intent invocations dispatched while JavaScript is observing.
Pending invocations recorded while JavaScript was not running are available through
getPendingInvocationsAsync()oruseAppIntents().
EventSubscriptionInterfaces
Handles a snapshot of pending invocations. After the initial call, it also receives the new invocation that triggered the handler.
Types
ExpoUI modifier config that associates a SwiftUI view with an AppEntity identifier.
Built on @expo/ui's own ModifierConfig rather than restating its shape, so that a change to
what the modifiers prop accepts is a type error here instead of a value ExpoUI rejects at
runtime.
Type: ModifierConfig extended by:
A single recorded App Intent invocation.
The native layer persists each invocation until removePendingInvocationAsync removes it.
Delivery is at-least-once, so handlers must be idempotent for each id.