---
title: "CustomWorkout, IntervalBlock and WorkoutScheduler in Swift"
canonical: "https://aifitnessapi.com/watch-apps/workoutkit-customworkout-swift"
cluster: "Watch Apps"
primary_query: "workoutkit customworkout intervalblock swift"
last_reviewed: "2026-10-03"
description: "Build a WorkoutKit CustomWorkout from WorkoutStep, IntervalStep and IntervalBlock, wrap it in a WorkoutPlan, then schedule, list and remove it."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"CustomWorkout, IntervalBlock and WorkoutScheduler in Swift\", AIFitnessAPI, https://aifitnessapi.com/watch-apps/workoutkit-customworkout-swift"
---

# CustomWorkout, IntervalBlock and WorkoutScheduler in Swift

> A WorkoutKit interval session is built bottom-up: WorkoutStep and IntervalStep each carry a WorkoutGoal and an optional WorkoutAlert, IntervalBlock repeats an array of interval steps a given number of iterations, and CustomWorkout(activity:location:displayName:warmup:blocks:cooldown:) assembles warmup, blocks and cooldown. You wrap the result in WorkoutPlan(.custom(workout)) and either hand it to the Workout app or pass it to WorkoutScheduler.shared, whose schedule(_:at:) takes DateComponents. Apple's metadata lists these types from iOS 17.0 and watchOS 10.0, with macOS 15.0 and Mac Catalyst 18.0 on the individual symbols, and lists openInWorkoutApp() for watchOS only. Check CustomWorkout.supportsGoal and supportsAlert before you build a plan, and read WorkoutScheduler.maxAllowedScheduledWorkoutCount at runtime rather than hard-coding a limit.

- Canonical: https://aifitnessapi.com/watch-apps/workoutkit-customworkout-swift
- Last reviewed: 2026-10-03
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "CustomWorkout, IntervalBlock and WorkoutScheduler in Swift", AIFitnessAPI, https://aifitnessapi.com/watch-apps/workoutkit-customworkout-swift

---

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](/watch-apps/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:

```swift
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](/healthkit/workout-activities). `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:

```swift
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`:

```swift
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 a `WorkoutScheduler.AuthorizationState`: `.authorized`, `.denied`, `.notDetermined` or `.restricted`. Unlike HealthKit read access, this state is reported, so you can show an honest "scheduling is off" screen.
- `schedule(_:at:)` takes `DateComponents`, not a `Date`, and is `async` without `throws`.
- `scheduledWorkouts` is "An array of all the workouts scheduled by your app", each a `ScheduledWorkoutPlan` carrying `plan`, `date` and `complete`.
- `maxAllowedScheduledWorkoutCount` is a static `Int` described 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:)` and `removeAllWorkouts()` maintain the schedule. `remove` takes the plan and the same `DateComponents`, 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](/watch-apps/hkworkoutsession-lifecycle-swift), and the architecture around it is [the anatomy of a watchOS workout app](/watch-apps/watchos-workout-app-anatomy).

## FAQ

### 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.

[Permalink](https://aifitnessapi.com/watch-apps/workoutkit-customworkout-swift#faq-1)

### 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.

[Permalink](https://aifitnessapi.com/watch-apps/workoutkit-customworkout-swift#faq-2)

### 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.

[Permalink](https://aifitnessapi.com/watch-apps/workoutkit-customworkout-swift#faq-3)

### 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.

[Permalink](https://aifitnessapi.com/watch-apps/workoutkit-customworkout-swift#faq-4)
