Make Your App Work With Siri AI Using App Schemas
The App Intents story got much bigger at WWDC26.
For the last few years, I mostly thought about App Intents as a way to expose specific actions to Shortcuts, Spotlight, widgets, and system surfaces. That is still true, but Siri AI raises the bar. The goal is no longer just “let the system run this action.” The goal is “let the system understand my app’s content, resolve what the user means, connect that to visible UI, and safely perform an action.”
That is where App Schemas come in.
An AppIntent describes an action. An AppEntity describes a piece of your app’s content. An App Schema tells Siri what kind of thing that entity or action is. A message is not just an arbitrary struct with a title. It can be a message-shaped entity in the Messages schema domain, alongside related concepts like conversations, message people, attachments, and message actions.
That distinction matters. If you only define a custom intent, you have an action the system can surface. If you adopt the right schema domain, Siri can reason about the user’s natural language in terms of concepts it already knows.
Start with app entities
The first step is not voice. It is modeling. Siri needs stable, meaningful entities that represent your app’s real data.
Apple’s WWDC26 session uses a messaging app. A message entity can adopt a system schema and mark which property should be indexed for search. This is the App Intents-facing shape; your real app still needs to supply stable identifier lookup and any suggested entities your workflow requires:
import AppIntents
import Foundation
@AppEntity(schema: .messages.message)
struct MessageEntity: IndexedEntity {
let id: UUID
@Property(indexingKey: \.textContent)
var body: AttributedString?
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Message")
static var defaultQuery = MessageQuery()
var displayRepresentation: DisplayRepresentation {
let title = body.map { String($0.characters.prefix(40)) } ?? "Message"
return DisplayRepresentation(title: "\(title)")
}
}
struct MessageQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [MessageEntity] {
// Query your app's message store here.
[]
}
}
The exact schema you choose depends on the domain your app belongs to. The important part is the mental model: your entity is not a second database. It is a typed, system-facing representation of the content your app already owns.
For content Siri should be able to search semantically, adopt IndexedEntity and donate the entities into Spotlight. Apple describes this as the path that lets Siri match by meaning instead of only by exact strings. That is a huge difference for requests like “find the message about the flight” or “open the note about the budget meeting.”
Fall back to string queries when indexing is not right
Some data cannot be indexed ahead of time. It might live on a server, be too large, or change too frequently. In those cases, you can still provide an EntityStringQuery and let your app perform the lookup:
import AppIntents
import SwiftData
struct ContactQuery: EntityStringQuery {
@Dependency
var modelContext: ModelContext
func entities(matching string: String) async throws -> [ContactEntity] {
let predicate = #Predicate<Contact> { contact in
contact.name.localizedStandardContains(string)
}
let descriptor = FetchDescriptor<Contact>(predicate: predicate)
let matches = try modelContext.fetch(descriptor)
return matches.map(\.entity)
}
}
That gives you control, but it is not the same experience as indexing. A string query can search your store. An indexed, schematized entity gives the system more room to resolve concepts and relationships.
My default would be:
- Use
IndexedEntityfor local user content that can safely be indexed and donated. - Use
EntityStringQuerywhen the app must query live state. - Keep identifiers stable so a resolved entity can be opened later.
- Make display representations human-readable, because these surfaces are user-facing.
Map actions to schemas Siri understands
Once the entities exist, actions become more interesting. A generic AppIntent can expose “Send Unicorn Message.” A schematized intent can map to the system’s idea of sending a message.
That mapping is what lets Siri handle natural language like “Send a message to Glow saying I’ll be late” without you writing a natural-language parser.
The app still owns the work. Your intent validates parameters, calls your app logic, and returns an entity. Siri owns the language understanding and the flow around missing parameters or confirmations.
The practical advice here is slightly humbling: do not invent a custom action if there is a schema domain that matches what your app already does. The schema is the contract that gives Siri a consistent action shape.
Annotate visible SwiftUI content
The most exciting part for SwiftUI apps is onscreen awareness. Siri can understand references like “this message” or “the last one” only if your UI tells the system what the visible rows represent.
For a list with multiple meaningful items, attach the entity identifier to each row:
import AppIntents
import SwiftUI
struct ConversationView: View {
let messages: [Message]
var body: some View {
List {
ForEach(messages) { message in
MessageRow(message: message)
.appEntityIdentifier(
EntityIdentifier(
for: MessageEntity.self,
identifier: message.id
)
)
}
}
}
}
For one primary thing on screen, a UserActivity can be the better fit. For repeated rows, view annotations are more precise.
This is one of those APIs that looks small but changes the product feel. It connects your SwiftUI hierarchy to Siri’s understanding of context. Without it, the user has to name everything. With it, the user can refer to what they are already looking at.
Let content move across apps
Siri AI workflows often cross app boundaries. “Email this reply to my wife” requires one app to describe the visible content and another app to act on it.
That is where Transferable and IntentValueRepresentation fit:
import AppIntents
import CoreTransferable
extension ContactEntity: Transferable {
static var transferRepresentation: some TransferRepresentation {
IntentValueRepresentation(exporting: \.person)
}
}
For incoming content, you can resolve it to something that already exists:
import AppIntents
import SwiftData
struct ContactEntityQuery: IntentValueQuery {
@Dependency
var model: ModelContainer
func values(for input: [IntentPerson]) async throws -> [ContactEntity] {
let names = input.map(\.displayName)
let descriptor = FetchDescriptor<Contact>()
let contacts = try model.mainContext.fetch(descriptor)
return contacts
.filter { contact in
names.contains(where: { name in
contact.name.localizedStandardContains(name)
})
}
.map(\.entity)
}
}
Or you can import a new entity with IntentValueRepresentation(importing:) when that is the right app behavior. The key is to be explicit. Cross-app automation gets weird fast when apps pretend that all incoming content maps perfectly to existing data.
Test the layers separately
The testing story matters because Siri integrations can otherwise become a manual QA ritual.
Apple’s recommended ladder is sensible:
- Use
AppIntentsTestingfirst to exercise intents and queries through the App Intents stack without invoking Siri. - Use Shortcuts to inspect the structured action UI and parameters.
- Use Spotlight to verify that entities are indexed and discoverable.
- Use Siri last, once the pieces underneath are already stable.
That order keeps you from debugging natural language, indexing, entity resolution, and business logic at the same time.
I would also keep a tiny test fixture for each entity type. If a message, contact, document, or recipe entity is important enough for Siri, it is important enough to validate outside the app UI.
The useful mental model
I think the WWDC26 App Schemas story is best understood as four layers:
- Entities: the nouns your app owns.
- Indexing and queries: how the system finds the right noun.
- Schematized intents: the verbs Siri can safely perform.
- UI annotations and transfer: the context that lets workflows cross screens and apps.
That is much more than “add voice control.” It is a way to make your app’s model legible to the system.
The caution is that partial adoption can feel partial. In the Messages domain, Apple documents the action schemas as a group; if your app supports one of them, it needs to support the rest, and Xcode can surface missing related schemas like drafting a message at build time. That is a good thing. It means the tooling is nudging you toward an integration that behaves like a real system feature instead of a demo command.
Further reading: