Developer guide

How to make your app work with Siri AI

By SwiftSoft · Updated · Based on Apple’s developer documentation

The short answer

Turn your key App Intents into tools with @AppIntent(.tool), describe every intent, parameter and entity property, index your entities in Spotlight, add card snippets and app skills where they help, then verify with the Siri Integration Advisor and Siri Evaluations on a real device running the 27.3 beta. Customers get the features when Apple ships them, and App Store submission opens in the spring.

What changes with Siri AI

Until now, Siri understood an app’s actions and content mainly when they fit one of Apple’s app schema domains, such as messaging or photos, or through App Shortcuts. With the Siri AI Developer Preview, Siri can also find your app’s custom content and perform its custom actions. One rule decides everything else in this guide: Siri can only find content and perform actions that your app makes explicitly available through App Intents.

People don’t have to mention your app by name when:

  • your app is in the foreground;
  • your app has an active Live Activity, so “change my pickup to the airport” just works;
  • they continue a conversation in the Siri app that already mentioned your app;
  • one of your app’s entities appears in a search result (per Apple’s ToolIntent documentation).

While your app is in the background, people have to name it: “Are the sneakers I liked in <app name> on sale?” Requests can also span apps. Siri can find content in one app, hand it to another, and act on it, for example sending a saved item to a contact through a messaging app.

Availability and timeline

  • November 3, 2026: the Developer Preview opened in the iOS, iPadOS, macOS and visionOS 27.3 betas, on devices that support Apple Intelligence and are set to a supported language.
  • Now: the features work in builds you sign with a development profile and for internal TestFlight testers (App Store Connect users on your team). They aren’t available to external TestFlight testers or App Store users: a build with the code can reach external testers, but the Siri AI features don’t work for them yet, and the App Store doesn’t accept builds made with a beta SDK.
  • In a few months: Apple says the features first ship to customers.
  • Spring 2027: App Store submission opens.
  • Still to come: Apple says it will follow up with terms and usage policies for experiences built on Siri AI.

Plan around these limits too:

  • During the preview, the App Intents API and how Siri behaves may change.
  • Siri AI is currently unavailable in the EU on iOS, iPadOS and watchOS, and isn’t available to users under 13.
  • Supported languages are English, French, Japanese, Korean, Portuguese and Spanish, with Traditional Chinese, Norwegian, Turkish and Vietnamese coming soon.
  • Siri AI relies on server-side models and has daily usage limits. Apple says expanded access will be available for a fee in the future.
  • The tool and generable-entity APIs are marked for iOS, iPadOS, Mac Catalyst, macOS and visionOS 27.3 (beta).

About the code: every sample uses one fictional app, Dinewell, which books restaurant tables. The samples follow the signatures in Apple’s 27.3 beta documentation, but they’re illustrations, not drop-in code: the APIs are in preview and may change, and types like Reservations and Availability stand in for your own model layer.

1. Pick which actions become tools

A tool is an app intent Siri can create, configure, and run. Apple’s rule of thumb: the best tools are actions a person can ask for in one sentence, with parameters Siri can fill in from that sentence. For Dinewell, that gives:

  • Find available tables: “Find a table for four outside tonight.”
  • Book a table, for restaurants that don’t take a deposit: “Book Luca for 7:30 tomorrow.”
  • Change party size: “Make it six.” With a Live Activity showing the reservation, this works without naming the app.

“What time is my dinner on Friday?” doesn’t need a tool at all. If reservations are described entities that Siri can find, it can answer from the entity. Apple lists three kinds of intent that make poor tools:

  • Widget configuration intents, which don’t perform an action (Dinewell’s “next reservation” widget).
  • Intents that only make sense as one step inside a shortcut (a “set reminder lead time” step).
  • Intents with side effects a person needs to start explicitly, such as placing an order and paying for it. Paying a deposit stays in the app.

Apple hasn’t published the Siri AI terms and usage policies yet, so revisit anything that touches money once it does.

2. Turn intents into tools

Add the @AppIntent(.tool) macro to a new or existing intent. It adds conformance to ToolIntent, and that’s what makes the intent available to Siri:

