Zwift
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/zwiftDEPENDENCIES
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/BrisbaneSee every setting, its default and its current value:
swamp model @svendowideit/zwift-rider method run setup zwift-riderScheduled 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.
Global Arguments
| Argument | Type | Description |
|---|---|---|
| username? | string | Zwift username; normally left empty and read from the vault |
| password? | string | Zwift password; normally left empty and read from the vault |
| refreshToken? | string | Pre-obtained refresh token; normally left empty and read from the vault |
| apiBase | string | Zwift REST API base URL |
| authBase | string | Zwift Keycloak base URL |
| timezone | string | IANA timezone for bucketing ride start times (e.g. Australia/Brisbane). |
| historyDays | number | Only rides within this many days are kept in the history and habit profile |
| halfLifeDays | number | Recency half-life: a ride this many days old counts half as much as one |
| maxActivities | number | Maximum activities to request from the history endpoint |
| sport | enum | Only keep rides of this sport; empty keeps every sport |
Resources
Global Arguments
| Argument | Type | Description |
|---|---|---|
| apiBase | string | Zwift REST API base URL |
| horizonDays | number | How many days ahead to collect events for |
| maxSeries | number | Maximum number of event series to expand (each is one extra request) |
| maxEventsPerSeries | number | Maximum events to request per series |
| sports | array | Sports to include |
| eventTypes | array | Event types to include; races and TTs by default |
| includePrivate | boolean | Include events flagged private/unlisted |
| referenceSpeedKph | number | Speed used to estimate a duration when Zwift does not publish one |
| userAgent | string | User-Agent header sent to the Zwift calendar |
Resources
Global Arguments
| Argument | Type | Description |
|---|---|---|
| timezone | string | IANA timezone used to bucket event start times into local hours. Should |
| topN | number | How many recommendations to return |
| horizonDays | number | Only consider events starting within this many days |
| raceBias | number | Preference for racing over group rides: 0.5 is neutral, 1 strongly |
| hourTolerance | number | Tolerance, in hours, when matching an event start to the rider's hour |
| durationTolerance | number | Tolerance, in duration buckets, when matching event length to the |
| repeatWindowDays | number | A route or event repeated within this many days incurs the repeat penalty |
| repeatPenalty | number | Score multiplier applied to recently repeated routes/events |
| maxPerSeries | number | Diversity cap: at most this many recommendations from the same event |
| weights | object | Relative weights of each score component |
Resources
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
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
- 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