Workouts
Workout CRUD, per-sport upload helpers, scheduling, and pushing to a device. For building the workout JSON itself, see WORKOUTS.md — buildWorkout is far easier than hand-writing it.
Every method below hangs off a Garmin instance. See Installation & setup for how to construct one:
import { GarminClient, Garmin, FileTokenStore } from "garminconnect-js";
const client = new GarminClient({ tokenStore: new FileTokenStore("./tokens") });
if (!(await client.loadTokens())) throw new Error("not connected to Garmin");
const garmin = new Garmin(client);
19 methods. The verification column says what has been
confirmed against a live Garmin account, not merely unit-tested —
AGENTS.md carries the full evidence per method.
| Method | Verified |
|---|---|
deleteWorkout | live-verified |
downloadWorkout | live-verified |
getAdaptiveWorkout | live-verified |
getNextScheduledWorkout | live-verified |
getScheduledWorkoutById | live-verified |
getScheduledWorkouts | live-verified |
getScheduledWorkoutSummaries | live-verified |
getTrainingPlanWorkouts | live-verified |
getWorkoutById | live-verified |
getWorkouts | live-verified |
pushWorkoutToDevice | live-verified |
scheduleWorkout | live-verified |
unscheduleWorkout | live-verified |
updateWorkout | live-verified |
uploadCyclingWorkout | live-verified |
uploadRunningWorkout | live-verified |
uploadStrengthWorkout | live-verified |
uploadSwimmingWorkout | live-verified |
uploadWorkout | live-verified |
deleteWorkout
garmin.deleteWorkout(workoutId: number | string): Promise<unknown>
const result = await garmin.deleteWorkout(activityId);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
deletes the template from the workout library, irreversible
Verification: live-verified
downloadWorkout
garmin.downloadWorkout(workoutId: number | string): Promise<Buffer>
const result = await garmin.downloadWorkout(activityId);
Returns
A Buffer of file bytes.
FIT-file bytes
Verification: live-verified
getAdaptiveWorkout
garmin.getAdaptiveWorkout(workoutUuid: string): Promise<WorkoutRecord | null>
const result = await garmin.getAdaptiveWorkout(activityId);
Returns
WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
GETs /workout-service/fbt-adaptive/{uuid}: a Garmin Coach workout with segments, steps and estimatedTrainingEffect. Coach workouts have no workoutId, and getWorkoutById cannot fetch them — /workout-service/workout/{uuid} is a 404. Take the uuid from getTrainingPlanWorkouts or getScheduledWorkoutSummaries
Verification: live-verified
getNextScheduledWorkout
garmin.getNextScheduledWorkout(): Promise<CalendarItem | {}>
const result = await garmin.getNextScheduledWorkout();
Returns
CalendarItem | {}
computed from two getScheduledWorkouts calls (this month + next, handling Dec->Jan rollover); returns {} if nothing matches, never throws
Verification: live-verified
getScheduledWorkoutById
garmin.getScheduledWorkoutById(scheduledWorkoutId: number | string): Promise<WorkoutRecord | null>
const result = await garmin.getScheduledWorkoutById(activityId);
Returns
WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
uses a DIFFERENT base (/workout-service/schedule) than getScheduledWorkouts (/calendar-service); passes through unchecked
Verification: live-verified
getScheduledWorkouts
garmin.getScheduledWorkouts(year: number | string, month: number | string): Promise<CalendarMonth | null>
const result = await garmin.getScheduledWorkouts(activityId, activityId);
Returns
CalendarMonth:
| Field | Type | Always present |
|---|---|---|
calendarItems | CalendarItem[] | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
month is 1-12 on the way in, converted to 0-indexed on the wire; validates year>=2000, month 1-12; passes through unchecked
Verification: live-verified
getScheduledWorkoutSummaries
garmin.getScheduledWorkoutSummaries(startdate: string | Date, enddate: string | Date): Promise<ScheduledWorkoutSummary[]>
const result = await garmin.getScheduledWorkoutSummaries("2026-09-24", "2026-09-24");
Returns
An array of ScheduledWorkoutSummary:
| Field | Type | Always present |
|---|---|---|
scheduledWorkoutId | `number | null` |
workoutId | `number | null` |
workoutUuid | `string | null` |
workoutName | `string | null` |
workoutType | `string | null` |
scheduleDate | string | yes |
trainingPlanId | `number | null` |
tpType | `string | null` |
workoutPhrase | `string | null` |
estimatedDurationInSecs | `number | null` |
estimatedDistanceInMeters | `number | null` |
tpPlanName | `string | null` |
itpPlanId | `number | null` |
atpPlanId | `number | null` |
fbtAdaptivePlanId | `number | null` |
selfGuidedPlanId | `number | null` |
associatedActivityId | `number | null` |
isRestDay | `boolean | null` |
race | boolean | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
POSTs the fixed GraphQL query workoutScheduleSummariesScalar(startDate, endDate): every scheduled workout in the range, plan or self-scheduled (tpType: null), as compact rows whose scheduledWorkoutId is what unscheduleWorkout takes — null for a Garmin Coach workout, which also has tpType: null like a self-scheduled one (tell them apart by fbtAdaptivePlanId). Dates are validated before they are spliced into the query. A GraphQL error arrives as HTTP 200 with errors, turned into a GarminError. It LAGS writes: a just-scheduled workout is in the month feed at once but absent here for a few seconds
Verification: live-verified
getTrainingPlanWorkouts
garmin.getTrainingPlanWorkouts(calendarDate: string | Date, options?: {firstDayOfWeek?: "monday" | "sunday", lang?: string}): Promise<TrainingPlanWorkoutSchedule[]>
const result = await garmin.getTrainingPlanWorkouts("2026-09-24");
Returns
An array of TrainingPlanWorkoutSchedule:
| Field | Type | Always present |
|---|---|---|
trainingPlanId | number | yes |
planName | string | yes |
trainingPlanClassification | string | yes |
trainingPlanDetailsDTO | { | yes |
athletePlanId | number | no |
athleteRace | `{ raceDay?: string | null; raceName?: string |
workoutsPerWeek | number | no |
registrationDate | string | no |
trainingType | string | no |
workoutScheduleSummaries | ScheduledWorkoutSummary[] | yes |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
POSTs GraphQL trainingPlanScalar(calendarDate, lang, firstDayOfWeek) — all three arguments are REQUIRED by Garmin — and unwraps trainingPlanWorkoutScheduleDTOS: one entry per enrolled plan, {trainingPlanId, planName, trainingPlanClassification, trainingPlanDetailsDTO, workoutScheduleSummaries}. Garmin picks the window (18 workouts across several weeks for an ITP plan); [] with no plan
Verification: live-verified
getWorkoutById
garmin.getWorkoutById(workoutId: number | string): Promise<WorkoutRecord | null>
const result = await garmin.getWorkoutById(activityId);
Returns
WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
passes through unchecked
Verification: live-verified
getWorkouts
garmin.getWorkouts(start?: number, limit?: number): Promise<WorkoutRecord[] | null>
const result = await garmin.getWorkouts();
Returns
An array of WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
defaults start=0, limit=100; passes through unchecked, stays nullable (does NOT coalesce to [])
Verification: live-verified
pushWorkoutToDevice
garmin.pushWorkoutToDevice(workoutId?: number | string, deviceId?: number | string): Promise<DeviceMessage[] | null>
const result = await garmin.pushWorkoutToDevice();
Returns
An array of DeviceMessage:
| Field | Type | Always present |
|---|---|---|
messageId | number | yes |
messageType | string | yes |
messageStatus | string | yes |
deviceId | number | yes |
deviceName | string | yes |
fileType | string | yes |
messageUrl | string | yes |
messageName | string | yes |
priority | number | yes |
metaDataId | number | yes |
wifiSetup | boolean | yes |
hidden | boolean | yes |
applicationKey | `string | null` |
firmwareVersion | `string | null` |
deviceXmlDataType | `string | null` |
createdTimeStamp | `string | null` |
updatedTimeStamp | `string | null` |
uniqueIdentifier | `string | null` |
groupName | `string | null` |
appDetails | unknown | yes |
multi-call: resolves a missing deviceId via /device-service/deviceservice/mylastused's userDeviceId, a missing workoutId via getWorkouts(0,1)'s first result (throws if none), then reads getWorkoutById(workoutId).workoutName for the push message on the final POST
Verification: live-verified
scheduleWorkout
garmin.scheduleWorkout(workoutId: number | string, dateStr: string | Date): Promise<WorkoutRecord | null>
const result = await garmin.scheduleWorkout(activityId, "2026-09-24");
Returns
WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
dateStr routed through formatDate
Verification: live-verified
unscheduleWorkout
garmin.unscheduleWorkout(scheduledWorkoutId: number | string): Promise<unknown>
const result = await garmin.unscheduleWorkout(activityId);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
removes the calendar entry without deleting the workout template; irreversible
Verification: live-verified
updateWorkout
garmin.updateWorkout(workoutId: number | string, workoutJson: Record<string, unknown> | string): Promise<WorkoutRecord | null>
const result = await garmin.updateWorkout(activityId, "workoutJson");
Returns
WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
full-replace PUT; forces workoutId into the body to match the path id; unlike uploadWorkout, a string must resolve to an object, not an array
Verification: live-verified
uploadCyclingWorkout
garmin.uploadCyclingWorkout(workout: WorkoutInput): Promise<WorkoutRecord | null>
const result = await garmin.uploadCyclingWorkout(payload);
Returns
WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
same pattern, default sportType {sportTypeId:2, sportTypeKey:"cycling", displayOrder:2}
Verification: live-verified
uploadRunningWorkout
garmin.uploadRunningWorkout(workout: WorkoutInput): Promise<WorkoutRecord | null>
const result = await garmin.uploadRunningWorkout(payload);
Returns
WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
fills the default running sportType ({sportTypeId:1, sportTypeKey:"running", displayOrder:1}) if the caller didn't supply one, then delegates to uploadWorkout; see gotchas for where the body shape came from
Verification: live-verified
uploadStrengthWorkout
garmin.uploadStrengthWorkout(workout: WorkoutInput): Promise<WorkoutRecord | null>
const result = await garmin.uploadStrengthWorkout(payload);
Returns
WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
same pattern, default sportType {sportTypeId:5, sportTypeKey:"strength_training", displayOrder:5}
Verification: live-verified
uploadSwimmingWorkout
garmin.uploadSwimmingWorkout(workout: WorkoutInput): Promise<WorkoutRecord | null>
const result = await garmin.uploadSwimmingWorkout(payload);
Returns
WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
same pattern, default sportType {sportTypeId:4, sportTypeKey:"swimming", displayOrder:3}
Verification: live-verified
uploadWorkout
garmin.uploadWorkout(workoutJson: Record<string, unknown> | unknown[] | string): Promise<WorkoutRecord | null>
const result = await garmin.uploadWorkout("workoutJson");
Returns
WorkoutRecord:
| Field | Type | Always present |
|---|---|---|
workoutId | number | no |
workoutName | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
a string is JSON-parsed (throws GarminError on invalid JSON or a non-object/array result)
Verification: live-verified