// Before: an intent Dinewell already uses for an App Shortcut.
struct ChangePartySize: AppIntent {
    // ...
}

// After: the macro adds ToolIntent conformance.
@AppIntent(.tool)
struct ChangePartySize: AppIntent {
    // ...
}

Schemas need care. Some app schema domains (Books, Browser, Files, Journaling and others) work only in Shortcuts. For an intent on one of those schemas, replace @AppIntent(schema:) with @AppIntent(.tool, schema:). Schemas in primary domains, such as messages, already work with Siri, and adding .tool to one is a build error:

// Dinewell's dining journal adopts a Shortcuts-only schema, so it can become a tool.
@AppIntent(.tool, schema: .journal.createEntry)
struct LogMealIntent {
    // The schema's properties stay as they were.
}

// Chat with the restaurant adopts a schema Siri already supports.
// Adding .tool here is a build error, so leave it as it is.
@AppIntent(schema: .messages.sendMessage)
struct MessageRestaurantIntent {
    // ...
}

3. Describe intents and parameters

Siri decides which tool to call, and how to fill it in, from text you write. Every tool needs a title that says what the action is, a description that says what it does, and a title and description on each @Parameter:

@AppIntent(.tool)
struct BookTable: AppIntent {
    static let title: LocalizedStringResource = "Book a Table"
    static let description = IntentDescription(
        "Reserves a table at a Dinewell restaurant for a party size and arrival time. Free bookings only: restaurants that need a card deposit are booked in the app."
    )

    @Parameter(title: "Restaurant", description: "The restaurant to book")
    var restaurant: RestaurantEntity

    @Parameter(title: "Guests", description: "Total number of guests, including the person booking, from 1 to 12")
    var partySize: Int

    @Parameter(title: "Arrival Time", description: "When the party arrives, in the restaurant's local time zone, on the hour or half hour")
    var time: Date

    func perform() async throws -> some ReturnsValue<ReservationEntity> {
        let booking = try await Reservations.shared.book(restaurant.id, guests: partySize, at: time)
        return .result(value: ReservationEntity(booking))
    }
}
  • Write each description for someone who has never opened your app.
  • Spell out formats, ranges, and units: “from 1 to 12”, “in the restaurant’s local time zone”, “a 3-letter airport code”.
  • Say what a tool doesn’t do when two tools sound alike. Dinewell’s search tool says it doesn’t book anything, so Siri doesn’t confuse it with Book a Table.
  • Check intents you already use in App Shortcuts, Spotlight, or the Shortcuts app. Some of this metadata may already be there.

4. Describe entities and protect sensitive data

Entities are your app’s content, and Siri reads them the same way it reads intents. Pass a description to every @Property, @ComputedProperty, and @DeferredProperty. Then decide what Siri may say on a locked phone, and what it should never see:

struct ReservationEntity: AppEntity {
    static let typeDisplayRepresentation: TypeDisplayRepresentation = "Reservation"
    static let defaultQuery = ReservationQuery()

    // Default for the whole entity: Siri can mention a booking on a locked phone.
    static let authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed

    let id: UUID

    @Property(title: "Restaurant", description: "Name of the restaurant the table is booked at")
    var restaurantName: String

    @Property(title: "Guests", description: "Total number of guests, including the person who booked")
    var partySize: Int

    @Property(title: "Arrival", description: "Booked arrival time, in the restaurant's local time zone")
    var arrival: Date

    // Allergies and celebrations are personal, so ask for authentication first.
    @Property(
        title: "Special Requests",
        description: "Notes for the restaurant, such as allergies or a birthday",
        authenticationPolicy: .requiresAuthentication
    )
    var specialRequests: String

    // Derived from other values, so it never goes stale.
    @ComputedProperty(title: "Is Upcoming", description: "True if the arrival time is still in the future")
    var isUpcoming: Bool { arrival > .now }

    // Fetched live from the restaurant, only when it's needed.
    @DeferredProperty(title: "Table Status", description: "Whether the table is ready, or the expected wait in minutes")
    var tableStatus: String {
        get async { await TableStatus.fetch(for: id) }
    }

