GitHub Actions scheduled workflows (on: schedule:) are supposed to run reliably on a cron expression. In practice, they stop running more often than developers expect — and they do it silently. Your workflow simply doesn't appear in the Actions tab. No failure notification. No email. Just a gap in the run history.
This is one of the more frustrating problems in developer tooling because the failure mode is invisible. Here are the five most common causes and how to handle each one.
This is the cause that surprises most people. GitHub automatically disables scheduled workflows on repositories that have had no activity for 60 days. "Activity" means pushes, pull requests, or other repository events — not workflow runs themselves. If you have a scheduled job that runs in an otherwise-idle repo, GitHub will disable it after 60 days and send you one email notification. If you miss that email, the workflow just stops.
How to check: Go to your repo → Actions → find the workflow → look for a yellow banner at the top saying the workflow was disabled.
How to fix: Click "Enable workflow" in the Actions tab. To prevent recurrence, add a workflow_dispatch: trigger as a second trigger — this doesn't require you to actually use it, but the existence of a manual trigger keeps the workflow considered "active".
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9am UTC
workflow_dispatch: # Add this — prevents inactivity disable
GitHub Actions cron syntax follows standard five-field cron format, but there are two important constraints:
0 9 * * * expecting 9am Pacific, it will run at 9am UTC (1am PT or 2am PDT). This causes jobs to appear to not run at all if you're checking at the wrong time.@daily, @weekly, and @monthly shorthands are supported, but @reboot is not. Non-standard extensions like H (Jenkins-style hash) are rejected.A subtle bug: writing 0 9 * * 1-5 to run on weekdays at 9am UTC works as expected, but 0 9 * * MON-FRI (using day names) may not be supported depending on your runner version. Stick to numbers.
Before deploying a scheduled workflow, verify the expression parses correctly and runs at the times you expect.
Parse cron expression →GitHub's documentation acknowledges that scheduled workflows may be delayed or not run at all during periods of high load on GitHub's infrastructure. The scheduler is best-effort, not guaranteed. In practice this is rare — but for workflows where timing matters (nightly jobs with downstream dependencies, billing cycles, time-sensitive cleanup), a 15–60 minute delay or an occasional complete miss is within the expected behavior.
The documented guidance is not to use GitHub Actions scheduled workflows for time-critical operations. If a job must run within a narrow window, use a dedicated scheduler (a cron daemon, AWS EventBridge, a monitoring service) that guarantees delivery.
Scheduled workflows are only active on the default branch of your repository. If you add or modify a schedule: trigger in a feature branch, it won't run — even after a push. The schedule only takes effect once the workflow file is merged to the default branch (usually main or master).
A common mistake: you add a scheduled workflow on a feature/monitoring branch for testing, it doesn't run, you assume the cron expression is wrong and start debugging the syntax, when the real problem is that the file hasn't been merged yet.
How to check: Confirm the workflow file exists in .github/workflows/ on your default branch, not just on a feature branch.
Sometimes the workflow does run, but a step exits with a non-zero code that doesn't cause the overall job to fail because the step is configured with continue-on-error: true, or the failure is swallowed inside a shell script with set -e but the error-producing command is in a subshell or pipeline where exit codes are lost.
This is different from the workflow not running — the run appears in the history as "Success" — but no actual work was done. The database wasn't updated. The email wasn't sent. The cleanup didn't happen.
How to check: Look at the workflow run log for each step. Check for outputs like "0 rows affected" or empty results that indicate the job ran but did nothing. If you're using shell scripts, add set -euo pipefail at the top of every script to surface errors that would otherwise be silently ignored.
steps:
- name: Run nightly sync
run: |
set -euo pipefail
node scripts/sync.js
| Check | Where to look | Fix |
|---|---|---|
| Repository inactivity freeze | Actions tab → yellow banner | Re-enable + add workflow_dispatch: |
| Wrong cron expression | Validate expression in a cron parser | Fix expression; remember UTC only |
| GitHub infrastructure delay | Check run history for missing slots | Use external scheduler for critical jobs |
| Workflow on wrong branch | Check .github/workflows/ on default branch |
Merge workflow file to main |
| Job runs but steps fail silently | Workflow run logs, step-by-step output | Add set -euo pipefail to shell steps |
All five causes above share a common root: there is no heartbeat for scheduled jobs. When a cron job works, you never hear from it — which means when it stops working, you also never hear from it. The default state is silence in both cases.
The inversion is monitoring from the outside: instead of waiting for a failure notification (which may never come), you configure an external service to expect a check-in at every scheduled interval. If the check-in doesn't arrive within the expected window, you get alerted. This is the dead man's switch pattern — a signal that must be renewed, rather than a signal sent only on failure.
For GitHub Actions specifically, the pattern is straightforward:
steps:
- name: Run nightly sync
run: node scripts/sync.js
- name: Ping monitor
if: success()
run: curl -s https://your-monitor-url/ping/your-job-id
If step 1 fails, step 2 never runs. Your monitoring service sees a missed check-in and alerts you within minutes of the missed window — rather than days later when a user notices something is wrong.
CronWatch monitors your scheduled jobs across GitHub Actions, Heroku Scheduler, crontab, and AWS EventBridge. When a job misses its window — for any reason — you get an alert within minutes.
Join CronWatch waitlist → Validate cron expression →