GitHub Actions scheduled workflow not running

Two causes cover most cases. In a public repository, GitHub turns off scheduled workflows after 60 days without repository activity. And under load, scheduled runs start late or are dropped. Neither produces an error. This guide shows how to check, then adds three pings so you hear about it when a run starts, finishes, fails, or never happens.

You need: a Lateping account, a repo with a scheduled workflow, about 5 minutes.

Why scheduled workflows stop

A workflow with an on: schedule trigger depends on GitHub deciding to start it. When GitHub doesn’t, there’s nothing in the Actions tab to look at.

Check whether it happened to you

List your workflows with the GitHub CLI. A workflow turned off by the 60-day rule shows the state disabled_inactivity.

bash
gh workflow list --all

To turn it back on, open the workflow in the repo’s Actions tab and choose Enable workflow, or run gh workflow enable <workflow-file>. It will be disabled again after another 60 quiet days, so you also want an alert when it happens.

Also check the basics:

  • Scheduled workflows run only from the default branch.
  • The shortest interval is every 5 minutes.
  • The cron expression is read in UTC.

Set it up

  1. Create a check

    In Lateping, create a check and set its schedule to the same cron expression as your workflow. Set the timezone to UTC, since that’s what GitHub uses. Give it a grace period longer than the job usually takes, plus room for queue delays. 30 minutes is a good start.

    Copy the check’s ping URL. It looks like https://lateping.com/p/<check-id>.

  2. Save the ping URL as a secret

    Anyone with the URL can ping your check, so keep it out of the repo. In GitHub, go to Settings → Secrets and variables → Actions, and add a secret named LATEPING_URL. Or use the GitHub CLI:

    bash
    gh secret set LATEPING_URL \
      --body "https://lateping.com/p/<check-id>"
    
  3. Add the pings to your workflow

    Ping /start first, then the plain URL when the job succeeds. The last step runs only if an earlier step failed, and calls /fail so we alert you right away instead of waiting for the grace period.

    .github/workflows/nightly-backup.yml
    name: Nightly backup
    
    on:
      schedule:
        - cron: "17 2 * * *"   # 02:17 UTC
      workflow_dispatch:
    
    jobs:
      backup:
        runs-on: ubuntu-latest
        steps:
          - name: Ping start
            run: curl -fsS -m 10 --retry 3 "${{ secrets.LATEPING_URL }}/start"
    
          - uses: actions/checkout@v4
    
          - name: Run backup
            run: ./scripts/backup.sh
    
          - name: Ping success
            run: curl -fsS -m 10 --retry 3 "${{ secrets.LATEPING_URL }}"
    
          - name: Ping failure
            if: failure()
            run: curl -fsS -m 10 --retry 3 "${{ secrets.LATEPING_URL }}/fail"
    
    • 17 2 * * * runs at 02:17 UTC. Picking a minute other than :00 avoids the busiest queue.
    • -m 10 --retry 3 stops a slow network call from hanging the job.
    • workflow_dispatch lets you run it by hand to test.
  4. Test it

    Run the workflow once by hand. In Lateping, the check should show a start ping and then a success ping.

    bash
    gh workflow run nightly-backup.yml
    gh run watch
    

Or use the Lateping action

The same pings in one step. Put it last in the job with if: always(), and pass the job’s status: a successful job pings success, and a failed or cancelled one pings /fail.

.github/workflows/nightly-backup.yml
name: Nightly backup

on:
  schedule:
    - cron: "17 2 * * *"   # 02:17 UTC
  workflow_dispatch:

jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - uses: lateping/ping@v1
        with:
          url: ${{ secrets.LATEPING_URL }}
          status: start

      - uses: actions/checkout@v4

      - name: Run backup
        run: ./scripts/backup.sh

      - uses: lateping/ping@v1
        if: always()
        with:
          url: ${{ secrets.LATEPING_URL }}
          status: ${{ job.status }}

A ping that can’t be sent logs a warning instead of failing your job. Set fail-on-error: 'true' if you’d rather it failed. Source: lateping/ping.

Pinging from a Node script

If the job is a single script, you can ping from inside it instead. Pass the secret in as an environment variable with env: on the step.

scripts/backup.mjs
const url = process.env.LATEPING_URL;

await fetch(`${url}/start`);

try {
  await runBackup();
  await fetch(url);
} catch (err) {
  await fetch(`${url}/fail`, { method: "POST" });
  throw err;
}

If GitHub disables the workflow

You’ll get a “late” email from us when the first run is missed. Turn the workflow back on as described in Check whether it happened to you. The next successful run sends a “recovered” email.

Next steps

On this page