{"$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/transcript.schema.json", "id": "wwdc2026-345", "language": "eng", "source": "https://developer.apple.com/videos/play/wwdc2026/345", "wordCount": 2786, "segments": [{"start": 7.0, "text": "Hi, my name is Moe, an engineer on the App Intents team."}, {"start": 11.0, "text": "I'm excited to share with you, the new App Intents capabilities"}, {"start": 14.0, "text": "we're introducing with our 2027 releases."}, {"start": 18.0, "text": "App Intents is the framework that lets you express your app's actions and content"}, {"start": 21.0, "text": "to other parts of the system"}, {"start": 23.0, "text": "in ways that feel natural and deeply integrated."}, {"start": 27.0, "text": "From Siri and the Shortcuts app to Spotlight and Widgets,"}, {"start": 33.0, "text": "App Intents has been the engine"}, {"start": 35.0, "text": "behind some of the most delightful experiences on Apple platforms."}, {"start": 39.0, "text": "And is now a key pillar of Apple Intelligence."}, {"start": 42.0, "text": "In our 2027 releases, driven by your feature requests,"}, {"start": 46.0, "text": "we're bringing more control, more flexibility,"}, {"start": 49.0, "text": "and a significantly smoother developer experience."}, {"start": 53.0, "text": "Today, we'll explore three areas."}, {"start": 55.0, "text": "We'll start with entities."}, {"start": 57.0, "text": "I'll show you new ways to share them across apps,"}, {"start": 60.0, "text": "tell the system when they're relevant, and process them at scale."}, {"start": 65.0, "text": "Then, I'll walk you through new support for native types"}, {"start": 68.0, "text": "and union values with full Shortcuts integration."}, {"start": 72.0, "text": "And finally, I'll show you how your intents can run longer,"}, {"start": 75.0, "text": "handle cancellation gracefully, and target the right process for execution."}, {"start": 81.0, "text": "I'm going to focus on new features today."}, {"start": 83.0, "text": "But, if you're new to App Intents or need a refresher on the basics,"}, {"start": 87.0, "text": "I'd recommend checking out \"Get to know App Intents\" from WWDC25."}, {"start": 91.0, "text": "That video introduces the Landmarks Travel Tracking app."}, {"start": 94.0, "text": "I'll be building on it with the APIs we cover today."}, {"start": 97.0, "text": "You can download the sample code and follow along."}, {"start": 101.0, "text": "Let's dive into what's new."}, {"start": 102.0, "text": "Your entities, your app's content, like a landmark or a playlist,"}, {"start": 106.0, "text": "live inside your app."}, {"start": 108.0, "text": "But the people using your app don't."}, {"start": 110.0, "text": "They move between apps all the time."}, {"start": 112.0, "text": "Let's go through a pair of examples using Mail and Maps with my travel tracking app."}, {"start": 117.0, "text": "I built a shortcut to share trip ideas with friends."}, {"start": 121.0, "text": "It finds a nearby landmark and sends it along with a message."}, {"start": 124.0, "text": "My entity conforms to transferable from the CoreTransferable framework."}, {"start": 129.0, "text": "So the shortcut can share it in a format Mail can use."}, {"start": 132.0, "text": "And that works great."}, {"start": 134.0, "text": "But what if instead of sending it to a friend,"}, {"start": 137.0, "text": "I want to get directions to that landmark?"}, {"start": 139.0, "text": "Well, that won't work."}, {"start": 141.0, "text": "Maps needs some structured information — a coordinate, an address,"}, {"start": 145.0, "text": "or something it can navigate to."}, {"start": 147.0, "text": "But that kind of data doesn't have an associated data format"}, {"start": 150.0, "text": "that can be put in a file or data."}, {"start": 153.0, "text": "The existing file and data representations"}, {"start": 155.0, "text": "work great for known formats like PDFs or images —"}, {"start": 159.0, "text": "but not for structured types that don't have any."}, {"start": 162.0, "text": "This is where ValueRepresentation comes in."}, {"start": 165.0, "text": "It's a new representation type that lets you share structured types"}, {"start": 169.0, "text": "that the system already understands."}, {"start": 171.0, "text": "Here's my LandmarkEntity — it represents a place in the travel tracking app."}, {"start": 176.0, "text": "It already conforms to Transferable, so I just need to add a ValueRepresentation"}, {"start": 181.0, "text": "alongside any existing representations."}, {"start": 184.0, "text": "Inside my ValueRepresentation,"}, {"start": 186.0, "text": "I export my landmark's coordinate and name,"}, {"start": 189.0, "text": "as a PlaceDescriptor from the GeoToolbox framework."}, {"start": 192.0, "text": "PlaceDescriptor carries coordinates and other metadata"}, {"start": 194.0, "text": "that Maps needs to navigate."}, {"start": 197.0, "text": "If my entity already has a PlaceDescriptor @Property,"}, {"start": 201.0, "text": "I can skip the closure entirely and use a key-path."}, {"start": 204.0, "text": "Same result, much less code."}, {"start": 208.0, "text": "So, going back to my shortcut, I tap run —"}, {"start": 212.0, "text": "my landmark flows to Maps as a PlaceDescriptor,"}, {"start": 215.0, "text": "and Maps opens with directions to the landmark."}, {"start": 219.0, "text": "Your entities now have more ways to travel across apps."}, {"start": 223.0, "text": "Now, let's talk about helping the system suggest them when they are relevant."}, {"start": 227.0, "text": "Suppose you're building a music app like CosmoTunes,"}, {"start": 230.0, "text": "the sample app from the video"}, {"start": 232.0, "text": "\"Explore advanced App Intents features for Siri and Apple Intelligence\"."}, {"start": 237.0, "text": "Your app has a brand new, high-tempo playlist,"}, {"start": 239.0, "text": "that's perfect for running."}, {"start": 241.0, "text": "When someone sets up a running workout in the Fitness app,"}, {"start": 244.0, "text": "they get a list of suggested playlists."}, {"start": 247.0, "text": "How do you get your playlist into those suggestions?"}, {"start": 251.0, "text": "Today, you have two ways to make your content available to the system."}, {"start": 255.0, "text": "The first is to index your content with Spotlight."}, {"start": 258.0, "text": "This makes it available to people searching for your content"}, {"start": 261.0, "text": "in the Spotlight UI, including semantic search."}, {"start": 264.0, "text": "This is also the primary way Siri is able to retrieve your content."}, {"start": 269.0, "text": "The second approach is interaction donation."}, {"start": 272.0, "text": "When people take actions in your app, you donate those interactions to the system"}, {"start": 276.0, "text": "through the IntentDonationManager API."}, {"start": 279.0, "text": "Over time, the system learns patterns"}, {"start": 281.0, "text": "and can suggest similar actions in the future."}, {"start": 284.0, "text": "Siri also uses these interactions to deliver a more personalized experience."}, {"start": 289.0, "text": "But what about that new playlist?"}, {"start": 291.0, "text": "Nobody's searched for it in Spotlight since they don't know it exists."}, {"start": 295.0, "text": "And since nobody's played it, there's no interaction to donate either."}, {"start": 299.0, "text": "You need a way to tell the system this playlist is relevant"}, {"start": 303.0, "text": "so it can surface it at the right moment."}, {"start": 306.0, "text": "Introducing RelevantEntities."}, {"start": 308.0, "text": "With RelevantEntities, you can suggest entities to the system"}, {"start": 312.0, "text": "and provide context about when and why they're relevant."}, {"start": 315.0, "text": "Here's how this works."}, {"start": 318.0, "text": "You start by identifying the relevant entities —"}, {"start": 320.0, "text": "in this case, your running playlists."}, {"start": 323.0, "text": "Next, you create a context to tell the system"}, {"start": 326.0, "text": "these playlists are relevant when someone starts a run."}, {"start": 329.0, "text": "Then you call updateEntities to register them."}, {"start": 333.0, "text": "The system surfaces these playlists as suggestions in the right context —"}, {"start": 337.0, "text": "even if they were never played before."}, {"start": 340.0, "text": "Entities stay registered until you remove them."}, {"start": 343.0, "text": "You can removeAllEntities for a specific context,"}, {"start": 347.0, "text": "remove specific entities from a context,"}, {"start": 350.0, "text": "or clear all your entities across all contexts."}, {"start": 354.0, "text": "Now you have more options for helping people discover your content."}, {"start": 358.0, "text": "How do you choose between them?"}, {"start": 360.0, "text": "Use Spotlight when you want your content to be searchable and retrievable by Siri."}, {"start": 365.0, "text": "Use interaction donation to teach Siri and the system"}, {"start": 369.0, "text": "how people use your app — so it can identify patterns"}, {"start": 371.0, "text": "and suggest actions people may want to repeat."}, {"start": 374.0, "text": "And use RelevantEntities to hint to the system"}, {"start": 377.0, "text": "which content is relevant in specific situations —"}, {"start": 380.0, "text": "so the system can suggest it at the right moment."}, {"start": 384.0, "text": "For more on these topics, check out our new documentation on Spotlight"}, {"start": 387.0, "text": "and interaction donation."}, {"start": 390.0, "text": "Your entities are shared and the system knows when they're relevant."}, {"start": 393.0, "text": "Now, let's make them more efficient."}, {"start": 396.0, "text": "Back in the travel tracking app."}, {"start": 398.0, "text": "The app has landmark photos,"}, {"start": 400.0, "text": "but I also wanted to let people save their own travel photos."}, {"start": 404.0, "text": "So I added a photo album view —"}, {"start": 406.0, "text": "and to make the photos available to the system,"}, {"start": 408.0, "text": "I defined a PhotoEntity with an app schema for photos."}, {"start": 412.0, "text": "This gives the system the context it needs to work with my photos across Siri,"}, {"start": 416.0, "text": "Shortcuts, and Spotlight."}, {"start": 420.0, "text": "I also created an intent to tag my photos by keyword"}, {"start": 423.0, "text": "so people could organize and find them easily."}, {"start": 426.0, "text": "As the photo library grew, I noticed something."}, {"start": 429.0, "text": "Tagging a lot of photos at once was slower than expected."}, {"start": 432.0, "text": "Let's walk through the code to understand why."}, {"start": 436.0, "text": "The intent is very simple, it just adds a keyword to my photos."}, {"start": 440.0, "text": "It takes a list of photo entities and a tag as @Parameter."}, {"start": 445.0, "text": "And the perform method just applies the tag to each photo item."}, {"start": 448.0, "text": "So why was that, actually a problem?"}, {"start": 452.0, "text": "Well, it has to do with how app intents resolve parameters."}, {"start": 457.0, "text": "Before an intent runs, the system resolves every entity."}, {"start": 461.0, "text": "That means calling the entity query to populate all of its properties,"}, {"start": 465.0, "text": "so the intent has everything it may need."}, {"start": 467.0, "text": "For most intents, that's exactly what you want."}, {"start": 470.0, "text": "But in my case, this meant resolving hundreds or thousands of photo entities,"}, {"start": 475.0, "text": "even though my code only needs the entity ID to update my data model."}, {"start": 479.0, "text": "So, how do I fix this?"}, {"start": 481.0, "text": "EntityCollection fixes this."}, {"start": 483.0, "text": "It's a new type that stores an array of entity identifiers,"}, {"start": 487.0, "text": "instead of the fully resolved entities."}, {"start": 489.0, "text": "When you use EntityCollection as your parameter type,"}, {"start": 492.0, "text": "the system passes just the identifiers to the intent's perform method,"}, {"start": 496.0, "text": "without resolving the full entities."}, {"start": 498.0, "text": "Here's the updated code."}, {"start": 500.0, "text": "I changed my @Parameter type to EntityCollection,"}, {"start": 502.0, "text": "and passed the identifiers directly to my tagging method."}, {"start": 506.0, "text": "And that's all it took."}, {"start": 508.0, "text": "To confirm the fix worked,"}, {"start": 510.0, "text": "I built a Shortcut to find and tag 1000 photos."}, {"start": 514.0, "text": "First, with a regular array of photo entities."}, {"start": 520.0, "text": "Then with EntityCollection, which was almost instant."}, {"start": 524.0, "text": "The code change is small,"}, {"start": 525.0, "text": "but the performance difference is significant."}, {"start": 529.0, "text": "Now, what happens when the same entity needs to work on multiple devices?"}, {"start": 535.0, "text": "With our 2027 releases, Siri can continue conversations across devices —"}, {"start": 540.0, "text": "and your entities can be part of those conversations."}, {"start": 543.0, "text": "If your app runs on multiple devices,"}, {"start": 546.0, "text": "people might start a conversation with Siri on one device"}, {"start": 549.0, "text": "and continue on another."}, {"start": 550.0, "text": "But there's a challenge."}, {"start": 552.0, "text": "If I ask Siri on my iPhone to add a photo to an album,"}, {"start": 556.0, "text": "then switch to my other device and ask Siri to tag that photo —"}, {"start": 560.0, "text": "Siri might not be able to find that photo."}, {"start": 562.0, "text": "To understand why, let's think about how entities are identified."}, {"start": 567.0, "text": "Every entity needs an ID, that's how the system finds it."}, {"start": 570.0, "text": "Your entity's ID might be generated locally on each device."}, {"start": 574.0, "text": "Local IDs work great on the device they were created on."}, {"start": 577.0, "text": "But each device generates its own local IDs."}, {"start": 581.0, "text": "So the same entity can end up with a different ID on each device."}, {"start": 585.0, "text": "For Siri to reference your entities across devices,"}, {"start": 588.0, "text": "it needs a stableID that's the same everywhere."}, {"start": 591.0, "text": "That could come from your server, or from CloudKit record IDs."}, {"start": 595.0, "text": "Then, you need a way to tell the system your entity's ID is stable."}, {"start": 599.0, "text": "That's what SyncableEntity does —"}, {"start": 601.0, "text": "it declares to the system that your entity's ID is stable"}, {"start": 605.0, "text": "and can be used across devices."}, {"start": 607.0, "text": "Here's how to adopt it."}, {"start": 609.0, "text": "I start by adding the SyncableEntity protocol to my entity."}, {"start": 613.0, "text": "Then, I need to provide the stable ID."}, {"start": 616.0, "text": "If your entity already uses an ID that's the same across all devices —"}, {"start": 621.0, "text": "like a server-assigned UUID or a CloudKit record ID —"}, {"start": 624.0, "text": "no more change is needed."}, {"start": 626.0, "text": "But if you use local identifiers, like CoreData row IDs, you need both:"}, {"start": 631.0, "text": "a local ID and a stable one."}, {"start": 633.0, "text": "SyncableEntityIdentifier pairs them into a single ID."}, {"start": 637.0, "text": "On-device, your code uses the local ID."}, {"start": 639.0, "text": "And across devices, the system uses the stable one."}, {"start": 643.0, "text": "So far, we've focused on entities."}, {"start": 646.0, "text": "Now, let's talk about the intents that use them."}, {"start": 649.0, "text": "Your intents take parameters — the inputs people provide,"}, {"start": 653.0, "text": "like a date, a name, or an address."}, {"start": 656.0, "text": "When you declare a @Parameter, the system gives you a native picker,"}, {"start": 659.0, "text": "Siri understanding, and localization for free."}, {"start": 662.0, "text": "We're extending that same support to more native types."}, {"start": 666.0, "text": "We're adding native support for Duration, so no more building custom time pickers."}, {"start": 671.0, "text": "And PersonNameComponents for structured name input"}, {"start": 674.0, "text": "instead of a plain string."}, {"start": 676.0, "text": "And more."}, {"start": 679.0, "text": "Each one gets a native picker and works everywhere your intent does —"}, {"start": 682.0, "text": "Siri, Shortcuts and Widgets."}, {"start": 685.0, "text": "Those are individual types — one type per @Parameter."}, {"start": 689.0, "text": "But sometimes a parameter needs to accept more than one type."}, {"start": 693.0, "text": "A union value is a Swift enum where each case wraps a different type,"}, {"start": 698.0, "text": "letting a single parameter represent one of several options."}, {"start": 702.0, "text": "Now that I have both landmarks and travel photos in the app,"}, {"start": 706.0, "text": "I wanted a widget that shows photos from either a photo album"}, {"start": 709.0, "text": "or a landmark collection."}, {"start": 711.0, "text": "With @UnionValue supporting input parameters,"}, {"start": 714.0, "text": "I can use one widget for both."}, {"start": 716.0, "text": "Here's the code."}, {"start": 717.0, "text": "I define my union value as an enum, the @UnionValue macro."}, {"start": 721.0, "text": "And each case wraps a different entity type —"}, {"start": 724.0, "text": "one for landmark collections, and one for photo albums."}, {"start": 728.0, "text": "The macro generates everything the system needs —"}, {"start": 731.0, "text": "type information, case metadata, and picker support."}, {"start": 735.0, "text": "I also configure how each option appears in the picker."}, {"start": 739.0, "text": "typeDisplayRepresentation is the label for the overall type"}, {"start": 742.0, "text": "and caseDisplayRepresentations maps each case to the name shown in the picker."}, {"start": 748.0, "text": "And this isn't limited to Widgets —"}, {"start": 750.0, "text": "@UnionValue parameters work everywhere your intent does,"}, {"start": 754.0, "text": "including the Shortcuts app."}, {"start": 757.0, "text": "To learn more, check out the travel tracking sample code project"}, {"start": 760.0, "text": "and its corresponding article."}, {"start": 763.0, "text": "Everything we've covered so far makes your entities and parameters"}, {"start": 766.0, "text": "more expressive and efficient."}, {"start": 768.0, "text": "Now, let's talk about execution."}, {"start": 772.0, "text": "When your intent runs — from Siri, Shortcuts, or any system surface —"}, {"start": 776.0, "text": "it only has 30 seconds to finish."}, {"start": 779.0, "text": "That works for most everyday actions."}, {"start": 781.0, "text": "But not every intent is that quick."}, {"start": 783.0, "text": "Now that my app supports tagging and organizing photos,"}, {"start": 787.0, "text": "I wanted to let people share their travel photos —"}, {"start": 789.0, "text": "uploading them to a shared album without opening the app."}, {"start": 793.0, "text": "So I created an upload intent"}, {"start": 795.0, "text": "and added a button to my widget to trigger it."}, {"start": 797.0, "text": "But with large photos, the upload takes time —"}, {"start": 800.0, "text": "and the intent kept failing because it couldn't finish"}, {"start": 803.0, "text": "within the 30-second limit."}, {"start": 805.0, "text": "LongRunningIntent fixes this."}, {"start": 807.0, "text": "It lets your intent run beyond the 30-second limit —"}, {"start": 810.0, "text": "and manages the background task lifecycle of your app."}, {"start": 813.0, "text": "And as your intent runs, progress updates appear automatically as a Live Activity."}, {"start": 818.0, "text": "Now, lets check out the code."}, {"start": 821.0, "text": "Here's the intent I wrote to upload my photos."}, {"start": 824.0, "text": "I'm conforming to LongRunningIntent."}, {"start": 827.0, "text": "I take a photo file as input."}, {"start": 830.0, "text": "Then I wrap my work in performBackgroundTask"}, {"start": 832.0, "text": "for extended execution."}, {"start": 835.0, "text": "LongRunningIntent requires the intent to report progress,"}, {"start": 838.0, "text": "so the system knows it's still working and hasn't stalled."}, {"start": 841.0, "text": "And because it builds on ProgressReportingIntent,"}, {"start": 844.0, "text": "I get a built-in progress object to track my work."}, {"start": 847.0, "text": "I calculate the number of chunks for the file and set the total count,"}, {"start": 852.0, "text": "then upload each chunk and update the progress as I go."}, {"start": 858.0, "text": "Here's what happens when my intent runs."}, {"start": 861.0, "text": "It can run longer and there's a stop button right on the Live Activity,"}, {"start": 865.0, "text": "so the person can cancel it at anytime."}, {"start": 868.0, "text": "Though, It'd be great if my intent got a heads-up before being stopped."}, {"start": 873.0, "text": "CancellableIntent lets your intent clean up gracefully when cancelled —"}, {"start": 877.0, "text": "whether the person tapped cancel,"}, {"start": 879.0, "text": "the system timed out or needed to reclaim resources."}, {"start": 883.0, "text": "Here's how I can add cancellation support."}, {"start": 886.0, "text": "I add CancellableIntent"}, {"start": 889.0, "text": "and implement the onCancel handler."}, {"start": 892.0, "text": "When cancellation reason happens,"}, {"start": 894.0, "text": "handler gives me the reason,"}, {"start": 896.0, "text": "and I can use it to cleanup partial uploads"}, {"start": 899.0, "text": "or cancel in-flight requests."}, {"start": 901.0, "text": "LongRunningIntent also supports background GPU access on supported devices —"}, {"start": 906.0, "text": "for tasks like photo processing or on-device inference."}, {"start": 910.0, "text": "Just make sure to add GPU access to your app's entitlement."}, {"start": 914.0, "text": "To learn more about the mechanics of running tasks in the background,"}, {"start": 917.0, "text": "check out this video from WWDC25."}, {"start": 920.0, "text": "So far we've covered how long your intent runs"}, {"start": 923.0, "text": "and what happens when it stops."}, {"start": 925.0, "text": "Let's talk about which process runs it."}, {"start": 928.0, "text": "As your app grows, you may move some intents into a Widget extension,"}, {"start": 932.0, "text": "or an App Intents extension."}, {"start": 934.0, "text": "Lightweight, separate processes that can handle requests without launching your app."}, {"start": 940.0, "text": "You may also create a shared Swift package"}, {"start": 944.0, "text": "or framework where your intents and entities live,"}, {"start": 947.0, "text": "and import it into your app and extensions."}, {"start": 950.0, "text": "In fact, that's exactly what I did with the travel tracking app —"}, {"start": 953.0, "text": "all my intents live in a shared package,"}, {"start": 956.0, "text": "imported by both the main app and the widget extension."}, {"start": 959.0, "text": "When your intents, entities, and queries live in a shared package like this —"}, {"start": 963.0, "text": "linked by your app and extensions —"}, {"start": 965.0, "text": "the system has to decide,"}, {"start": 967.0, "text": "which process runs each intent when a request comes in."}, {"start": 970.0, "text": "It picks a target based on heuristics like"}, {"start": 972.0, "text": "if the app is already running, it prefers the app."}, {"start": 975.0, "text": "and if not, it launches the extension."}, {"start": 978.0, "text": "But sometimes that's not the right choice."}, {"start": 981.0, "text": "For example, I wanted to add a favorite button to my widget"}, {"start": 985.0, "text": "so people can mark a photo as favorite right from the Home Screen."}, {"start": 990.0, "text": "My widget shares the data model with the app —"}, {"start": 992.0, "text": "but having two processes write to the same data store"}, {"start": 996.0, "text": "can cause conflicts."}, {"start": 998.0, "text": "So I gave the widget read-only access and the main app handles all the writes."}, {"start": 1003.0, "text": "When someone taps that button, the intent needs to run in the main app."}, {"start": 1007.0, "text": "ExecutionTargets lets you tell the system"}, {"start": 1010.0, "text": "exactly which process should run your intent."}, {"start": 1013.0, "text": "Here's how."}, {"start": 1015.0, "text": "You can target the main app,"}, {"start": 1017.0, "text": "an appIntentsExtension,"}, {"start": 1020.0, "text": "a widgetKitExtension,"}, {"start": 1022.0, "text": "or any combination."}, {"start": 1023.0, "text": "With ExecutionTargets, you override the system's heuristics"}, {"start": 1027.0, "text": "and control exactly which process handles your intent."}, {"start": 1030.0, "text": "That wraps up the new features I wanted to share."}, {"start": 1033.0, "text": "As next steps: add ValueRepresentation to your entities"}, {"start": 1037.0, "text": "so they can carry structured data across apps."}, {"start": 1041.0, "text": "Register relevant content with the system —"}, {"start": 1043.0, "text": "so it gets surfaced at the right moment."}, {"start": 1046.0, "text": "Adopt EntityCollection to make your intents faster"}, {"start": 1049.0, "text": "when working with large numbers of entities."}, {"start": 1051.0, "text": "And add LongRunningIntent to any intent that needs more than 30 seconds to finish."}, {"start": 1056.0, "text": "To build your app's Siri experience step by step,"}, {"start": 1059.0, "text": "check out \"Code-along: Make your app available to Siri\"."}, {"start": 1063.0, "text": "And to test your intents with the new AppIntentsTesting framework,"}, {"start": 1067.0, "text": "check out \"Validate your App Intents adoption with AppIntentsTesting\"."}, {"start": 1071.0, "text": "I can't wait to see what you build and thanks for watching!"}]}