How to Monitor GitHub Actions Scheduled Workflows
GitHub Actions supports cron-scheduled workflows via schedule triggers. They're convenient — no separate cron server needed. But they have a critical flaw: when they fail, nobody knows.
The problem with scheduled workflows
GitHub Actions scheduled workflows have several failure modes that produce zero alerts:
1. The workflow is disabled — GitHub automatically disables scheduled workflows in repos with no activity for 60 days 2. The schedule doesn't trigger — GitHub doesn't guarantee exact timing; during high load, schedules can be delayed by minutes or skipped entirely 3. The workflow fails silently — unless you've configured notifications (most people haven't), a red X in the Actions tab goes unnoticed 4. The repo is forked — scheduled workflows are disabled by default in forks
GitHub's own documentation warns: "To prevent unnecessary workflow runs, scheduled workflows may be disabled automatically. When a public repository is forked, scheduled workflows are disabled by default."
Adding heartbeat monitoring
The fix is to add a CronSafe ping at the end of your workflow. If the ping doesn't arrive on schedule, you get alerted via Slack, Discord, email, or Telegram.
Basic example
# .github/workflows/daily-sync.yml
name: Daily data sync
on:
schedule:
- cron: '0 6 * * *' # 6 AM UTC daily
workflow_dispatch: # allow manual trigger
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run sync script
run: |
python scripts/sync_data.py
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Ping CronSafe
if: success()
run: curl -s https://api.getcronsafe.com/ping/daily-syncThe if: success() condition ensures the ping only fires when all previous steps passed.
With start/end tracking
For longer workflows, track duration by sending start and end pings:
name: Nightly backup
on:
schedule:
- cron: '0 2 * * *' # 2 AM UTC
workflow_dispatch:
jobs:
backup:
runs-on: ubuntu-latest
steps:
- name: Signal start
run: curl -s https://api.getcronsafe.com/ping/nightly-backup/start
- uses: actions/checkout@v4
- name: Run backup
run: |
pg_dump $DATABASE_URL > backup.sql
gzip backup.sql
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Upload to S3
run: |
aws s3 cp backup.sql.gz s3://my-backups/$(date +%Y%m%d).sql.gz
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Signal success
if: success()
run: curl -s https://api.getcronsafe.com/ping/nightly-backup
- name: Signal failure
if: failure()
run: curl -s https://api.getcronsafe.com/ping/nightly-backup/failThis gives you:
- •Start time tracking
- •Success confirmation with completion time
- •Explicit failure notification with faster alert delivery
With job output
Send the workflow's output with the ping for debugging context:
name: Hourly health check
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run health checks
id: health
run: |
OUTPUT=$(python scripts/health_check.py 2>&1)
echo "$OUTPUT"
echo "result=$OUTPUT" >> $GITHUB_OUTPUT
- name: Ping CronSafe with output
if: success()
run: |
curl -s -X POST https://api.getcronsafe.com/ping/health-check \
-d "${{ steps.health.outputs.result }}"
- name: Report failure
if: failure()
run: |
curl -s -X POST https://api.getcronsafe.com/ping/health-check/fail \
-d "Health check failed"Handling the 60-day inactivity problem
GitHub disables scheduled workflows after 60 days of no repo activity. This is the most common cause of silent failures for scheduled workflows.
Two ways to prevent it:
Option 1: Use workflow_dispatch as backup
Always include workflow_dispatch as a trigger. This lets you manually run the workflow from the Actions tab, which counts as activity.
Option 2: Automate the keep-alive
Create a workflow that commits a timestamp to keep the repo active:
name: Keep alive
on:
schedule:
- cron: '0 0 1 * *' # monthly
jobs:
keepalive:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Update keep-alive timestamp
run: |
date > .github/keep-alive
git config user.name "github-actions"
git config user.email "actions@github.com"
git add .github/keep-alive
git diff --cached --quiet || git commit -m "chore: keep alive"
git pushThis ensures the repo always has recent activity, preventing GitHub from disabling your scheduled workflows.
Setting up the CronSafe monitor
When creating the monitor in CronSafe, set these values:
- •Schedule: Match your workflow's cron expression
- •Grace period: GitHub Actions schedules can be delayed by up to 15 minutes during high load. Set your grace period to at least 20 minutes for anything more frequent than daily, and 1 hour for daily jobs.
Complete production-ready template
name: Scheduled job with monitoring
on:
schedule:
- cron: '*/30 * * * *' # every 30 minutes
workflow_dispatch:
concurrency:
group: scheduled-job
cancel-in-progress: false
jobs:
run:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Signal start
run: curl -fsS https://api.getcronsafe.com/ping/my-job/start
- uses: actions/checkout@v4
- name: Execute job
id: job
run: |
set -euo pipefail
OUTPUT=$(./scripts/my-job.sh 2>&1)
echo "output<<EOF" >> $GITHUB_OUTPUT
echo "$OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Signal success
if: success()
run: |
curl -fsS -X POST https://api.getcronsafe.com/ping/my-job \
-d "${{ steps.job.outputs.output }}"
- name: Signal failure
if: failure()
run: |
curl -fsS -X POST https://api.getcronsafe.com/ping/my-job/fail \
-d "Workflow failed — check Actions tab"Key details in this template:
- •
concurrencyprevents overlapping runs - •
timeout-minuteskills hung workflows - •
curl -fsSfails silently on network errors but shows HTTP errors - •Start/success/fail pings cover all cases
- •Job output is captured and sent with the ping
Start monitoring your cron jobs for free
20 monitors, email alerts, GitHub badges. No credit card required.
Get started free →