relevancekit — an installable skill for AI agents, published by dpearson2699/swift-ios-skills.
RelevanceKit
Provide on-device contextual clues that increase a widget's visibility in the
Smart Stack on Apple Watch. RelevanceKit tells the system when a widget is
relevant -- by time, location, fitness state, sleep schedule, or connected
hardware -- so the Smart Stack can surface the right widget at the right moment.
Targets Swift 6.3 / watchOS 26+.
Beta-sensitive. RelevanceKit shipped with watchOS 26. Re-check Apple
documentation before making strong claims about API availability or behavior.
See references/relevancekit-patterns.md for complete code patterns including
relevant widgets, timeline provider integration, grouping, previews, and
permission handling.
Contents
Overview
Setup
Relevance Providers
Time-Based Relevance
Location-Based Relevance
Fitness and Sleep Relevance
Hardware Relevance
Combining Signals
Widget Integration
Common Mistakes
Review Checklist
References
Overview
watchOS uses two mechanisms to determine widget relevance in the Smart Stack:
Timeline provider relevance -- implement relevance() on an existing
AppIntentTimelineProvider to attach RelevantContext clues to timeline
entries. Available across platforms; only watchOS acts on the data.
Relevant widget -- use RelevanceConfiguration with a
RelevanceEntriesProvider to build a widget driven entirely by relevance
clues. The system creates individual Smart Stack cards per relevant entry.
watchOS 26+ only.
Choose a timeline provider when the widget always has data to show and relevance
is supplementary. Choose a relevant widget when the widget should only appear
when conditions match, or when multiple cards should appear simultaneously (e.g.,
several upcoming calendar events).
Key Types
Type
Module
Role
RelevantContext
RelevanceKit
A contextual clue (date, location, fitness, sleep, hardware)
WidgetRelevance
WidgetKit
Collection of relevance attributes for a widget kind
WidgetRelevanceAttribute
WidgetKit
Pairs a widget configuration with a RelevantContext
WidgetRelevanceGroup
WidgetKit
Controls grouping behavior in the Smart Stack
RelevanceConfiguration
WidgetKit
Widget configuration driven by relevance clues (watchOS 26+)
RelevanceEntriesProvider
WidgetKit
Provides entries for a relevance-configured widget (watchOS 26+)
RelevanceEntry
WidgetKit
Data needed to render one relevant widget card (watchOS 26+)
Setup
Import
import RelevanceKit
import WidgetKit
Platform Availability
RelevantContext is declared across platforms (iOS 17+, watchOS 10+), but
RelevanceKit functionality only takes effect on watchOS. Calling the API on
other platforms has no effect. RelevanceConfiguration, RelevanceEntriesProvider,
and RelevanceEntry are watchOS 26+ only.
Permissions
Certain relevance clues require the user to grant permission to both the app
and the widget extension:
Clue
Required Permission
.location(inferred:)
Location access
.location(_:) (CLRegion)
Location access
.location(category:)
Location access
.fitness(_:)
HealthKit workout/activity rings permission
.sleep(_:)
HealthKit sleepAnalysis permission
.hardware(headphones:)
None
.date(...)
None
Request permissions in both the main app target and the widget extension target.
Relevance Providers
Option 1: Timeline Provider with Relevance
Add a relevance() method to an existing AppIntentTimelineProvider. This
approach shares code across iOS and watchOS while adding watchOS Smart Stack
intelligence.
struct MyProvider: AppIntentTimelineProvider {
// ... snapshot, timeline, placeholder ...
func relevance() async -> WidgetRelevance<MyWidgetIntent> {
let attributes = events.map { event in
let context = RelevantContext.date(
from: event.startDate,
to: event.endDate
)
return WidgetRelevanceAttribute(
configuration: MyWidgetIntent(event: event),
context: context
)
}
return WidgetRelevance(attributes)
}
}
Option 2: RelevanceEntriesProvider (watchOS 26+)
Build a widget that only appears when conditions match. The system calls
relevance() to learn when the widget matters, then calls entry() with
the matching configuration to get render data.
struct MyRelevanceProvider: RelevanceEntriesProvider {
func relevance() async -> WidgetRelevance<MyWidgetIntent> {
let attributes = events.map { event in
WidgetRelevanceAttribute(
configuration: MyWidgetIntent(event: event),
context: RelevantContext.date(event.date, kind: .scheduled)
)
}
return WidgetRelevance(attributes)
}
func entry(
configuration: MyWidgetIntent,
context: Context
) async throws -> MyRelevanceEntry {
if context.isPreview {
return .preview
}
return MyRelevanceEntry(event: configuration.event)
}
func placeholder(context: Context) -> MyRelevanceEntry {
.placeholder
}
}
Time-Based Relevance
Time clues tell the system a widget matters at or around a specific moment.
Single Date
RelevantContext.date(eventDate)
Date with Kind
DateKind provides an additional hint about the nature of the time relevance:
Kind
Use
.default
General time relevance
.scheduled
A scheduled event (meeting, flight)
.informational
Information relevant around a time (weather forecast)
RelevantContext.date(meetingStart, kind: .scheduled)
Date Range
// Using from/to
RelevantContext.date(from: startDate, to: endDate)
// Using DateInterval
RelevantContext.date(interval: dateInterval, kind: .scheduled)
// Using ClosedRange
RelevantContext.date(range: startDate...endDate, kind: .default)
Location-Based Relevance
Inferred Locations
The system infers certain locations from a person's routine. No coordinates
needed.
RelevantContext.location(inferred: .home)
RelevantContext.location(inferred: .work)
RelevantContext.location(inferred: .school)
RelevantContext.location(inferred: .commute)
Requires location permission in both the app and widget extension.
Specific Region
import CoreLocation
let region = CLCircularRegion(
center: CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090),
radius: 500,
identifier: "apple-park"
)
RelevantContext.location(region)
Point-of-Interest Category (watchOS 26+)
Indicate relevance near any location of a given category. Returns nil if the
category is unsupported.
import MapKit
if let context = RelevantContext.location(category: .beach) {
// Widget is relevant whenever the person is near a beach
}
Fitness and Sleep Relevance
Fitness
// Relevant when activity rings are incomplete
RelevantContext.fitness(.activityRingsIncomplete)
// Relevant during an active workout
RelevantContext.fitness(.workoutActive)
Requires HealthKit workout/activity permission.
Sleep
// Relevant around bedtime
RelevantContext.sleep(.bedtime)
// Relevant around wakeup
RelevantContext.sleep(.wakeup)
Requires HealthKit sleepAnalysis permission.
Hardware Relevance
// Relevant when headphones are connected
RelevantContext.hardware(headphones: .connected)
No special permission required.
Combining Signals
Return multiple WidgetRelevanceAttribute values in the WidgetRelevance
array to make a widget relevant under several different conditions.
func relevance() async -> WidgetRelevance<MyIntent> {
var attributes: [WidgetRelevanceAttribute<MyIntent>] = []
// Relevant during morning commute
attributes.append(
WidgetRelevanceAttribute(
configuration: MyIntent(mode: .commute),
context: .location(inferred: .commute)
)
)
// Relevant at work
attributes.append(
WidgetRelevanceAttribute(
configuration: MyIntent(mode: .work),
context: .location(inferred: .work)
)
)
// Relevant around a scheduled event
for event in upcomingEvents {
attributes.append(
WidgetRelevanceAttribute(
configuration: MyIntent(eventID: event.id),
context: .date(event.date, kind: .scheduled)
)
)
}
return WidgetRelevance(attributes)
}
Order matters. Return relevance attributes ordered by priority. The system
may use only a subset of the provided relevances.
Widget Integration
Relevant Widget with RelevanceConfiguration
@available(watchOS 26, *)
struct MyRelevantWidget: Widget {
var body: some WidgetConfiguration {
RelevanceConfiguration(
kind: "com.example.relevant-events",
provider: MyRelevanceProvider()
) { entry in
EventWidgetView(entry: entry)
}
.configurationDisplayName("Events")
.description("Shows upcoming events when relevant")
}
}
Associating with a Timeline Widget
When both a timeline widget and a relevant widget show the same data, use
associatedKind to prevent duplicate cards. The system replaces the timeline
widget card with relevant widget cards when they are suggested.
RelevanceConfiguration(
kind: "com.example.relevant-events",
provider: MyRelevanceProvider()
) { entry in
EventWidgetView(entry: entry)
}
.associatedKind("com.example.timeline-events")
Grouping
WidgetRelevanceGroup controls how the system groups widgets in the Smart Stack.
// Opt out of default per-app grouping so each card appears independently
WidgetRelevanceAttribute(
configuration: intent,
group: .ungrouped
)
// Named group -- only one widget from the group appears at a time
WidgetRelevanceAttribute(
configuration: intent,
group: .named("weather-alerts")
)
// Default system grouping
WidgetRelevanceAttribute(
configuration: intent,
group: .automatic
)
RelevantIntent (Timeline Provider Path)
When using a timeline provider, also update RelevantIntentManager so the
system has relevance data between timeline refreshes.
import AppIntents
func updateRelevantIntents() async {
let intents = events.map { event in
RelevantIntent(
MyWidgetIntent(event: event),
widgetKind: "com.example.events",
relevance: RelevantContext.date(from: event.start, to: event.end)
)
}
try? await RelevantIntentManager.shared.updateRelevantIntents(intents)
}
Call this whenever relevance data changes -- not only during timeline refreshes.
Previewing Relevant Widgets
Use Xcode previews to verify appearance without simulating real conditions.
// Preview with sample entries
#Preview("Events", widget: MyRelevantWidget.self, relevanceEntries: {
[EventEntry(event: .surfing), EventEntry(event: .meditation)]
})
// Preview with relevance configurations
#Preview("Relevance", widget: MyRelevantWidget.self, relevance: {
WidgetRelevance([
WidgetRelevanceAttribute(configuration: MyIntent(event: .surfing),
context: .date(Date(), kind: .scheduled))
])
})
// Preview with the full provider
#Preview("Provider", widget: MyRelevantWidget.self,
relevanceProvider: MyRelevanceProvider())
Testing
Enable WidgetKit Developer Mode in Settings > Developer on the watch to
bypass Smart Stack rotation limits during development.
Common Mistakes
Ignoring return order. The system may only use a subset of relevance
attributes. Return them sorted by priority (most important first).
Missing permissions in widget extension. Location, fitness, and sleep
clues require permission in both the app and the widget extension. If only
the app has permission, the clues are silently ignored.
Using RelevanceKit API expecting iOS behavior. The API compiles on all
platforms but only has effect on watchOS.
Duplicate Smart Stack cards. When offering both a timeline widget and a
relevant widget for the same data, use .associatedKind(_:) to prevent
duplication.
Forgetting placeholder and preview entries. RelevanceEntriesProvider
requires both placeholder(context:) and a preview branch in
entry(configuration:context:) when context.isPreview is true.
Not calling updateRelevantIntents. When using timeline providers,
calling this only inside timeline() means the system has stale relevance
data between refreshes. Update whenever data changes.
Ignoring nil from location(category:). This factory returns an optional.
Not all MKPointOfInterestCategory values are supported.
Review Checklist
import RelevanceKit is present alongside import WidgetKit
RelevantContext clues match the app's actual data model
Relevance attributes are ordered by priority
Permissions requested in both app target and widget extension for location/fitness/sleep clues
RelevanceEntriesProvider implements entry, placeholder, and relevance
context.isPreview handled in entry(configuration:context:) to return preview data
.associatedKind(_:) used when a timeline widget and relevant widget show the same data
RelevantIntentManager.updateRelevantIntents called when data changes (timeline provider path)
location(category:) nil return handled
WidgetKit Developer Mode used for testing
Widget previews verify appearance across display sizes
References
references/relevancekit-patterns.md -- extended patterns, full provider
implementations, permission handling, and grouping strategies
RelevanceKit documentation
RelevantContext
Increasing the visibility of widgets in Smart Stacks
RelevanceConfiguration
RelevanceEntriesProvider
What's new in watchOS 26 (WWDC25 session 334)don't have the plugin yet? install it then click "run inline in claude" again.
added explicit inputs with external connection details, decision points for permission and api return handling, structured procedure with input/output per step, edge cases for optional returns and stale data, and outcome signals tied to system behavior.
relevancekit provides on-device contextual signals that tell watchOS when a widget is relevant, so the Smart Stack surfaces the right widget at the right moment. use it when you need widgets to appear based on time, location, fitness state, sleep schedule, or connected hardware. targets swift 6.3 and watchos 26+. the api compiles across platforms but only affects watchos. this is beta-sensitive, so re-check apple documentation before making strong claims about api availability.
imports required
import RelevanceKitimport WidgetKitexternal connections
environment setup
edge cases to account for
RelevanceConfiguration and RelevanceEntriesProvider are watchos 26+ only; guard with @available(watchOS 26, *).RelevantContext.location(category:) returns optional; not all MKPointOfInterestCategory values are supported.updateRelevantIntents is called.input: xcode project with app and widget extension targets.
RelevanceKit and WidgetKit in both targets.CLLocationManager.requestWhenInUseAuthorization() or requestAlwaysAndWhenInUseAuthorization() in the app.HKHealthStore.requestAuthorizationToShare().output: both targets compile with no missing import warnings; permissions appear in settings on the watch.
input: widget data model and use case (supplementary relevance vs. standalone relevant widget).
if the widget always has data to show and relevance is supplementary:
relevance() on an existing AppIntentTimelineProvider. relevance is cross-platform; only watchos acts on it.if the widget should only appear when conditions match, or multiple cards should appear simultaneously:
RelevanceEntriesProvider). watchos 26+ only. implement relevance() to return conditions and entry(configuration:context:) to fetch render data for matching conditions.output: decision made; provider skeleton added to the widget bundle.
input: app data (events, locations, fitness/sleep state) and chosen provider pattern.
time-based clues:
RelevantContext.date(eventDate) for a single date.RelevantContext.date(eventDate, kind: .scheduled) for events (meetings, flights). other kinds: .default, .informational.RelevantContext.date(from: startDate, to: endDate) for a date range.RelevantContext.date(interval: dateInterval, kind: .scheduled) using a dateinterval.RelevantContext.date(range: startDate...endDate, kind: .default) using a closed range.location-based clues:
RelevantContext.location(inferred: .home) for system-inferred locations. other options: .work, .school, .commute. requires location permission.RelevantContext.location(region) where region is a CLCircularRegion. requires location permission.RelevantContext.location(category: .beach) for any point-of-interest category (watchos 26+). returns optional; check for nil. requires location permission.fitness and sleep clues:
RelevantContext.fitness(.activityRingsIncomplete) when rings are not closed. requires healthkit workout/activity permission.RelevantContext.fitness(.workoutActive) during an active workout. requires healthkit workout permission.RelevantContext.sleep(.bedtime) around bedtime. requires healthkit sleepanalysis permission.RelevantContext.sleep(.wakeup) around wakeup. requires healthkit sleepanalysis permission.hardware clues:
RelevantContext.hardware(headphones: .connected) when headphones are connected. no permission needed.
for timeline providers: map each data item (event, location check, fitness state) to a WidgetRelevanceAttribute with a configuration intent and a RelevantContext clue.
for RelevanceEntriesProvider: do the same in the relevance() method.
return attributes in priority order (most important first). the system may discard lower-priority ones.
output: WidgetRelevance object containing ordered WidgetRelevanceAttribute array.
input: widget relevance attributes and grouping strategy.
group: .automatic (default) to group by app.group: .ungrouped to let each card appear independently.group: .named("identifier") to create a named group where only one widget from the group shows at a time.output: updated WidgetRelevanceAttribute with group parameter set.
input: matched configuration and context from the system.
entry(configuration:context:) to fetch and return render data for the given configuration.context.isPreview and return preview data if true.placeholder(context:) to return a placeholder entry shown while loading.output: RelevanceEntry (or custom entry type) with data ready for the widget view.
input: provider implementation and widget view.
@available(watchOS 26, *)
struct MyRelevantWidget: Widget {
var body: some WidgetConfiguration {
RelevanceConfiguration(
kind: "com.example.relevant-events",
provider: MyRelevanceProvider()
) { entry in
EventWidgetView(entry: entry)
}
.configurationDisplayName("Events")
.description("Shows upcoming events when relevant")
}
}
.associatedKind("com.example.timeline-events") to prevent duplicate cards in the smart stack.output: widget bundle includes RelevanceConfiguration with provider and entry view.
input: app data changes (events added/removed, user moves, etc.).
RelevantIntentManager.shared.updateRelevantIntents(intents) whenever data changes, not only during timeline refreshes.RelevantIntent with configuration, widget kind, and relevance context.func updateRelevantIntents() async {
let intents = events.map { event in
RelevantIntent(
MyWidgetIntent(event: event),
widgetKind: "com.example.events",
relevance: RelevantContext.date(from: event.start, to: event.end)
)
}
try? await RelevantIntentManager.shared.updateRelevantIntents(intents)
}
output: system has fresh relevance data between timeline refreshes.
input: widget implementation and sample data.
#Preview("name", widget: MyWidget.self, relevanceEntries: { [...] }) or relevanceProvider: MyProvider().output: widget displays correctly in smart stack under relevant conditions.
if the user has not granted location permission:
if the user has not granted healthkit permission:
if RelevantContext.location(category:) returns nil:
.home, .work) or a circular region, or remove the location clue.if the widget must appear on ios as well as watchos:
if the widget should only appear on watchos and show multiple simultaneous cards:
@available(watchOS 26, *).if both a timeline widget and a relevant widget show the same data:
.associatedKind(_:) on the relevant widget to replace the timeline card in the smart stack instead of duplicating it.if the system discards low-priority relevance attributes:
for timeline providers with relevance:
relevance() returns a WidgetRelevance<MyIntent> containing zero or more WidgetRelevanceAttribute values.RelevantContext clue and optional group.RelevantIntentManager.shared.updateRelevantIntents() is called whenever data changes; accepts an array of RelevantIntent objects.for RelevanceEntriesProvider:
relevance() returns a WidgetRelevance<MyIntent> with attributes in priority order.entry(configuration:context:) returns a RelevanceEntry (or custom type conforming to Decodable and Hashable).placeholder(context:) returns a placeholder entry.entry() to populate it.widget kind slug:
RelevanceConfiguration(kind:) or used in timeline provider."com.example.relevant-events".file locations:
MyApp.app/PlugIns/MyAppWidgets.appex/~/Library/Developer/Xcode/DerivedData/.entry() (RelevanceEntriesProvider) or the timeline entry (timeline provider with relevance).