Monitor GitHub Actions Scheduled Workflows
GitHub Actions lets you run automated workflows on a cron schedule using the on: schedule trigger, but this convenience comes with a serious operational blind spot: the platform can silently stop running your jobs with no alert, no error in the Actions tab, and no log entry to investigate. Whether it is the 60-day inactivity auto-disable policy, unpredictable runner queue delays, or a workflow that exits with code zero despite a logical failure, your scheduled automation can go dark while your team assumes everything is fine. Adding an external heartbeat monitor is the only reliable way to know your scheduled workflows are actually executing on time.
Why GitHub Actions Scheduled Workflows Fail Without Warning
GitHub's on: schedule trigger is documented explicitly as best-effort. There is no service level agreement on timing, and GitHub provides no built-in mechanism to notify you when a scheduled workflow stops firing altogether. The most dangerous aspect of this is not that jobs can be late or slow — it is that they can stop running entirely while leaving zero signal in any log you would normally check. The failure modes below are all confirmed by GitHub's own documentation and by widespread community reports, and every one of them is invisible unless you have external monitoring in place.
- Auto-disabled after 60 days of repo inactivity: GitHub automatically disables all scheduled workflows in any repository that has had no commit activity for 60 consecutive days. Only new commits count as qualifying activity — creating tags, opening issues, or merging pull requests does not reset the timer. When the disable happens, there is no error in the Actions tab and no banner on the workflow page unless you navigate directly to it. You may receive a single email notification routed to whoever last enabled the workflow, which is easily missed on a shared team account.
- Schedule runs are best-effort with no timing guarantees: GitHub's documentation states that scheduled workflows are subject to high-load delays. Delays of 5 to 30 minutes are routine, and community reports from 2025 document delays exceeding 60 minutes during peak usage windows. Runs scheduled at the top of any hour (:00) experience the highest contention on GitHub's shared runner fleet. GitHub can also silently skip a run entirely when the queue is saturated, leaving no trace of the missed execution.
- Minimum interval of 5 minutes is silently enforced: Any cron expression more frequent than */5 * * * * is silently rejected. GitHub will not error; it will simply not trigger the workflow at the cadence you specified, potentially skipping the majority of your intended runs with no explanation.
- Scheduled workflows are disabled by default on forked repositories: If your workflow lives in a forked repository, GitHub disables the schedule trigger by default. This is a common surprise when teams fork a template repo and expect automation to carry over. The fork must have the workflow manually enabled, and the parent's 60-day inactivity clock applies independently.
- Workflow-level failures do not stop future schedule triggers: If your workflow job exits with code zero but produces incorrect output — a data pipeline that writes an empty file, a notification job that silently catches its own exception — GitHub marks the run as successful and continues scheduling it. There is no distinction between a job completing correctly and a job completing quietly with a business logic failure.
- Actions can be disabled at the organization or repository level without notifying workflow authors: Organization owners can disable GitHub Actions entirely, or restrict which workflows are allowed to run, at any time. This produces no notification to workflow authors and shows no error in the workflow history for future scheduled runs that never happen.
The Right Fix: External Heartbeat Monitoring
Internal GitHub tooling — the Actions tab, workflow run history, status badges, and email notifications for failed runs — all share the same fundamental weakness: they only report on runs that actually happen. When a scheduled workflow stops firing entirely, there is no run to fail, no log to read, and no notification to send. This is why internal monitoring cannot catch the failure modes described above. The correct approach is a dead-man's switch, also called a heartbeat monitor. You configure a monitoring service with the schedule you expect: for example, once every 24 hours. That service gives you a unique ping URL. Your workflow calls that URL as its final step on every successful execution. If the monitoring service does not receive a ping within the expected window plus a configurable grace period, it fires an alert to your chosen channel — email, Slack, Discord, Teams, PagerDuty, Opsgenie, or a webhook. CronJobPro provides exactly this mechanism. You create a heartbeat monitor at https://cronjobpro.com, set the expected interval and grace window, and receive a ping URL in the form https://cronjobpro.com/ping/<token>. Your workflow hits that URL on success. If the ping does not arrive — whether because GitHub disabled the workflow, the runner queue dropped the run, your job crashed before reaching the ping step, or any other reason — CronJobPro alerts you. You can also signal explicit failure by calling https://cronjobpro.com/ping/<token>/fail, or report the process exit code via https://cronjobpro.com/ping/<token>/exitcode/<n>. This approach catches every failure mode that internal GitHub monitoring misses, because the monitoring service is watching for the absence of a signal rather than waiting to be notified of a problem.
Add a heartbeat to GitHub Actions
- 1
Create a heartbeat monitor on CronJobPro
Log in to CronJobPro and create a new Heartbeat monitor. Set the monitor type to Heartbeat (dead-man's switch). Configure the expected period to match your workflow's schedule — for a workflow that runs daily at 06:00 UTC, set the period to 24 hours. Set a grace window appropriate to how much delay you can tolerate before alerting, for example 30 minutes. CronJobPro will generate a unique ping URL in the form https://cronjobpro.com/ping/<token>. Copy this URL.
- 2
Store the ping URL as a repository secret
In your GitHub repository, go to Settings, then Secrets and variables, then Actions. Create a new repository secret named CRONJOBPRO_HEARTBEAT_URL and paste the full ping URL as its value. Using a secret keeps the token out of your workflow YAML and your repository history.
- 3
Add the heartbeat ping as the last step of your workflow job
Edit your workflow YAML file. In the job that your schedule trigger runs, add a final step after all your actual work steps. This step should call the ping URL using curl. Place it after all other steps so that it only executes if all preceding steps succeeded. If you want to explicitly report failure, add a separate step using the if: failure() condition that calls the /fail endpoint instead.
- 4
Configure your alert channels in CronJobPro
In the CronJobPro monitor settings, add the notification channels you want to alert when a ping is missed. Options include email, Slack, Discord, Microsoft Teams, PagerDuty, Opsgenie, and generic webhooks. Set the alert policy to notify you when the expected ping has not arrived within the period plus the grace window. Test the alert path by temporarily disabling your workflow to confirm an alert fires.
- 5
Commit a keepalive to prevent the 60-day auto-disable
For repositories where scheduled workflows are the primary use case and there may be no other commit activity, add a secondary scheduled workflow that runs once per month and makes a trivial commit or calls the GitHub API to touch a timestamp file. The open-source actions/gh-action-keepalive and Keepalive Workflow action on the GitHub Marketplace both automate this. This addresses the root cause of the 60-day disable problem independently of your heartbeat monitoring.
yaml
name: Daily Data Sync
on:
schedule:
# Avoid top-of-hour — high runner contention on GitHub's shared fleet
- cron: '17 6 * * *'
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Run data sync
run: |
# Your actual job logic here
python sync.py
# This step only runs if ALL previous steps succeeded.
# If any step fails, this step is skipped and the /fail ping
# step below fires instead — giving you both success and
# failure visibility in CronJobPro.
- name: Ping CronJobPro heartbeat on success
if: success()
run: |
curl --silent --show-error --max-time 10 \
"${{ secrets.CRONJOBPRO_HEARTBEAT_URL }}"
# Optional: explicitly signal failure so CronJobPro can
# distinguish a missed run (no ping at all) from a run that
# fired but the job crashed.
- name: Ping CronJobPro on failure
if: failure()
run: |
curl --silent --show-error --max-time 10 \
"${{ secrets.CRONJOBPRO_HEARTBEAT_URL }}/fail"Frequently asked questions
Does GitHub send me an email when it disables my scheduled workflow after 60 days?
GitHub may send a single email notification, but it is routed to the user who most recently enabled the workflow — not necessarily the workflow author or the whole team. If that person has notifications muted or has left the organization, the email is never seen. The workflow simply stops running with no further signal. This is why relying on GitHub's own notification system is not sufficient for production scheduled workflows.
How long can GitHub Actions schedule delays actually get in practice?
GitHub's documentation describes the schedule trigger as best-effort with no timing guarantee. Community reports consistently document 5 to 30 minute delays as routine, with delays exceeding 60 minutes reported during high-demand periods. Runs scheduled at the top of the hour face the worst contention. In extreme cases, individual runs can be dropped entirely with no log entry. If your workflow must complete within a narrow time window, the on: schedule trigger alone is not a reliable mechanism.
Can I use GitHub's built-in workflow failure notifications instead of an external heartbeat monitor?
GitHub's built-in notifications for workflow failures only fire when a run actually happens and exits with a non-zero status. They cannot detect a workflow that has been silently disabled, a run that was skipped entirely by the scheduler, or a job that exits with code zero despite producing incorrect output. An external heartbeat monitor watches for the absence of a ping rather than waiting to be informed of a failure, which means it catches every scenario that GitHub's own tooling misses.
What is the difference between calling /ping/<token>, /ping/<token>/fail, and /ping/<token>/exitcode/<n>?
Calling /ping/<token> reports a successful execution and resets the dead-man's switch timer. Calling /ping/<token>/fail explicitly records that the job ran but failed, which allows CronJobPro to distinguish between a job that crashed and a job that never ran at all. Calling /ping/<token>/exitcode/<n> lets you pass the actual numeric exit code from your process, which is useful when wrapping shell scripts or compiled binaries where the exit code carries semantic meaning. For most GitHub Actions workflows, using the success and fail endpoints with if: success() and if: failure() conditions is the clearest pattern.
Does the 60-day inactivity rule apply to private repositories as well?
Yes. The 60-day auto-disable policy applies to both public and private repositories. Only new commits reset the inactivity timer. Other repository events such as creating releases, pushing tags, opening issues, or merging pull requests do not count as qualifying activity and will not prevent the scheduled workflows from being disabled.
More monitoring guides
Catch silent failures in GitHub Actions
Add one HTTP ping and CronJobPro alerts you the moment a run is missed or fails.