GitHub Actions scheduled workflow not running? Why, and how to get alerted
You added on: schedule, the workflow ran fine for a while, and then it didn't. Or it never ran at all, or it ran an hour late. GitHub won't email you about a run that never started. Here's what usually causes it, how to check in two minutes, and how to add a heartbeat so you're told the first time it happens.
Short answer: check that the workflow is enabled and on the default branch, and that your cron time is in UTC. Public repositories have scheduled workflows switched off after 60 days without activity, and any schedule can start late under load. To catch it next time, end the job with a step that pings a heartbeat monitor on success and a /fail URL on failure. If the ping doesn't arrive on time, you get an alert.
Why scheduled workflows don't run
| Symptom | Likely cause | Fix |
|---|---|---|
| Never ran at all | The workflow file (or the schedule trigger) is only on a feature branch. Scheduled workflows run on the latest commit of the default branch. | Merge it to the default branch. |
| Ran at the "wrong" time | Schedules are evaluated in UTC by default. 0 9 * * * is 9:00 UTC, not 9:00 where you are. | Convert to UTC and recheck when daylight saving changes, or set a time zone on the schedule if your GitHub plan supports the timezone key (check the current docs). |
| Stopped after weeks of working | In public repositories, GitHub disables scheduled workflows after 60 days with no repository activity. The Actions tab shows a banner; nothing is emailed by default. | Re-enable in the Actions tab or with gh workflow enable. |
| Stopped, but still shows as enabled | GitHub's troubleshooting docs note that a scheduled workflow can stop when its associated actor account (usually whoever last changed the cron) is suspended, deleted or deprovisioned, even though manual runs still work. | Have someone with write access commit a change to the cron line. |
| Starts 10 to 60+ minutes late, or skips a run | GitHub documents that scheduled events can be delayed during high load, most often at the top of the hour, and that some queued runs may be dropped. | Avoid minute 0, and set a realistic grace period. |
| Disabled or silently ignored | Someone disabled the workflow, the YAML or cron expression is invalid, Actions is disabled for the repository, or it's a fork where scheduled workflows don't run until Actions is enabled there. | Check gh workflow list --all and the Actions tab for errors. |
| Ran, "succeeded", produced nothing | A script swallowed an error, an API returned zero rows, a step was skipped by an if:. | Report a count and alert on zero (see below). |
The shortest schedule Actions allows is once every 5 minutes. If you need tighter timing than "roughly when I asked", a scheduled workflow is the wrong tool; trigger it from outside with workflow_dispatch or repository_dispatch instead.
Check it right now
With the GitHub CLI signed in to the repository:
# Is the workflow enabled? Disabled ones show "disabled_inactivity" or "disabled_manually"
gh workflow list --all
# Did any scheduled runs happen recently?
gh run list --workflow nightly.yml --event schedule --limit 10
# Re-enable a workflow GitHub switched off
gh workflow enable nightly.yml
# Is the file (with the schedule) on the default branch?
gh repo view --json defaultBranchRef --jq .defaultBranchRef.name
git log origin/main -1 -- .github/workflows/nightly.yml
- Enabled? If
gh workflow list --allshows the workflow as disabled, that's your answer. - Any scheduled runs at all? No rows from
gh run list --event schedulemeans the trigger never fired: check the branch and the YAML. - Late or missing ones? Compare the run start times (shown in UTC) with your cron in UTC.
- Ran but wrong? Open the latest run and check that the steps that do the work actually ran and produced output.
Get alerted when a scheduled run is missed
GitHub can notify you when a run fails, but a disabled workflow, a dropped run or a schedule on the wrong branch produces no run, so there's nothing to fail. A heartbeat monitor flips it around: the workflow checks in after each good run, and silence raises the alert. The examples use a Pulsecheck ping URL like https://pulsecheck.run/p/YOUR-JOB-ID, but any heartbeat service works the same way.
1. Work out the expected schedule in UTC
17 19 * * * is once a day at 19:17 UTC. Pick a grace period that covers GitHub's delays plus your run time: 30 to 60 minutes is sensible for most scheduled workflows.
2. Create a monitor and store its URL as a secret
In Pulsecheck, add a job with that period and grace and copy its ping URL. Save it as a repository secret (Settings › Secrets and variables › Actions) called PULSECHECK_URL, so it isn't sitting in your public YAML.
3. Add a success heartbeat step
Make it the last step, with if: success(). POSTing a small JSON body like {"count": 42} lets an output rule such as count at least 1 flag a run that succeeded with nothing in it as Ran but output looks wrong. (Output checks are on Pulsecheck's Pro and Agency plans; missed-run and /fail alerts work on every plan, including the free one.)
4. Add a failure heartbeat step
A step with if: failure() that POSTs a link to the run to /fail means you hear about a broken run immediately instead of waiting for the grace period to run out, and the alert takes you straight to the logs.
5. Bound the job with timeout-minutes
The default job timeout is 360 minutes. Set timeout-minutes to a little over your longest normal run, so a hung step fails, triggers the failure step, and frees the runner.
6. Test with workflow_dispatch
Add workflow_dispatch so you can run it by hand from the Actions tab or with gh workflow run nightly.yml. Confirm the monitor turns green, then make a step fail on purpose and confirm the /fail alert arrives.
Copy-paste workflow
Replace build_report.py with your own work, and add PULSECHECK_URL as a secret first.
name: nightly-report
on:
schedule:
- cron: "17 19 * * *" # 19:17 UTC = 06:17 in Melbourne during daylight saving
workflow_dispatch: {} # lets you run it by hand to test
jobs:
report:
runs-on: ubuntu-latest
timeout-minutes: 30 # a stuck run fails instead of running for hours
steps:
- uses: actions/checkout@v4
- name: Build report
id: build
run: |
python build_report.py > out.txt
echo "rows=$(wc -l < out.txt)" >> "$GITHUB_OUTPUT"
- name: Heartbeat (success)
if: success()
run: |
curl -fsS -m 10 --retry 3 -X POST "${{ secrets.PULSECHECK_URL }}" \
-H "Content-Type: application/json" \
-d '{"count": ${{ steps.build.outputs.rows }}, "run": "${{ github.run_id }}"}'
- name: Heartbeat (failure)
if: failure()
run: |
curl -fsS -m 10 --retry 3 -X POST "${{ secrets.PULSECHECK_URL }}/fail" \
-H "Content-Type: text/plain" \
--data-binary "Run failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
If the job is cancelled (for example by a newer run in the same concurrency group), neither heartbeat step runs, and the missed-ping alert covers it. Running the same job somewhere else? See the guides for cron jobs, scheduled AI agents and n8n workflows.
Common mistakes
- Scheduling at minute 0. That's when GitHub is busiest.
17 * * * *beats0 * * * *. - Writing cron in local time. Schedules default to UTC; a daylight saving change will shift the run by an hour.
- Assuming the failure email reaches the team. GitHub sends scheduled-workflow notifications to the user who last modified the cron syntax. If they've moved on, nobody hears.
- Testing only on a branch. The schedule only fires from the default branch.
- Relying on "failed run" emails. They can't tell you about a run that never started.
- Pinging at the start of the job. That proves GitHub started it, not that it worked. Ping last, on success.
- Hardcoding the ping URL in a public repo. Anyone could ping it and hide a real outage. Use a secret.
- Letting the ping fail the run. Keep
-m 10and--retryon curl; a monitoring hiccup shouldn't turn a good run red.
FAQ
Why is my GitHub Actions cron schedule not running?
The most common reasons are: the workflow file isn't on the default branch, the cron time is in UTC rather than your local time, GitHub disabled the workflow after 60 days without repository activity (public repositories), the workflow was disabled by hand, or the YAML is invalid. Check gh workflow list --all and gh run list --event schedule first.
What time zone does a GitHub Actions schedule use?
Schedules are evaluated in UTC by default. A cron of 0 9 * * * runs at 9:00 UTC, which is evening in Australia and early morning in the US. Convert your local time to UTC and remember that daylight saving changes the offset, or use the timezone key on the schedule where GitHub supports it.
Why did my scheduled workflow run late?
GitHub documents that scheduled events can be delayed during periods of high load, especially at the start of every hour, and that some queued runs may be dropped. Pick an odd minute such as 17 instead of 0, and give your monitor a grace period of 30 to 60 minutes.
Why did GitHub disable my scheduled workflow?
In public repositories, scheduled workflows are automatically disabled when there has been no repository activity for 60 days. GitHub shows a banner in the Actions tab; re-enable it there or with gh workflow enable. A heartbeat monitor catches it the first day it stops.
Does a scheduled workflow run on branches other than main?
No. Scheduled workflows run on the latest commit on the default branch. A schedule that only exists in a feature branch, or in a renamed workflow file that hasn't been merged, never fires.
How do I get alerted when a scheduled GitHub Action doesn't run?
GitHub's own notifications only cover runs that started and failed. To catch a run that never started, add a final step that pings a heartbeat monitor on success and a /fail URL on failure. If the success ping doesn't arrive on schedule plus a grace period, the monitor alerts you.
Know the moment a scheduled workflow goes quiet.
Pulsecheck gives every scheduled workflow its own ping URL and alerts you by email (or Slack on Pro and Agency) when a run is missed, fails, or comes back empty. Free for 3 jobs.
Start free