Skip to content

Schedule unattended token refresh

Keep unattended posting alive. keryx auth refresh refreshes/rotates the stored platform tokens and alerts when it can't — run it from a scheduled pipeline in the owning project so a token never goes silently stale (the #1 unattended failure mode; spec 0010-auth-refresh.md).

keryx is stateless and ships no schedule — the schedule lives in your project's CI (the same repo that holds your content and runs keryx post due). This guide wires that up on GitLab.

Prerequisite: you've already captured each platform's token with keryx auth <platform> — see the credential guides for Instagram, YouTube, TikTok, and LinkedIn.

What refresh does per platform

auth refresh all fans out across your enabled platforms, runs each independently, reports per-platform, and exits non-zero if any needs attention (so the pipeline goes red):

Platform Action Persists? Alerts when
Instagram refresh the long-lived token in place (near its ~60-day expiry) new access token refresh fails / token already dead
YouTube health-check the durable refresh token nothing (durable) refresh token revoked/invalid
TikTok rotate — exchange for a new access and refresh token new refresh token immediately rotation fails (old refresh dead)
LinkedIn none possible (standard app has no refresh) re-auth due within 14 days

TikTok is the one that must write its rotated refresh token back, or the next scheduled run is locked out — which is what the gitlab write-back backend (below) is for.

1. Preview it first (--dry-run)

--dry-run reports what each platform's token looks like and the action it would take, and changes nothing:

$ keryx auth refresh all --dry-run
instagram refreshed   expires —   would refresh the long-lived token in place
tiktok    rotated     expires —   would rotate the refresh token
youtube   healthy     expires —   would health-check the refresh token

You can scope to one platform — keryx auth refresh tiktok --dry-run. An unknown name errors (no refresher for "…", exit 1).

2. Choose the write-back backend

Locally, refreshed tokens persist via keychain/config. In CI there's no keychain, and a job can't natively write back to the CI variables it read — so select the gitlab backend, which updates the CI/CD variables over the API:

# keryx config (committed — non-secret)
auth:
  writeback:
    backend: gitlab            # local (default) | gitlab
    gitlab:
      # project + api_url default to CI_PROJECT_ID / CI_API_V4_URL inside a pipeline;
      # set them explicitly only when running outside CI.
      # project: "12345678"
      # api_url: "https://gitlab.com/api/v4"

The backend updates the same masked, protected variables the adapters read, keyed by each token's variable name (e.g. TIKTOK_REFRESH_TOKEN). It only ever updates an existing variable's value — it never creates a new, unmasked secret.

3. Set the CI/CD variables

In the owning project → Settings → CI/CD → Variables, all Masked and Protected:

Variable Why Written back?
TIKTOK_REFRESH_TOKEN TikTok's rotating refresh token yes — every run
INSTAGRAM_ACCESS_TOKEN Instagram long-lived token yes, near expiry
YOUTUBE_REFRESH_TOKEN YouTube durable refresh token no (health-check only)
LINKEDIN_ACCESS_TOKEN LinkedIn access token no (standard app can't refresh)
LINKEDIN_REFRESH_TOKEN only if you're a LinkedIn MDP partner yes (MDP only)
GITLAB_TOKEN api-scoped project/group access token for write-back

GITLAB_TOKEN must have the api scopeCI_JOB_TOKEN cannot write CI/CD variables. Make it a project or group access token and mark it Protected. CI_PROJECT_ID and CI_API_V4_URL are predefined by GitLab; you don't set those.

Plus each platform's existing client secrets/ids (TIKTOK_CLIENT_SECRET, YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET, INSTAGRAM_APP_SECRET / INSTAGRAM_USER_ID, LINKEDIN_CLIENT_SECRET) as set up in the credential guides.

4. Configure alerts

Any failure (or an impending non-refreshable expiry) must reach a human. The red pipeline is the always-on baseline; add a notifier for an active ping:

auth:
  alerts:
    backend: webhook           # none (default) | webhook | email
    webhook:
      url: ""                  # or the ALERT_WEBHOOK_URL variable (Slack/Teams incoming webhook)
    # email:
    #   host: smtp.example.com
    #   port: 587              # default
    #   from: [email protected]
    #   to: [[email protected]]

Secrets come from the environment: ALERT_WEBHOOK_URL, or ALERT_SMTP_USERNAME / ALERT_SMTP_PASSWORD for email.

5. Add the scheduled job

In the owning project's .gitlab-ci.yml:

token-refresh:
  stage: maintenance
  image: registry.gitlab.com/phpboyscout/keryx:latest   # or however you install keryx
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TASK == "token-refresh"'
  script:
    - keryx auth refresh all
  # GITLAB_TOKEN, TIKTOK_REFRESH_TOKEN, … come from the protected CI/CD variables above.

Then CI/CD → Schedules → New schedule:

  • Interval: weekly (see cadence below).
  • Variable: SCHEDULE_TASK = token-refresh (so this schedule runs only this job).
  • Target the protected default branch (the variables are Protected).

A failing run turns the schedule red and fires your notifier; a green run means every enabled platform is fresh (and TikTok's rotated token has been written back for next time).

Cadence — why weekly

Weekly sits comfortably inside every window: it keeps Instagram's ~60-day token fresh, rotates TikTok well within its ~365-day refresh wall, health-checks YouTube's durable token, and gives ~7 weeks of warning before a LinkedIn token expires (the alert fires at 14 days). More frequent is harmless; much less frequent risks missing the LinkedIn re-auth window.

When an alert fires

  • Instagram / TikTok / YouTube "refresh failed" — the refresh/rotate/health-check didn't work; the token is likely revoked or dead. Re-capture it: keryx auth <platform>, then update the CI variable.
  • LinkedIn "re-auth due" — a standard LinkedIn app can't refresh; run keryx auth linkedin before the date in the alert and update LINKEDIN_ACCESS_TOKEN.

See also