CustomWorkout, IntervalBlock and WorkoutScheduler in Swift
Last verified October 3, 2026 · 5 min read
Covered here:HealthKit
This page is the code. The product question (whether a coaching program belongs in Apple's Workout app at all, and how to sequence preview, scheduling and your own watch app) is answered in WorkoutKit scheduled workouts. Everything below is the WorkoutKit API surface as Apple's reference documents it, read on 2026-10-03.
The type tree, from the leaves up#
Apple's reference describes each piece in one line, and together they read like a grammar for an interval session:
| Type | Apple's description | How you create it |
|---|---|---|
WorkoutGoal | "A value that specifies the goal for a workout" | Cases including .open, .distance(_:_:), .energy(_:_:), .time(_:_:) |
WorkoutAlert | "An alert that notifies the user of significant events during a workout" | Concrete types such as HeartRateZoneAlert(zone:) |
WorkoutStep | "A step in a workout" | init(goal:alert:), both parameters defaulted |
IntervalStep | "An interval that represents a work or recovery step in a workout" | init(_:goal:alert:) or init(_:step:) with .work or .recovery |
IntervalBlock | "Blocks of work and recovery steps that repeat in a custom workout" | init(steps:iterations:) |
CustomWorkout | "A workout that includes a repeating series of work and recovery steps" | init(activity:location:displayName:warmup:blocks:cooldown:) |
The goal is what ends a step. Apple's parameter note on WorkoutStep says so directly: the goal is "A goal that determines when the step ends." A step with .open has no end condition of its own. The goal cases carry Foundation units: .distance takes a UnitLength, .time takes a UnitDuration.
IntervalStep.Purpose has two cases, .work and .recovery. Apple documents no third value, so a coaching model that distinguishes "easy", "steady" and "rest" has to map onto those two plus the goal and alert you attach.
A minimal interval workout#
This uses only initializers and cases from Apple's reference. Six repeats of 400 metres with a heart-rate zone alert, 90 seconds of recovery, a timed warmup and cooldown:
import WorkoutKit
import HealthKit
let work = IntervalStep(.work,
goal: .distance(400, .meters),
alert: HeartRateZoneAlert(zone: 4))
let recovery = IntervalStep(.recovery, goal: .time(90, .seconds))
let repeats = IntervalBlock(steps: [work, recovery], iterations: 6)
let workout = CustomWorkout(activity: .running,
location: .outdoor,
displayName: "6 x 400 m",
warmup: WorkoutStep(goal: .time(10, .minutes)),
blocks: [repeats],
cooldown: WorkoutStep(goal: .time(5, .minutes)))
let plan = WorkoutPlan(.custom(workout))
activity is an HKWorkoutActivityType and location an HKWorkoutSessionLocationType, the same HealthKit enums a workout session uses; the full activity list is on HealthKit workout activity types. WorkoutPlan(_:id:) defaults id to a fresh UUID(). Pass your own when the plan has an identity in your backend, so you can find it again among scheduled workouts.
Check support before you build#
CustomWorkout exposes three static checks: supportsActivity(_:), supportsGoal(_:activity:location:) and supportsAlert(_:activity:location:). Apple describes supportsGoal as returning "a Boolean value that indicates whether the system supports the specified goal for the given activity type and location". Run them when you translate your own program format into WorkoutKit types, not after a user taps schedule:
let ok = CustomWorkout.supportsGoal(.distance(400, .meters),
activity: .running,
location: .outdoor)
&& CustomWorkout.supportsAlert(HeartRateZoneAlert(zone: 4),
activity: .running,
location: .outdoor)
Our judgement: treat a false here as "this session cannot be represented faithfully" and keep it in your own app, rather than silently dropping the alert and scheduling something the coach did not write.
Previewing and opening#
There are two documented ways to show a plan before the user runs it. On iPhone, SwiftUI's workoutPreview(_:isPresented:) modifier "Presents a preview of the workout contents as a modal sheet"; Apple lists it from iOS 17.0 and watchOS 11.0. On the watch, WorkoutPlan.openInWorkoutApp() "Opens the workout in Workout on Apple Watch". It is declared async throws, and its availability metadata names watchOS 10.0 and no iOS version, so do not plan an iPhone button around it.
Scheduling and managing the schedule#
WorkoutScheduler is a final class with a shared instance. Everything that touches the schedule is async:
let scheduler = WorkoutScheduler.shared
guard WorkoutScheduler.isSupported else { return }
if await scheduler.authorizationState != .authorized {
let state = await scheduler.requestAuthorization()
guard state == .authorized else { return }
}
var when = DateComponents()
when.year = 2026; when.month = 10; when.day = 6
when.hour = 7; when.minute = 0
await scheduler.schedule(plan, at: when)
let upcoming = await scheduler.scheduledWorkouts // [ScheduledWorkoutPlan]
Details from the reference that shape real code:
requestAuthorization()returns aWorkoutScheduler.AuthorizationState:.authorized,.denied,.notDeterminedor.restricted. Unlike HealthKit read access, this state is reported, so you can show an honest "scheduling is off" screen.schedule(_:at:)takesDateComponents, not aDate, and isasyncwithoutthrows.scheduledWorkoutsis "An array of all the workouts scheduled by your app", each aScheduledWorkoutPlancarryingplan,dateandcomplete.maxAllowedScheduledWorkoutCountis a staticIntdescribed as "The maximum number of workouts your app can schedule." Apple does not print the number on the page, so read it at runtime and schedule a rolling window, not a whole season.markComplete(_:at:),remove(_:at:)andremoveAllWorkouts()maintain the schedule.removetakes the plan and the sameDateComponents, which is a reason to persist exactly what you scheduled.
Apple's WorkoutKit sample describes the round trip: a planned workout "appears at the top of the Workout list on the watch", and after it is run and the sample's list is refreshed, "A green checkmark appears next to the completed workout." How the completion is surfaced to your code beyond the complete property is not spelled out on the reference pages we read, so verify against the sample before you build on it.
Availability, symbol by symbol#
The framework root lists iOS, iPadOS and Mac Catalyst 17.0 with watchOS 10.0. The individual types used above (CustomWorkout, WorkoutPlan, WorkoutScheduler, IntervalBlock and the rest) also list macOS 15.0, and their Mac Catalyst line reads 18.0 rather than 17.0. If you ship a Catalyst build, take the higher number from the symbol pages, not the framework page.
Where the live session lives#
A scheduled composition runs in Apple's Workout app, not in yours. If you need your own in-session screens, the live side is the HKWorkoutSession lifecycle, and the architecture around it is the anatomy of a watchOS workout app.
Frequently asked questions
- Does WorkoutScheduler.schedule take a Date or DateComponents?
- DateComponents. Apple declares it as schedule(_ workout: WorkoutPlan, at: DateComponents) async, and describes the parameter as date components that represent the workout's starting time. The same type appears in ScheduledWorkoutPlan.date and in remove(_:at:) and markComplete(_:at:), which both take the plan plus the DateComponents it was scheduled at. Persist exactly what you scheduled so you can remove or complete it later without reconstructing the components from a Date and getting a time zone or field mismatch.
- How many workouts can one app schedule with WorkoutKit?
- Apple exposes the limit as WorkoutScheduler.maxAllowedScheduledWorkoutCount, a static Int described as the maximum number of workouts your app can schedule, but the reference page does not state the number. Read it at runtime instead of hard-coding one. Our recommendation is to schedule a rolling window of upcoming sessions from your backend's plan and top it up, rather than trying to push an entire training block, so a plan change touches only a few scheduled entries.
- Can an iPhone app call openInWorkoutApp on a WorkoutPlan?
- Apple's availability metadata for WorkoutPlan.openInWorkoutApp() lists watchOS 10.0 and no iOS version, and describes it as opening the workout in Workout on Apple Watch. On iPhone the documented preview path is SwiftUI's workoutPreview(_:isPresented:) modifier, listed from iOS 17.0, which presents the workout contents as a modal sheet, and the documented delivery path is WorkoutScheduler. Design the iPhone flow around preview and scheduling, and keep the open-now action in your watch app.
- What are the two purposes an IntervalStep can have?
- Apple's IntervalStep.Purpose enum has two cases: work, described as a work step, and recovery, described as a recovery step. Each IntervalStep pairs one purpose with a WorkoutStep, either through init(_:step:) or the convenience init(_:goal:alert:). If your coaching model has more step kinds than that, express the difference through the goal and alert you attach, and check each with CustomWorkout.supportsGoal and supportsAlert before scheduling.
Keep reading
Elsewhere on the site
Pages that share this one’s concepts and sources, from other sections.
Next steps
Was this page useful?
Independent comparison, last reviewed October 3, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.
← All watch apps · by AIFitnessAPI