Skip to main content

Zwift

@svendowideit/zwiftv2026.10.01.1· 27d agoBEHAVIORSWORKFLOWS
01README

Recommend which Zwift events to enter, from your own ride history.

WHAT IT DOES

Zwift's calendar makes you scan hundreds of events by hand, and it does not know that you are a Cat C rider who normally trains for an hour at 6am. This extension reads your own Zwift history, weights recent rides more heavily than old ones (a configurable 21-day half-life), and ranks the upcoming calendar against that profile. Each event subgroup is scored on ability fit (race category / w/kg pace band), habit fit (the local hour and duration you actually ride), route/event freshness, availability, and a race-vs-ride bias — and every score component is written out with a human-readable reason, so a pick can be audited rather than merely trusted. It also keeps your ride history and the event calendar in swamp so other models can use them. Read-only: it never writes to your Zwift account.

INSTALL

swamp extension pull @svendowideit/zwift

DEPENDENCIES

None. It uses only Deno's built-in fetch and the bundled deno binary that swamp already ships. The workflows auto-create their model instances on first run — no manual 'swamp model create' is needed.

RUN

# Your ride history is private, so the models need a credential stored in
# a vault. Create the vault once, then store the credential in it. This
# example uses the systemd-creds backend (encrypted at rest, no daemon;
# systemd v256+), but any vault type works:
#   https://swamp-club.com/manual/reference/vaults
swamp vault create @svendowideit/systemd-creds zwift-secrets

# Store your Zwift account credentials. Use a refresh token instead of a
# password if you prefer:
#   swamp vault put zwift-secrets ZWIFT_REFRESH_TOKEN
swamp vault put zwift-secrets ZWIFT_USERNAME
swamp vault put zwift-secrets ZWIFT_PASSWORD

# Verify credentials/config and see exactly what is missing (read-only).
swamp model @svendowideit/zwift-rider method run setup zwift-rider

# Sync 120 days of history, derive the ability profile, and fetch 10 days
# of events. Scheduled 06:00, 12:00 and 15:00 local time.
swamp workflow run @svendowideit/zwift-sync

# Rank the upcoming calendar and write the top 12 picks. Scheduled 30
# minutes after each sync.
swamp workflow run @svendowideit/zwift-recommend

# Read the picks, with the reason for each.
swamp data get zwift-recommender current --json \
  | jq '.content.recommendations[] | {rank, score, eventName, reason}'

CONFIGURE

Set optional values on the model instance (recommended — scheduled runs have no --input), or override per run. Common overrides:

# A rider coming back from injury: shorter history, faster decay.
swamp workflow run @svendowideit/zwift-sync --input historyDays=180 --input halfLifeDays=14

# Races only, top 8, two-week window.
swamp workflow run @svendowideit/zwift-recommend \
  --input includeGroupRides=false --input topN=8 --input horizonDays=14

# A rider in another timezone than the host running 'swamp serve'.
swamp workflow run @svendowideit/zwift-sync --input timezone=Australia/Brisbane

See every setting, its default and its current value:

swamp model @svendowideit/zwift-rider method run setup zwift-rider

Scheduled workflows fire in the LOCAL timezone of the 'swamp serve' host. swamp has no per-workflow timezone field, so set TZ in the service environment (e.g. Environment=TZ=Australia/Brisbane) when serving on a server. The models' 'timezone' argument separately controls which local hour a ride or event is bucketed into.

WHAT IT INSTALLS

Nothing on the host — no services, daemons, or webhooks. Two cron triggers are registered while 'swamp serve' runs: zwift-sync at 0 6,12,15 * * * and zwift-recommend at 30 6,12,15 * * *. All state lives in swamp resources; credentials live in the vault (including the rotated refresh token). The sensitive session refresh token is pinned to the zwift-secrets vault (vaultName on the session resource spec), so creating another vault can never redirect it — see the README's 'Vault pinning' section.

02Behaviors3
@svendowideit/zwift-riderv2026.10.01.1zwift_rider.ts

Global Arguments

ArgumentTypeDescription
username?stringZwift username; normally left empty and read from the vault
password?stringZwift password; normally left empty and read from the vault
refreshToken?stringPre-obtained refresh token; normally left empty and read from the vault
apiBasestringZwift REST API base URL
authBasestringZwift Keycloak base URL
timezonestringIANA timezone for bucketing ride start times (e.g. Australia/Brisbane).
historyDaysnumberOnly rides within this many days are kept in the history and habit profile
halfLifeDaysnumberRecency half-life: a ride this many days old counts half as much as one
maxActivitiesnumberMaximum activities to request from the history endpoint
sportenumOnly keep rides of this sport; empty keeps every sport