    // Only Dinewell and its widgets read this. Siri and Shortcuts never see it.
    @Property(title: "POS Ticket", visibility: .internal)
    var posTicketID: String

    var displayRepresentation: DisplayRepresentation {
        "\(restaurantName), \(partySize) guests"
    }

    // init(_ booking: Booking) fills these from Dinewell's own model.
}
  • By default, entities don’t require authentication. Set a static authenticationPolicy on the entity for a default, and pass authenticationPolicy: to a property macro to override it for one property.
  • Property-level policies only cover what Siri says in its responses. If an intent returns sensitive data as its result, set the policy on the intent too.
  • visibility: .internal limits a property to your app and its extensions. Don’t combine it with a Spotlight indexingKey or customIndexingKey, which would make the property discoverable again.

5. Let Siri create entities from the request

Some entities can be built entirely from what the person says. Mark a TransientAppEntity with @AppEntity(.generable) and Siri fills its properties straight from the request, without calling your queries. “Find a table for four outside in the West Village after 7 tonight in Dinewell” becomes one TableSearch:

enum Seating: String, AppEnum {
    case any, indoor, outdoor, bar

    static let typeDisplayRepresentation: TypeDisplayRepresentation = "Seating"
    static let caseDisplayRepresentations: [Seating: DisplayRepresentation] = [
        .any: "No preference", .indoor: "Indoors", .outdoor: "Outdoors", .bar: "At the bar",
    ]
}

@AppEntity(.generable)
struct TableSearch: TransientAppEntity {
    static let typeDisplayRepresentation: TypeDisplayRepresentation = "Table Search"

    @Property(title: "Guests", description: "Number of guests, from 1 to 12")
    var partySize: Int

    @Property(title: "Earliest Arrival", description: "The earliest time the party can arrive")
    var earliest: Date

    @Property(title: "Neighborhood", description: "Neighborhood or street to search near, or empty for near the person")
    var neighborhood: String

    @Property(title: "Seating", description: "Where the party wants to sit")
    var seating: Seating

    var displayRepresentation: DisplayRepresentation {
        "Table for \(partySize)"
    }

    init() {}
}

@AppIntent(.tool)
struct FindAvailableTables: AppIntent {
    static let title: LocalizedStringResource = "Find Available Tables"
    static let description = IntentDescription(
        "Lists Dinewell restaurants with a free table that matches the search. Doesn't book anything."
    )

    @Parameter(title: "Search", description: "Party size, time, area and seating to search for")
    var search: TableSearch

    func perform() async throws -> some ReturnsValue<[RestaurantEntity]> {
        .result(value: try await Availability.search(search))
    }
}

Every property of a generable entity has to be a plain value such as Int, String, or Date, an AppEnum, another generable entity, or an array of these. Anything else is a compile error. Parameters that use ordinary, non-generable entities still go through your entity queries.

6. Make content findable and contextual

Siri has to find an entity before it can use it. The main route is Spotlight: conform the entity to IndexedEntity, map key properties to Spotlight attributes, and index the entities in a named index (Apple reserves the default index for prototyping):

struct RestaurantEntity: IndexedEntity {
    static let typeDisplayRepresentation: TypeDisplayRepresentation = "Restaurant"
    static let defaultQuery = RestaurantQuery()   // also adopts IndexedEntityQuery for reindexing

    let id: String               // for example "luca-soho"
    var restaurant: Restaurant   // Dinewell's own model

    @ComputedProperty(title: "Name", description: "The restaurant's name as shown on its sign")
    var name: String { restaurant.name }

    @ComputedProperty(
        title: "About",
        description: "Cuisine, price range and neighborhood, in one sentence",
        indexingKey: \.contentDescription
    )
    var about: String { restaurant.summary }

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(name)", subtitle: "\(about)")
    }
}

// Re-index whenever favorites or bookings change.
try await CSSearchableIndex(name: "Dinewell_Restaurants")
    .indexAppEntities(favoriteRestaurants)
  • Apple’s reference says a @DeferredProperty isn’t included when the system indexes an IndexedEntity. Put anything Siri needs to search on in a @Property or @ComputedProperty.
  • Content you don’t index can still be found through an EnumerableEntityQuery, or offered as parameter options through a DynamicOptionsProvider.
  • Add an OpenIntent for each indexed entity type, so tapping a result opens the right screen instead of just launching the app.

