← Blog  ·  October 2026

GitHub Actions Scheduled Job Monitoring: Know When Your Cron Workflows Stop Running

Your nightly sync workflow ran fine for months. Then GitHub quietly disabled it. You found out when the data was stale.

The 60-day silence rule: GitHub automatically disables scheduled workflows on repositories that have had no commit activity for 60 days. No notification is sent. The jobs simply stop running. Everything looks normal in the Actions tab until you go looking for the missing runs.

GitHub Actions on: schedule workflows are one of the most popular ways to run recurring automation — nightly reports, stale-issue sweeps, dependency updates, data syncs, cache warm-ups. They're easy to set up and cheap to run. They're also surprisingly easy to lose track of, because GitHub gives you almost no visibility when a scheduled workflow stops executing.

This guide covers every failure mode for GitHub Actions cron jobs, how to detect them, and how to add reliable external monitoring so you know immediately when a scheduled run is missed.


The 60-Day Inactivity Disable: The Failure Nobody Expects

GitHub's documented behavior: scheduled workflows are automatically disabled if the repository has no activity for 60 days. "Activity" means commits, pull requests, or other pushes to the default branch — not workflow runs themselves.

This creates a specific trap for utility repositories. Consider a small repo that exists solely to run a nightly script — a backup job, an inventory sync, a report mailer. Once the initial setup is committed, there's no ongoing code activity. The repo sits idle. Sixty days later, GitHub disables the scheduled workflow. The repository owner gets no email, no notification, and no indication in the GitHub UI unless they actively navigate to the Actions tab and notice the workflow shows as "disabled."

How to check if your workflow was disabled: Go to the repository → Actions tab → click the scheduled workflow. If it has been auto-disabled, you'll see a banner that says "This workflow has a schedule trigger, but schedules are disabled for inactive repositories." You can re-enable it with one click — but you have to know to look.

The fix is to keep the repository minimally active. A common workaround is a second workflow that commits a timestamp file every 30 days, but this adds complexity. The more reliable solution is external monitoring that alerts you when the expected run doesn't happen.


Other Failure Modes for GitHub Actions Cron Jobs

The 60-day disable is the most invisible failure, but it's not the only one.

Queue delays during high-traffic periods

GitHub Actions runs on shared infrastructure. During incidents, major events, or peak usage times, scheduled workflows can be delayed by minutes to hours — or skipped entirely if the queue backlog causes them to miss their window. GitHub doesn't retry missed scheduled runs; if the slot was missed, that run is gone.

Billing limits and spending caps

Free GitHub accounts include a monthly compute budget for Actions minutes. Private repositories draw from this budget; public repositories run for free. If your account hits its spending cap — or if a billing card fails and the account is restricted — scheduled workflows stop running without warning. The error state shows in the billing settings, not in the Actions tab.

Runner exhaustion on self-hosted setups

If your organization uses self-hosted runners, a scheduled workflow queues up and waits for an available runner. If all runners are busy, crashed, or offline, the job stays queued indefinitely. GitHub will eventually cancel long-queued jobs, but there's no alert sent when this happens.

Workflow file errors after a merge

A pull request that changes the workflow YAML can introduce a syntax error or invalid expression that only manifests at runtime. The workflow runs until the bad change is merged, then silently fails. The failure appears in the Actions tab — but only if someone goes looking.

Cron expression misconfiguration

GitHub Actions uses UTC for all cron expressions. A workflow meant to run at 3 AM local time will run at a different UTC hour depending on DST transitions and the team's time zone. If the intended window is critical (e.g., "before business opens"), a misconfigured expression can mean the job runs at the wrong time without technically failing.

You can verify any cron expression before committing it:

🕐 Parse your cron expression

Enter any cron expression to see exactly when it will next fire, what the human-readable schedule means, and whether there are edge cases (DST, leap years, end-of-month behavior).

Free cron parser →

How to Detect Missed GitHub Actions Runs

Option 1: Check the Actions tab manually

The most obvious approach, and the least reliable. You can go to the repository Actions tab, filter by the scheduled workflow, and verify the last successful run timestamp. For critical workflows, this means manual checking on a regular schedule — which is easy to forget and doesn't work across many repositories.