Resources

activity(infinite)— One completed ride, keyed by Zwift activity id (factory output)
history(infinite)— All recent rides in one resource, so a workflow can pass the whole
profile(infinite)— The athlete's public Zwift profile (normalised units)
ability(infinite)— Derived, recency-decayed ability + habit profile for the recommender
session(infinite)— Cached auth session; the rotated refresh token is stored in the vault
sync(30d)— Summary of the most recent history sync
setup(infinite)— Configuration-readiness report from the setup method
@svendowideit/zwift-eventsv2026.10.01.1zwift_events.ts

Global Arguments

ArgumentTypeDescription
apiBasestringZwift REST API base URL
horizonDaysnumberHow many days ahead to collect events for
maxSeriesnumberMaximum number of event series to expand (each is one extra request)
maxEventsPerSeriesnumberMaximum events to request per series
sportsarraySports to include
eventTypesarrayEvent types to include; races and TTs by default
includePrivatebooleanInclude events flagged private/unlisted
referenceSpeedKphnumberSpeed used to estimate a duration when Zwift does not publish one
userAgentstringUser-Agent header sent to the Zwift calendar

Resources

schedule(infinite)— Every event + subgroup in the requested horizon, ready for scoring
@svendowideit/zwift-recommenderv2026.10.01.1zwift_recommender.ts

Global Arguments

ArgumentTypeDescription
timezonestringIANA timezone used to bucket event start times into local hours. Should
topNnumberHow many recommendations to return
horizonDaysnumberOnly consider events starting within this many days
raceBiasnumberPreference for racing over group rides: 0.5 is neutral, 1 strongly
hourTolerancenumberTolerance, in hours, when matching an event start to the rider's hour
durationTolerancenumberTolerance, in duration buckets, when matching event length to the
repeatWindowDaysnumberA route or event repeated within this many days incurs the repeat penalty
repeatPenaltynumberScore multiplier applied to recently repeated routes/events
maxPerSeriesnumberDiversity cap: at most this many recommendations from the same event
weightsobjectRelative weights of each score component

Resources

recommendations(infinite)— Ranked events for this rider, with per-component scores and reasons
03Workflows2
@svendowideit/zwift-sync4c48a0c6-0b12-4989-bfe9-ba16c1ac831d

Refresh the rider's Zwift history and the upcoming event calendar. Two independent jobs run in parallel: rider-history signs in to Zwift, pulls the athlete profile and recent activities, persists each ride, and derives the decayed ability + habit profile the recommender reads. upcoming-events pulls every race/group event over the next N days from the public calendar, expanding the event series the capped feed referen

rider-historySign in to Zwift, sync ride history, derive the ability profile.
1.sync— Fetch the profile + activities, persist each ride, and write the decayed ability/habit profile. Rotates and stores the refresh token.
upcoming-eventsCollect races and group events over the next horizon days.
1.fetch— Fetch the public event calendar and expand every referenced series, then keep only events inside the horizon.
@svendowideit/zwift-recommend606c09ab-12b8-4100-8b42-c78e36d3afdc

Recommend which Zwift events to enter, from the rider's own history. Consumes the two data sets the zwift-sync workflow produces, wired in with CEL expressions so nothing is re-fetched: ability ← zwift-rider ability-current (decayed w/kg + habit profile) recentRides ← zwift-rider history-current (rides, each with a decay weight) schedule ← zwift-events upcoming (every event in the next 10 days) Each event subgroup is scored on ability fit (race category / w-kg pace

recommendRank the upcoming schedule against the rider's decayed profile.
1.assert-inputs— Fail fast with a clear message when the sync workflow has not produced the ability/history/schedule data this run needs.
2.recommend— Score every event subgroup and write the ranked top N.
3.has-recommendations— Warn (as a recorded assertion) when no event in the window scored.
04Previous Versions1
2026.09.21.1
05Stats
A
93 / 100
Downloads
28
Archive size
64.9 KB
  • Has README or module doc2/2earned
  • README has a code example1/1earned
  • README is substantive1/1earned
  • Most symbols documented1/1earned
  • No slow types (deprecated)1/1earned
  • Dependencies pass trust audit2/2earned
  • Has description1/1earned
  • Platform support declared (or universal)2/2earned
  • License1/2partial

    no SPDX license declared

  • Verified public repository2/2earned
06Platforms