Monitor a GitHub Actions scheduled workflow
A scheduled workflow can stop running without failing. GitHub disables schedules in repositories with no activity for 60 days, and delays or drops runs when the shared cron queue is busy. Neither produces a failed run to notice, because there is no run.
By Paul, published , last verified
The failure this catches
A workflow with an on.schedule trigger has two failure modes that produce no failed run. GitHub disables scheduled workflows in a public repository after 60 days without commits, which is easy to hit on a stable automation repo. Separately, scheduled runs are queued on shared infrastructure and can be delayed, particularly on the hour, and a run that is delayed long enough may be dropped.
In both cases the Actions tab shows no failure, because nothing ran. PostDeploy records a heartbeat only when a request arrives. Use an external watchdog when a skipped or delayed workflow needs an alert.
Add the ping
Ask a connected MCP client to create the heartbeat, then store its returned token as a repository secret named POSTDEPLOY_HEARTBEAT_TOKEN.
Put the ping in its own step at the end of the job. It runs only if every previous step succeeded, which is the behaviour you want: a failed build should not report a healthy run.
name: Nightly sync
on:
schedule:
# 03:17 rather than 03:00: on-the-hour schedules queue behind
# everyone else's and are the most likely to be delayed.
- cron: "17 3 * * *"
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run the sync
run: ./scripts/sync.sh
- name: Report success to PostDeploy
run: curl -fsS "$POSTDEPLOY_INGEST_URL/h/$POSTDEPLOY_HEARTBEAT_TOKEN"
env:
POSTDEPLOY_INGEST_URL: https://ingest.postdeploy.dev
POSTDEPLOY_HEARTBEAT_TOKEN: ${{ secrets.POSTDEPLOY_HEARTBEAT_TOKEN }}
Reporting failures too
The step above is skipped when an earlier step fails, so it records no result. To alert the moment a run fails, add a second step that runs on failure and reports a non-zero exit code.
if: failure() runs the step only when a previous step failed. The /1 path segment reports a failing exit code, which opens an incident immediately rather than waiting for the schedule to lapse.
- name: Report failure to PostDeploy
if: failure()
run: curl -fsS "$POSTDEPLOY_INGEST_URL/h/$POSTDEPLOY_HEARTBEAT_TOKEN/1"
env:
POSTDEPLOY_INGEST_URL: https://ingest.postdeploy.dev
POSTDEPLOY_HEARTBEAT_TOKEN: ${{ secrets.POSTDEPLOY_HEARTBEAT_TOKEN }}
Check it works before you trust it
A monitor you never tested is a monitor you are guessing about. Trigger the workflow manually with workflow_dispatch, which the example enables, and confirm the monitor moved to a passing state in PostDeploy.
Then test the failure path, because that is the one you actually care about. Temporarily make the job exit non-zero, run it, and confirm you receive the alert. A successful ping proves the token and network path; only a failed run proves the alert reaches you.
# Verify the token and URL from your terminal first.
curl -i "https://ingest.postdeploy.dev/h/hb_YOUR_TOKEN"
# Expect: HTTP/2 202 with an empty body.
# A 404 means the token is wrong or the monitor was deleted.
Troubleshooting
A 404 from the ping means the token is not recognised: check for a truncated secret, or a monitor that was deleted and recreated with a new token.
A 202 with no change in the PostDeploy console means the monitor is paused. Paused monitors accept pings and record nothing, by design.
A skipped or delayed workflow does not create a PostDeploy incident without an inbound failure ping. Use an external watchdog for that condition.
If the workflow stopped running entirely on a public repository, check whether GitHub disabled the schedule for inactivity; the Actions tab shows a banner and a button to re-enable it.
Questions
Why did my scheduled workflow stop running?
The most common cause on a public repository is GitHub disabling schedules after 60 days without repository activity. The other is queue delay on shared runners, which is worst for schedules on the hour. Neither creates a failed run. Use an external watchdog when that absence needs an alert.
Should I put the ping in a separate job or the same one?
The same job, as a final step. A separate job needs an explicit needs: dependency and runs on a fresh runner, which adds a queue wait and another thing that can fail between the work and the report.
Does this work on a private repository?
Yes. The 60-day inactivity disabling applies to public repositories, but queue delay and ordinary breakage apply everywhere, and the ping itself is just an outbound HTTPS request from the runner.
Limits
- PostDeploy does not read GitHub Actions run logs
- PostDeploy does not restart or re-run a failed workflow
- PostDeploy does not track workflow duration, minutes usage or cost