Option 2: Add a self-monitor step to the workflow

A workflow can check its own recent run history using the GitHub API and fail loudly if the previous run was too long ago:

- name: Check for missed previous run
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    LAST_RUN=$(gh run list \
      --workflow=nightly-sync.yml \
      --status=success \
      --limit=1 \
      --json=createdAt \
      --jq='.[0].createdAt')
    
    HOURS_SINCE=$(( ($(date +%s) - $(date -d "$LAST_RUN" +%s)) / 3600 ))
    
    if [ "$HOURS_SINCE" -gt 26 ]; then
      echo "ERROR: Last successful run was $HOURS_SINCE hours ago"
      exit 1
    fi

This step only runs if the workflow itself runs — it won't catch the case where the workflow stops executing entirely. It's useful for catching partial failures (the workflow runs but the logic inside breaks), not missed runs.

Option 3: Dead man's switch with external monitoring

The only approach that catches every failure mode — including the 60-day disable — is a dead man's switch: your workflow sends an HTTP ping to an external monitoring service at the end of each successful run. If the monitoring service doesn't receive the ping within the expected window, it sends an alert.

Implementation is a single step at the end of your workflow:

name: Nightly Sync
on:
  schedule:
    - cron: '0 3 * * *'  # 3:00 AM UTC daily

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run sync
        run: python sync.py
      
      - name: Notify CronWatch (heartbeat)
        if: success()
        run: |
          curl -fsS "https://cronwatch.example.com/heartbeat/${{ secrets.CRONWATCH_TOKEN }}"

The if: success() condition ensures the ping only fires when the sync step completes without errors. If the sync fails, the heartbeat step is skipped, and the monitoring service alerts you after the expected window passes.

Why this catches the 60-day disable: When GitHub disables your workflow, no run happens, so no ping is sent. The monitoring service doesn't receive the expected heartbeat for 24+ hours and fires an alert — even though nothing "failed" in the traditional sense.

Writing a Reliable Scheduled Workflow

Beyond monitoring, a few structural choices make scheduled workflows more resilient:

Use workflow_dispatch alongside schedule

Adding a manual trigger lets you re-run a missed job on demand without modifying the code:

on:
  schedule:
    - cron: '0 3 * * *'
  workflow_dispatch:  # allows manual trigger from Actions UI or API

Add explicit timeouts

Without a timeout, a hung job occupies a runner indefinitely and blocks subsequent runs. Set a job-level timeout:

jobs:
  sync:
    runs-on: ubuntu-latest
    timeout-minutes: 30  # force-kill if the job exceeds this
    steps:
      - ...

Use concurrency controls to prevent overlap

If a scheduled run takes longer than its period, the next run can start before the previous finishes. Use concurrency to prevent stacked runs:

concurrency:
  group: nightly-sync
  cancel-in-progress: false  # let the running job finish; queue the next one

Pin action versions

Using @main or @latest for actions means a third-party change can silently break your workflow. Pin to a specific commit SHA for scheduled jobs where reliability matters:

- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

Monitoring Across Many Repositories

If your organization has dozens of repositories with scheduled workflows, manual checking is not viable. The approaches that scale:


What to Do When a Scheduled Workflow Stops Running

When you discover a scheduled workflow hasn't run, work through this checklist:

  1. Go to Actions tab → check if the workflow shows as disabled (60-day inactivity)
  2. Check the most recent runs — look for queued jobs that never started, or jobs cancelled mid-run
  3. Check GitHub status at githubstatus.com for any Actions incidents during the missed window
  4. Check your GitHub billing settings — verify the account isn't restricted
  5. Look at the workflow YAML for any recent changes merged since the last successful run
  6. Try triggering the workflow manually via workflow_dispatch to verify it runs successfully
  7. If using self-hosted runners, check runner health in Settings → Actions → Runners

🔔 Get alerted when your GitHub Actions cron jobs stop running

CronWatch monitors your scheduled workflows with a dead man's switch. Add one curl step to your workflow, set the expected interval, and get alerted via email or Slack if any run is missed — including the silent 60-day GitHub disable.

Join the CronWatch waitlist → Parse a cron expression →

← All articles  ·  CronWatch  ·  Cron parser tool