heartbeetsign in

monitor a GitHub Actions schedule

scheduled workflows fail in ways GitHub never emails you about: the schedule gets disabled after 60 days of repo inactivity, runs get delayed or dropped under load, and a workflow that errors on its first step just… stops happening. a heartbeat at the end of the run catches all of it.

the workflow step

name: nightly-sync
on:
  schedule:
    - cron: "17 3 * * *"

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/sync.sh

      # last step: only reached if everything above succeeded
      - name: heartbeat
        run: |
          curl -fsS -X POST "https://heartbeet.page/nightly-sync?tag=ci" \
            -H "Authorization: ${{ secrets.HEARTBEET_API_KEY }}"

add HEARTBEET_API_KEY as a repository secret. the heartbeat is the last step, so a failed sync sends nothing and the silence pages you. a nightly schedule needs no hint, and the beets learn the real rhythm from the first heartbeats: the 17-past-3 offset, the minutes GitHub's scheduler adds under load, all of it becomes the job's own learned wobble instead of config you maintain.

what this catches that GitHub doesn't tell you

  • the 60-day disable: GitHub silently disables schedules in repos with no activity; the workflow stops existing as far as cron is concerned. no heartbeat, you're paged.
  • scheduler delays and dropped runs: schedule: is best-effort and drifts under load; chronic lateness shows up as close calls on the dashboard, and on a paid plan the beets automatically warn you about the arrhythmia.
  • broken first steps: a bad checkout, an expired token, a runner image change; the run fails before your code even starts. workflow-failure emails are easy to filter into oblivion.

several workflows, one project

use one api key for the repo and a job name per workflow; tag them (?tag=ci, ?tag=nightly) if you wish, and the dashboard groups them. on the team plan, route the ci tag to the people who own ci instead of paging everyone.

deploys that pause their own monitors

if a deploy workflow takes a job down on purpose, let its predeploy step say so:

- name: pause monitors for the deploy
  run: |
    curl -fsS -X POST "https://heartbeet.page/pause?tag=api&drain=2m&wait=10m" \
      -H "Authorization: ${{ secrets.HEARTBEET_API_KEY }}"

every job carrying the api tag is paused in one call; after the window the beets resume watching each job on its own rhythm, and the pause shows up on the dashboard instead of being a mystery gap.

next