GitHub Actions Cron Workflow Monitoring
The Workflow That Silently Stopped Running
TL;DR: GitHub Actions scheduled workflows fail silently when GitHub disables them after 60 days of repository inactivity, when the cron expression targets a non-default branch, when concurrent runs get cancelled, or when workflow permissions change. External dead man's switch monitoring detects all of these by tracking whether the expected success signal arrives.
I had a GitHub Actions workflow that ran every 6 hours to sync data from a third-party API into our staging database. It worked perfectly for three months. Then I took a vacation, didn't push any commits for 65 days, and GitHub silently disabled the scheduled workflow. No email, no notification, no banner in the repo. I found out when a colleague asked why the staging data was two months stale.
GitHub Actions scheduled workflows have several gotchas that make them unreliable for critical recurring tasks -unless you monitor them externally. Here's what I've learned.
Why Do GitHub Actions Cron Workflows Fail Silently?
There are at least four ways a scheduled GitHub Actions workflow can stop running without telling you:
- 60-day inactivity disabling. If no commits are pushed to the repository for 60 days, GitHub automatically disables all scheduled workflows. You get no notification. The workflow just stops. You have to manually re-enable it in the Actions tab.
- Delayed or skipped execution. GitHub doesn't guarantee your cron schedule runs on time. During periods of high load, scheduled runs can be delayed by minutes or even hours. In extreme cases, they're dropped entirely. The GitHub docs explicitly say: "Scheduled workflows are only run on the default branch" and "the shortest interval you can run scheduled workflows is once every 5 minutes" -but they also note that during peak times, scheduled runs may be delayed.
- Workflow file errors. If someone merges a change that introduces a YAML syntax error in the workflow file, the schedule silently stops. There's no "your workflow file is broken" alert.
- Branch protection or permissions changes. If repository permissions change and the workflow's
GITHUB_TOKENno longer has the required permissions, the workflow fails on every run. Unless you're actively checking the Actions tab, you won't notice.
What Does a Typical Scheduled Workflow Look Like?
Here's a basic example -a workflow that syncs data every 6 hours:
name: Sync External Data
on:
schedule:
- cron: '0 */6 * * *' # Every 6 hours
workflow_dispatch: {} # Allow manual trigger
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run sync script
env:
API_KEY: ${{ secrets.EXTERNAL_API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: node scripts/sync-data.jsThis workflow has no monitoring. If it stops running -due to inactivity, GitHub load shedding, or a broken YAML merge -nobody knows until someone manually checks.
How Do You Add Dead Man's Switch Monitoring?
Add a final step that pings DeadPing after all previous steps succeed. If any earlier step fails, the workflow stops (default behavior), and the ping never fires:
name: Sync External Data
on:
schedule:
- cron: '0 */6 * * *'
workflow_dispatch: {}
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run sync script
env:
API_KEY: ${{ secrets.EXTERNAL_API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: node scripts/sync-data.js
- name: Ping DeadPing on success
if: success()
run: curl -fsS --retry 3 --max-time 10 https://deadping.io/api/ping/${{ secrets.DEADPING_TOKEN }}A few things to note. The if: success() condition is technically the default, but I prefer being explicit. The token is stored as a repository secret so it's not exposed in logs. The--retry 3 handles transient network issues within the GitHub Actions runner.
How Do You Handle Multi-Job Workflows?
If your workflow has multiple jobs, you only want to ping after all of them succeed. Add a dedicated monitoring job that depends on everything else:
jobs:
fetch:
runs-on: ubuntu-latest
steps:
- name: Fetch data
run: ./scripts/fetch.sh
transform:
needs: fetch
runs-on: ubuntu-latest
steps:
- name: Transform data
run: ./scripts/transform.sh
load:
needs: transform
runs-on: ubuntu-latest
steps:
- name: Load into database
run: ./scripts/load.sh
notify-success:
needs: [fetch, transform, load]
runs-on: ubuntu-latest
if: success()
steps:
- name: Ping DeadPing
run: curl -fsS --retry 3 --max-time 10 https://deadping.io/api/ping/${{ secrets.DEADPING_TOKEN }}The notify-success job only runs if all three upstream jobs succeeded. If any one of them fails, no ping, and DeadPing alerts you after the grace period.
What Grace Period Should You Set?
For a workflow that runs every 6 hours, I'd set the expected interval to 6 hours with a 1-hour grace period. This accounts for GitHub's delayed scheduling (which can be 15-30 minutes during peak times) plus the workflow's own execution time. If the ping doesn't arrive within 7 hours, something is wrong.
For daily workflows, a 2-hour grace period is usually sufficient. For weekly workflows, I go with 6 hours -long enough to not get false alarms, short enough to catch real failures the same day.
How Do You Prevent the 60-Day Disable?
The most reliable solution is to not rely solely on the cron trigger. Add a workflow_dispatch trigger (shown in the examples above) so you can manually re-enable and run the workflow. Some teams add a separate "keep-alive" workflow that makes a trivial commit to the repo every 50 days, but that feels like a hack.
The better approach: accept that GitHub will disable your workflow eventually, and use external monitoring to catch it when it happens. With a dead man's switch, the 60-day disable becomes a known failure mode that triggers an alert instead of a silent gap. You get the email, re-enable the workflow, and move on.
I've been using this pattern for about a year across a dozen repositories. DeadPing has caught three silent disables and two cases where a merge broke the workflow YAML. Total setup time per workflow is under five minutes: create a monitor, add the secret, add the curl step. The getting started guide covers the whole process, and the API reference is useful if you want to automate monitor creation across many repos.
Start monitoring in 60 seconds
Free forever for up to 20 monitors. No credit card required.
Get Started Free