Developing Custom Intents for Siri
Custom Intents are the next level after basic Siri Shortcuts integration. Instead of simply "open this screen," the user gets a full dialog: Siri clarifies parameters, processes the command in the background, returns the result by voice. All of this works without opening the app through INExtension.
Custom Intent Structure
The .intentdefinition file is the central element. Xcode generates Swift code from it: Intent classes, Response, parameters with types. Parameter structure:
Parameter: date
Type: Date (INDateComponentsResolutionResult)
Display Name: "date"
Prompt: "What date?"
Siri Dialog: "What date should I schedule the meeting?"
Parameter types: String, Integer, Boolean, Date, CLPlacemark (geolocation), INPerson (contact from address book), custom INObject types for your app entities.
Custom INObject is needed when the parameter is an entity from your database (project, task, track):
// Auto-generated from .intentdefinition
class ProjectObject: INObject {
// identifier and displayString are mandatory
}
// In IntentHandler
func provideProjectOptionsCollection(
for intent: CreateTaskIntent,
with completion: @escaping (INObjectCollection<ProjectObject>?, Error?) -> Void
) {
let projects = ProjectRepository().fetchAll()
let objects = projects.map {
ProjectObject(identifier: $0.id, display: $0.name)
}
completion(INObjectCollection(items: objects), nil)
}
Siri will show a list of projects to choose from — either the user will say the name, or Siri will ask for clarification.
Resolve, Confirm, Handle
Intent lifecycle has three methods:
Resolve — validation and clarification of each parameter:
func resolveTaskName(for intent: CreateTaskIntent,
with completion: @escaping (INStringResolutionResult) -> Void) {
guard let name = intent.taskName, name.count >= 2 else {
completion(.needsValue()) // Siri: "What to name the task?"
return
}
guard name.count <= 255 else {
completion(.unsupported(forReason: .tooLong))
return
}
completion(.success(with: name))
}
Confirm — final check before execution:
func confirm(intent: CreateTaskIntent,
completion: @escaping (CreateTaskIntentResponse) -> Void) {
guard ProjectRepository().exists(id: intent.project?.identifier) else {
let response = CreateTaskIntentResponse(code: .failure, userActivity: nil)
response.failureReason = "Project not found"
completion(response)
return
}
completion(CreateTaskIntentResponse(code: .ready, userActivity: nil))
}
Handle — execution:
func handle(intent: CreateTaskIntent,
completion: @escaping (CreateTaskIntentResponse) -> Void) {
let store = TaskStore(appGroup: "group.com.yourapp")
let task = store.create(
name: intent.taskName!,
projectId: intent.project?.identifier,
dueDate: intent.dueDate?.dateComponents
)
let response = CreateTaskIntentResponse(code: .success, userActivity: nil)
response.task = TaskObject(identifier: task.id, display: task.name)
completion(response)
}
Response templates are set in .intentdefinition: Siri will say "Task "$(taskName)" created in project "$(project)"" with actual value substitution.
Integration with Shortcuts App
INShortcut + INVoiceShortcutCenter allow adding a shortcut directly from the app without going to Settings:
func addToSiri(intent: CreateTaskIntent) {
let shortcut = INShortcut(intent: intent)
let vc = INUIAddVoiceShortcutViewController(shortcut: shortcut)
vc.delegate = self
present(vc, animated: true)
}
"Add to Siri" button with native view — standard pattern for onboarding.
Integration with Widgets Through AppIntents (iOS 16+)
Since iOS 16, custom Intents migrated to the new AppIntents framework. Old SiriKit Intents (.intentdefinition) continue to work, but for configurable widgets through Shortcuts you need AppIntent:
struct CreateTaskAppIntent: AppIntent {
static var title: LocalizedStringResource = "Create Task"
@Parameter(title: "Task Name")
var taskName: String
@Parameter(title: "Project")
var project: ProjectEntity?
func perform() async throws -> some IntentResult & ProvidesDialog {
let task = try await TaskService().create(name: taskName, project: project?.id)
return .result(dialog: "Task \(task.name) created")
}
}
AppIntents is the future of SiriKit on iOS 16+. Less boilerplate, Swift concurrency support, works in widgets, Spotlight, Shortcuts app.
Typical Problems
Extension is not called — the most common issue. Causes: Intent not added to NSExtension.NSExtensionAttributes.IntentsSupported in extension Info.plist; or Intent added to main target but not extension target.
App Group not passed through — Extension and main app run in different sandboxes. Share data via App Group: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.yourapp") or UserDefaults(suiteName: "group.com.yourapp").
Siri doesn't understand domain-specific terms — INVocabulary.shared().setVocabularyStrings(projectNames, of: .organizationName) and global AppIntentVocabulary.plist for static terms.
Timeline Benchmarks
| Task | Timeline |
|---|---|
| 1 custom Intent (simple parameters) | 1–2 days |
| Intent with custom INObject + resolve dialog | 2–3 days |
| Migration to AppIntents (iOS 16+) + widget configuration | 3–5 days |







