How to monitor a GitHub Actions scheduled workflow that GitHub turned off
Put a schedule trigger in a GitHub Actions workflow and you’ve got a free cron
server with logs, retries, and a green checkmark when it works:
on:
schedule:
- cron: "0 3 * * *"
There’s a line in the GitHub docs that deserves more attention than it gets:
In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days.
Sixty days of quiet and GitHub switches your job off.
Why this one is nastier than it sounds
Think about which repos this hits. Not the ones you’re actively working in — those have activity by definition. It hits the repo you set up because it works without you: the nightly sync, the certificate renewal, the weekly report generator. Finished, working, untouched.
Untouched is the trigger condition. The better the job, the more likely GitHub turns it off.
And it produces no failure. A workflow that was disabled doesn’t run, so there’s no run to fail. No red X, no annotation, no notification email of the kind you get for a broken build. Your Actions tab just… stops having new entries in it, which looks exactly like a period where nothing was scheduled.
Every alert you have is wired to runs that happened. This is the absence of one.
Two smaller versions of the same problem
While you’re in there, the same docs page has two more:
Runs can be dropped. GitHub is explicit that the schedule event “can be
delayed during periods of high loads,” that high load includes the top of every
hour, and that “if the load is sufficiently high enough, some queued jobs may be
dropped.” Not delayed — dropped. Your 0 * * * * job silently skips a run and
there’s nothing in the UI to say so. (Moving off the top of the hour to
something like 7 * * * * genuinely helps.)
Scheduled workflows only run on the default branch. If you added the cron on a branch and merged something else, or renamed the default branch, the schedule quietly stops applying. Again: no error. The workflow is valid, it’s just never triggered.
The check that survives all three
You can’t catch “GitHub decided not to run this” from inside GitHub. Whatever is watching has to be outside, and it has to be watching for silence rather than for failure.
Add a heartbeat as the last step, gated on the job succeeding:
on:
schedule:
- cron: "7 3 * * *"
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./sync.sh
- name: Heartbeat
if: success()
run: curl -fsS --retry 3 --max-time 10 "$PING_URL"
env:
PING_URL: ${{ secrets.PING_URL }}
if: success() is the important part. Steps don’t run after a previous step
fails by default, but being explicit means the ping can’t drift above a failing
step during a later edit and start reporting success for a job that broke.
Whether the workflow errored, got dropped at the top of the hour, or was switched off two months ago, the outcome is identical from the outside: no ping arrived. One check covers all of it.
Two practical notes:
Add workflow_dispatch. It costs nothing and lets you trigger a run by hand
to confirm the heartbeat actually lands before you trust it.
Put the URL in a repository secret. Anyone who can read it can send a fake heartbeat and hold your monitor green while the job is dead.
The 60-day fix people reach for
The common workaround is a second workflow that commits an empty change on a schedule to keep the repo “active.” It works — until the workflow doing the keep-alive is itself disabled for inactivity, which is a fun morning.
Keeping a repo artificially awake is a workaround for a symptom. Knowing within an hour that the job stopped is the actual fix, and it also covers the other five ways a scheduled workflow dies.
That’s what Did My Job Run does: add one step, and the silence becomes the alert.