Three more signals help Siri understand requests. Onscreen context lets people say “book this one”: attach the entity to the view that shows it. Donations are behavioral cues that help Siri choose between similar intents: donate an intent whenever someone performs that action in your own UI. Transferable representations let Siri pass your entities to other apps:

// Onscreen context: "book this one for 8 p.m."
RestaurantDetailView(restaurant: restaurant)
    .appEntityIdentifier(
        EntityIdentifier(for: RestaurantEntity.self, identifier: restaurant.id)
    )

// Behavioral cue: someone just booked through Dinewell's own screens.
var intent = BookTable()
intent.restaurant = restaurant
intent.partySize = partySize
intent.time = time
try await intent.donate()

// Cross-app: "send my Friday booking to Sam in Messages".
extension ReservationEntity: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        ProxyRepresentation(exporting: \.shareText)   // "Luca, Fri 7:30 PM, 4 guests"
    }
}

7. Show results in card snippets

When Siri shows one of your entities, it can use a card you design in SwiftUI instead of a plain title and subtitle. Adopt SnippetRepresentableEntity and return a CardSnippet:

extension ReservationEntity: SnippetRepresentableEntity {
    static var snippet: some EntitySnippetContent<Self> {
        CardSnippet(supportedFamilies: [.standard]) { reservation, _ in
            ReservationCard(reservation: reservation)
        }
    }
}

struct ReservationCard: View {
    let reservation: ReservationEntity

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Label("Dinewell", image: "DinewellMark")   // bundled asset, not a network image
                .font(.caption.bold())
                .foregroundStyle(.secondary)
            Text(reservation.restaurantName)
                .font(.title2.bold())
            Text("\(reservation.partySize) guests · \(reservation.arrival.formatted(date: .abbreviated, time: .shortened))")
        }
        .padding()
    }
}
  • The system renders cards ahead of time into a static archive it can show even when your app isn’t running, so the content can’t change afterward.
  • Use mostly static content such as text and images, and prepare any data synchronously before you return the view. Data you load asynchronously probably won’t arrive in time.
  • If Siri asks for a family you don’t support, it falls back to the entity’s display representation, so keep that readable too.
  • Buttons and toggles that run an app intent are allowed. After the intent runs, the system renders the card again.

8. Add app skills for complex tasks

Siri often calls just one tool. For multi-step requests, an app skill tells it how your tools fit together, which one to prefer, and what to avoid. Add a Skills Bundle to your app target (File > New > File from Template, then Skills Bundle in the Resource section, keeping the name Skills), and give each skill its own folder:

Skills.skillsbundle/
└── Skills/
    └── plan-group-dinner/
        └── SKILL.md
---
name: plan-group-dinner
description: Finds and books a table for a group dinner in Dinewell when the person gives a group size, a rough time, and preferences such as seating or neighborhood.
---
Search before you book. Use the find available tables tool first, and
only use the book a table tool after the person picks a restaurant
and a time from the results.

For groups larger than 12, don't book. Explain that Dinewell restaurants
take large parties by phone, and offer to open the restaurant's page.

Never offer to pay a deposit. Deposits are handled in the app.
  • The frontmatter takes only name and description. The name is 1 to 64 lowercase letters, digits, and hyphens, with no leading, trailing, or double hyphens, and it must match the folder name.
  • The description is required, fits on one line, and is at most 1,024 characters. Siri reads only the description to decide whether a skill applies, so make it specific.
  • The body is free-form Markdown of up to 10,000 characters. Refer to tools by what they do (“the find available tables tool”), not by type name, and don’t repeat what your titles and descriptions already say.
  • Each skill folder contains only SKILL.md: no scripts or reference files. A target can have one .skillsbundle, and it has to be in the Copy Bundle Resources phase. Skill names must be unique across your app, its extensions, and embedded frameworks.
  • Skills aren’t deterministic. They’re guidance, and Siri doesn’t run the steps in order, so don’t rely on one to enforce a flow.

