Skip to content
Docs menu

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.

MethodVerified
deleteWorkoutlive-verified
downloadWorkoutlive-verified
getAdaptiveWorkoutlive-verified
getNextScheduledWorkoutlive-verified
getScheduledWorkoutByIdlive-verified
getScheduledWorkoutslive-verified
getScheduledWorkoutSummarieslive-verified
getTrainingPlanWorkoutslive-verified
getWorkoutByIdlive-verified
getWorkoutslive-verified
pushWorkoutToDevicelive-verified
scheduleWorkoutlive-verified
unscheduleWorkoutlive-verified
updateWorkoutlive-verified
uploadCyclingWorkoutlive-verified
uploadRunningWorkoutlive-verified
uploadStrengthWorkoutlive-verified
uploadSwimmingWorkoutlive-verified
uploadWorkoutlive-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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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:

FieldTypeAlways present
calendarItemsCalendarItem[]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:

FieldTypeAlways present
scheduledWorkoutId`numbernull`
workoutId`numbernull`
workoutUuid`stringnull`
workoutName`stringnull`
workoutType`stringnull`
scheduleDatestringyes
trainingPlanId`numbernull`
tpType`stringnull`
workoutPhrase`stringnull`
estimatedDurationInSecs`numbernull`
estimatedDistanceInMeters`numbernull`
tpPlanName`stringnull`
itpPlanId`numbernull`
atpPlanId`numbernull`
fbtAdaptivePlanId`numbernull`
selfGuidedPlanId`numbernull`
associatedActivityId`numbernull`
isRestDay`booleannull`
racebooleanno

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:

FieldTypeAlways present
trainingPlanIdnumberyes
planNamestringyes
trainingPlanClassificationstringyes
trainingPlanDetailsDTO{yes
athletePlanIdnumberno
athleteRace`{ raceDay?: stringnull; raceName?: string
workoutsPerWeeknumberno
registrationDatestringno
trainingTypestringno
workoutScheduleSummariesScheduledWorkoutSummary[]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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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:

FieldTypeAlways present
messageIdnumberyes
messageTypestringyes
messageStatusstringyes
deviceIdnumberyes
deviceNamestringyes
fileTypestringyes
messageUrlstringyes
messageNamestringyes
prioritynumberyes
metaDataIdnumberyes
wifiSetupbooleanyes
hiddenbooleanyes
applicationKey`stringnull`
firmwareVersion`stringnull`
deviceXmlDataType`stringnull`
createdTimeStamp`stringnull`
updatedTimeStamp`stringnull`
uniqueIdentifier`stringnull`
groupName`stringnull`
appDetailsunknownyes

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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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:

FieldTypeAlways present
workoutIdnumberno
workoutNamestringno

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

Methods in this category