PostDeploy Start your 30-day free trial

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. A heartbeat notices, because the expected ping does not arrive.

Add the ping

Create a heartbeat monitor in PostDeploy, set its interval to your schedule and give it a grace period wide enough to absorb normal queue delay. Store the 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 PostDeploy finds out only when the grace period expires. 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.

Alerts that fire while runs are succeeding usually mean the grace period is tighter than GitHub's queue delay. Widen it rather than lowering the schedule.

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, which is why a heartbeat is the way to detect it.

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.

Ship your next project with one toolkit, not three.

Sign up and every feature described on this page is yours on the trial. No credit card.

Create a heartbeat