9. Test with Apple’s tools

Each layer catches problems the others miss:

  • App Intents Testing runs your intents out of process, the way Siri and Shortcuts do. Use it to check that perform() returns the right result.
  • The Shortcuts app shows whether your intents appear under your app and whether parameter summaries read naturally.
  • Spotlight on a physical device shows whether your entities are findable, and whether tapping one opens the right screen. Indexing in Simulator doesn’t always match a device.
  • The Siri Integration Advisor in Xcode gives feedback and suggestions on your App Intents implementation. Apple’s documentation describes it only briefly so far.
  • Siri Evaluations scripts conversations with Siri and checks what Siri does in each turn.

In Siri Evaluations, a SiriScenario is a conversation made of UserTurns. Each turn can expect intent calls (Calls), entities found by search (Finds), entities on screen (OnScreen), skills loaded (Loads), and text in the reply (SiriResponse). Here are Dinewell’s booking scenarios:

let dinewell = IntentDefinitions(bundleIdentifier: "com.example.dinewell")
let findTables = dinewell.intents["FindAvailableTables"]
let bookTable = dinewell.intents["BookTable"]
let changePartySize = dinewell.intents["ChangePartySize"]
let restaurant = dinewell.entities["RestaurantEntity"]
let reservation = dinewell.entities["ReservationEntity"]
let groupDinner = dinewell.skills["plan-group-dinner"]

