2026-10-25
Why is my GitHub Actions scheduled workflow not running?
A GitHub Actions cron runs in UTC from the default branch, and a skipped schedule creates no failed run. Ping at the end so a workflow that never starts still pages you.
A scheduled GitHub Actions workflow can miss its time and leave the Actions tab exactly as you left it. There is no red X, because a run that never started has no conclusion. GitHub's failure notifications describe workflow runs that completed. A schedule that creates no run has nothing to report. The check lives outside the workflow: a monitor that expects a ping, and alerts when that ping does not arrive.
What the schedule event actually does
GitHub's events that trigger workflows page is the spec for `schedule`. The cron is POSIX, five fields, and it runs in UTC unless that schedule sets an IANA timezone. The run uses the latest commit on the default branch. The event only triggers when the workflow file exists on the default branch, and the run itself is on the default branch. The shortest interval is once every 5 minutes.
The same page says GitHub Actions does not support the non-standard shortcuts `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`, and `@reboot`. A line copied from a server crontab that says `@daily` is not a GitHub schedule.
The workflow syntax page says you define a workflow as a YAML file, with a `.yml` or `.yaml` extension, stored in `.github/workflows`. The schedule reads the file on the default branch, and the file has to be valid YAML. Troubleshooting workflows says a workflow file that exists only on some other branch does not trigger `schedule`.
Delay, a dropped job, and a workflow GitHub turned off
The events page says the schedule event can be delayed when GitHub Actions is under high load. High load times include the start of every hour. If the load is high enough, some queued jobs may be dropped. The documented way to lower the chance of a delay is to schedule the workflow at a different time of the hour. `17 * * * *` is 17 minutes past every hour in UTC, when you have not set `timezone`. GitHub does not publish a maximum delay. A late run can still finish. A dropped job never starts.
In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days. That sentence is about public repositories. Disabling and enabling a workflow adds the fork case: when a public repository is forked, scheduled workflows are disabled by default. A workflow you disable yourself stops too. The troubleshooting page says a disabled workflow does not respond to its triggers.
A second stop leaves the workflow enabled. Troubleshooting says a scheduled workflow can stop running while it is still enabled, and while manual or `workflow_dispatch` runs still work, when the actor account tied to the workflow is suspended, deleted, or deprovisioned. The events page says the account's status is what matters. Removing someone from the organization leaves the schedule running. A suspended or deleted account stops it, and so does an Enterprise Managed User whose identity provider has deprovisioned them. The documented restart is a commit, from someone with write access, that changes the cron schedule. That commit reactivates a deactivated scheduled workflow, and that person becomes the actor.
A checklist before you rewrite the cron
- The file is in `.github/workflows` on the default branch, named `.yml` or `.yaml`, and it is valid YAML. The schedule reads that copy, on the latest commit.
- The cron under `on.schedule` is five POSIX fields. It is UTC unless that schedule sets `timezone` to an IANA name. `@daily` and the other `@` shortcuts are not supported.
- The interval is 5 minutes or slower. Once every 5 minutes is the shortest interval GitHub documents for scheduled workflows.
- The minute is not 0, unless you mean to sit in the busiest minute. High load includes the start of every hour, and a queued job can be delayed or dropped.
- On the Actions tab, the workflow is enabled. In a public repository, 60 days without repository activity disables scheduled workflows. A fork of a public repository starts with scheduled workflows disabled.
- If the workflow is enabled and a manual run works, check the actor. A suspended, deleted, or deprovisioned account stops the schedule. A write-access commit that changes the cron is the documented restart.
- The Actions tab shows a run for the slot you expected. No run means the workflow did not start. A failed run is a job that did start.
How to turn the workflow back on
When the workflow is disabled, GitHub's steps are: open the repository, click Actions, click the workflow in the left sidebar, and click Enable workflow. The GitHub CLI does the same thing. `WORKFLOW` is the name, the id, or the file name, and leaving it off opens a menu.
gh workflow enable WORKFLOWThat enable action is the documented way back for a workflow you disabled, for the 60-day shutdown in a public repository, and for scheduled workflows on a fork of a public repository. Disabling a workflow does not cancel runs that are already going. You can re-run those runs after you enable the workflow again.
When the workflow is still enabled and the schedule is the only thing that stopped, Enable workflow is for a different case. Commit a change to the cron, with write access, as the troubleshooting page describes. Notifications for workflow runs then go to the person who updated the cron syntax. If someone re-enables a disabled scheduled workflow, later notifications go to that person.
Why a failure email stays quiet
Those notifications are opt-in. If you enable email or web notifications for GitHub Actions, you get a notice when a workflow run you triggered has completed. The status can be successful, failed, neutral, or canceled. You can limit that notice to failed runs. In notification settings, under Actions, "Only notify for failed workflows" is that switch. Managing GitHub Actions notifications describes it.
Every one of those notices is about a run that exists. A schedule that was delayed and then dropped, a disabled workflow, a public repository with 60 days of no activity, a fork whose schedules were never enabled, a cron GitHub does not accept, a YAML file that does not parse, a file that is not on the default branch: none of those create a completed run. A failure notification has no run to describe. The Actions tab looks the way it did last week.
Ping from the job, and let a missing ping be the alert
Store the monitor's ping URL in a repository secret named NIGHTSHIFT_PING_URL. The token in that URL is the credential. These steps are the GitHub Actions snippet. The work of the job goes between the start step and the finish step. The example cron is hourly at minute 17, in UTC, which stays off the top of the hour.
on:
schedule:
- cron: '17 * * * *'
jobs:
work:
runs-on: ubuntu-latest
steps:
- name: Tell Nightshift the job started
run: curl -fsS -m 10 -X POST "$NIGHTSHIFT_PING_URL/start"
env:
NIGHTSHIFT_PING_URL: ${{ secrets.NIGHTSHIFT_PING_URL }}
- name: Do the work
run: ./your-job
- name: Tell Nightshift the job finished
if: always()
env:
NIGHTSHIFT_PING_URL: ${{ secrets.NIGHTSHIFT_PING_URL }}
run: |
if [ "${{ job.status }}" = "success" ]; then
curl -fsS -m 10 -X POST "$NIGHTSHIFT_PING_URL"
else
curl -fsS -m 10 -X POST "$NIGHTSHIFT_PING_URL/fail"
fi/start records that the runner began. The ping API is specific: /start does not reset the deadline and does not clear a down monitor. The finish step uses `if: always()`, so it runs when the job started and then succeeded, failed, or was canceled. A `job.status` of success POSTs the secret URL. That success ping is the heartbeat, and it moves the deadline. Any other status POSTs the same URL with `/fail` on the end, which marks the monitor down when the fail ping arrives, before grace runs out.
`if: always()` runs only if the job started. When GitHub never creates a run, the runner never evaluates that condition, and neither curl leaves. The monitor still has a deadline. The schedules page says the next deadline is the next cron slot plus the grace period you set, and the hosted checker looks once a minute. A success ping that never arrives is the alert. A failure notification cannot send that alert, because it waits for a completed run.
Create the monitor with the same five-field cron. Leave the timezone on UTC when the workflow does not set `timezone`. If the workflow sets `timezone`, use that same IANA name on the monitor. Grace is how long a delayed run may finish before you want a page. GitHub documents the delay and the drop, and it does not document a maximum delay, so pick a grace you would still call late. A dropped job never pings. A run that finishes after grace sends the success ping, and you get a recovery message.
A schedule that sets a timezone can also move on the spring daylight-saving change. GitHub documents one case: a time in the skipped hour advances to the next valid time, and the example is 2:30 AM moving to 3:00 AM. The daylight-saving post covers the 2026 US dates and what a UTC cron does on those mornings.
Match the monitor to a cadence GitHub will actually run. Five minutes is the floor on their side. An hourly workflow watched every five minutes will page you every hour. The ping URL is on the monitor page after you sign up.