let bookingScenarios = SiriScenarioLoader {
    // A direct request, then a follow-up that doesn't name the app.
    SiriScenario {
        UserTurn("Book a table for four at Luca tomorrow at 7:30 in Dinewell") {
            Calls {
                bookTable.expect(
                    restaurant: restaurant.expectIdentifier("luca-soho"),
                    partySize: .matches(#Predicate<Int> { $0 == 4 })
                )
            }
        }
        UserTurn("Actually, make it six") {
            Calls {
                changePartySize.expect(partySize: .matches(#Predicate<Int> { $0 == 6 }))
            }
        }
    }

    // A question should look things up, never book.
    SiriScenario {
        UserTurn("Any tables for two outside tonight in Dinewell?") {
            Calls { findTables }
            Calls(.disallowed) { bookTable }
        }
    }

    // Content Siri has to find, not an action.
    SiriScenario {
        UserTurn("What time is my dinner on Friday in Dinewell?") {
            Finds { reservation }
        }
    }

    // A vague, multi-step request: the skill should load, and nothing gets booked yet.
    SiriScenario {
        UserTurn("Plan dinner for eight near Union Square on Saturday in Dinewell") {
            Loads(.required) { groupDinner }
            Calls { findTables }
            Calls(.disallowed) { bookTable }
        }
    }
}

// Needs a different starting state, so it gets its own evaluation and test.
// A UI test opens Luca's page with XCUIAutomation first.
let thisRestaurant = SiriScenario {
    UserTurn("Book this place for two at 8") {
        OnScreen { restaurant.expectIdentifier("luca-soho") }
        Calls { bookTable.expect(restaurant: restaurant.expectIdentifier("luca-soho")) }
    }
}

Run the scenarios through an evaluation from a test:

struct DinewellEvaluation: Evaluation {
    let name: String
    let dataset: SiriScenarioLoader

    func subject(from sample: SiriScenario) async throws -> SiriScenarioSubject {
        try await SiriService.run(scenario: sample)
    }

    var evaluators: Evaluators { SiriExpectationEvaluator() }

    func aggregateMetrics(using aggregator: inout MetricsAggregator) {
        Self.computeSiriMetrics(into: &aggregator)
    }
}

@Test(.evaluates(DinewellEvaluation(name: "Booking requests", dataset: bookingScenarios)))
func bookingRequests() async throws {
    let result = EvaluationContext.current.result
    // Replies vary between runs, so gate on a pass rate and run the suite several times.
    #expect(result.aggregateValue(.mean(of: .intentsAllPass)) >= 0.9)
}
  • Start with a few direct requests. Then add the indirect, incomplete, and unexpected wording real people use.
  • Because what Siri does can vary between runs, run the set more than once. Improve your intents, parameter names, descriptions, and skills until more of it passes, consistently. Xcode shows a report of what happened in each turn.
  • Calls only checks that the listed calls happen. It doesn’t catch extra ones, so add Calls(.disallowed) for actions Siri must not take.
  • Loads proves a skill loaded, not that Siri followed it. Pair it with Calls for the tools the skill chains.
  • Put evaluations in a UI testing target, turn on Developer Mode and Enable UI Automation (Settings > Developer), and sign the app with the same development team as the test target.
  • Test skills on a device, not in Simulator. Try requests with your app in the foreground, and with your app named while it’s in the background.

Siri Evaluations is new and in preview. The test code above is assembled from Apple’s reference pages, so check the imports and names against the SDK you build with.

React Native and Expo apps

Apple’s documentation doesn’t mention React Native, Expo, or any other cross-platform framework. What follows is SwiftSoft’s guidance, based on how these APIs are built, not Apple’s.

  • Tools, entities, and card snippets are Swift macros, protocols, and SwiftUI views compiled into the iOS app target, and the skills bundle is an Xcode resource in that target. So they’re written in Swift, in your native iOS project, not in JavaScript.
  • For Expo, that means a development build or prebuild, with a config plugin or a local module, so your Swift files and Skills.skillsbundle survive each prebuild. Expo Go can’t load custom native code.
  • The hard part is usually data, not syntax. Intents run natively, often without the JavaScript runtime, so whatever they act on (bookings, carts, favorites) has to be reachable from Swift: shared native storage, a native API client, or a bridge that wakes the JavaScript side. Decide this intent by intent.
  • Flutter and Ionic apps follow the same pattern: the Siri pieces are Swift in the app’s Xcode project.

FAQ

Does my app need an update to work with Siri AI?+

Yes. Siri can only find content and perform actions that an app makes explicitly available through App Intents, which are native code in your app. Mark your key intents as tools with @AppIntent(.tool) and describe them, their parameters, and your entities. Intents that adopt a schema Siri already supports, such as sending a message, already work and shouldn’t be marked as tools.

Do people have to say my app’s name?+

Only when your app is in the background. While your app is in the foreground, has an active Live Activity, or came up earlier in the same conversation in the Siri app, people can just ask. Apple’s ToolIntent documentation adds that Siri can call your tools when one of your entities appears in a search result, which is another reason to index your content in Spotlight.

Can I ship Siri AI features to customers now?+

Not yet. The features work in development builds and for internal TestFlight testers. A beta-SDK build with the code can reach external TestFlight testers, but the Siri AI features don’t work for them, and the App Store doesn’t accept beta-SDK builds, until Apple ships the features to customers, which Apple says will be in a few months. App Store submission opens in the spring.

Do React Native and Expo apps work with Siri AI?+

Yes, with native code. Apple’s docs don’t cover cross-platform frameworks, so this is our guidance: tools, entities, and card snippets are Swift in the iOS project. Expo apps need a development build or prebuild with a config plugin or local module, not Expo Go. The real work is making the data your intents need reachable from Swift without the JavaScript runtime.

Can SwiftSoft do this for us, and what does it cost?+

Yes. It starts at $1,999 and can be delivered in as little as 3 days: your app’s top 3 actions as tools Siri can call, one content type indexed in Spotlight with a branded result card, privacy controls, a Siri Evaluations suite, and an internal TestFlight build. The readiness audit is free, and the spring launch re-check is included: we rebuild on Apple’s final SDK, re-run the evaluations, fix API changes, and submit. We work with React Native, Expo, SwiftUI, UIKit, Flutter, and Ionic apps.

Apple, iPhone, iPad, Siri, Apple Intelligence and Xcode are trademarks of Apple Inc., registered in the U.S. and other countries and regions. SwiftSoft is an independent developer and is not affiliated with, sponsored, or endorsed by Apple Inc.

Discover if Swiftsoft is Your Perfect Fit

Schedule an informative call with our experts to discuss your unique needs and discover how Swiftsoft can elevate your service.

PlansContactClient loginPortfolioServicesTerms & conditionsPrivacy policy