← Blog  ·  October 2026

GitHub Actions Cron Job Monitoring: Get Alerts When Scheduled Workflows Fail

Your nightly workflow ran Tuesday. It hasn't run since. You found out Friday — from a broken deploy.

GitHub Actions schedule: triggers are convenient. You write a cron expression, commit a workflow file, and the job runs on a cadence. What the UI doesn't surface clearly: when a scheduled workflow stops running — or runs but fails — the default notification behavior is nearly silent. No dashboard alert. No Slack message. Often, just a quiet failure that nobody catches until downstream breakage appears.

This article covers why GitHub Actions cron jobs fail silently, three concrete approaches to monitor them, and how to set up an external heartbeat monitor with a single workflow step.


Why GitHub Actions scheduled workflows fail silently

There are three distinct failure modes, each with different observability characteristics.

The 60-day inactivity freeze

GitHub automatically disables scheduled workflows on repositories with no activity for 60 days. "Activity" means commits, pull requests, or other repository events — not workflow runs themselves. A repo that has only a scheduled job (and nothing else happening) is especially vulnerable: the workflow keeps the repo "active" in your mind, but GitHub doesn't count workflow runs as activity. When the freeze triggers, GitHub sends one email notification. If you miss it, the workflow just stops. For the full list of causes, see our post on GitHub Actions scheduled workflows not running.

Infrastructure-level skips

GitHub's scheduler is best-effort. Under high load, queued scheduled runs can be delayed or dropped entirely. The GitHub documentation acknowledges this. In practice it's rare, but for workflows with downstream dependencies — nightly data processing, billing cycles, time-sensitive cleanup — a dropped run at 2 AM may not surface until the next morning when something is obviously broken.

The job runs but the steps fail quietly

GitHub does send email on workflow failure — but only to the repository owner, and only if the failure is a hard exit. Steps marked continue-on-error: true swallow failures silently. Business logic that catches exceptions and exits with code 0 looks like success to GitHub. A workflow that completes in 2 seconds when it should take 4 minutes completed, technically, without error.

The core problem GitHub notifies you when a workflow fails. It does not notify you when a workflow stops running. These are two different things, and the second one is usually harder to catch.

Three approaches to monitor GitHub Actions scheduled workflows

Approach 1: External heartbeat monitor (most reliable)

An external heartbeat monitor inverts the alerting logic. Instead of watching for a failure signal, you watch for the absence of a success signal. Your workflow sends an HTTP ping at the end of each successful run. If the monitor doesn't receive the ping within a defined window, it alerts you.

This is the only approach that catches all three failure modes — job skipped, job failed, job ran too slowly — with a single mechanism. The monitor doesn't care why the ping didn't arrive. It just alerts when the expected heartbeat is missing.

Tools in this space: CronWatch, healthchecks.io, Better Uptime, and Dead Man's Snitch. See our comparison of cron monitoring tools for a detailed breakdown.

Approach 2: Slack notification step in the workflow

Add a step that posts to Slack on failure. This is more visible than email but still only catches explicit failures — not missed runs. It's a good complement to a heartbeat monitor, not a replacement.

- name: Notify Slack on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    channel-id: 'C12345678'
    slack-message: "Nightly sync failed in ${{ github.repository }}"
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

The limitation: if: failure() only fires when the job actually runs and fails. A skipped or frozen workflow never triggers it.

Approach 3: Status badge monitoring

GitHub provides a workflow status badge URL for every workflow. You can embed this badge in your README or monitor it with an uptime checker. When a workflow fails, the badge changes from passing to failing.

![Nightly Sync](https://github.com/your-org/your-repo/actions/workflows/nightly.yml/badge.svg)

This is a passive approach — useful for at-a-glance status in a README, but it requires someone to look at it. An uptime monitor polling the badge URL can make it active, but you're now running infrastructure to monitor infrastructure.


Setting up an external heartbeat monitor

The heartbeat approach requires two things: a monitoring service that expects periodic pings, and a workflow step that sends the ping on successful completion.

The workflow step is one line:

- name: Heartbeat
  if: success()
  run: curl -s https://your-monitor-url/heartbeat/TOKEN

Here's a complete example of a monitored nightly workflow:

name: Nightly Data Sync

on:
  schedule:
    - cron: '0 3 * * *'   # 3 AM UTC every night

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run sync
        run: python sync.py

      - name: Heartbeat
        if: success()
        run: curl -s https://your-monitor-url/heartbeat/TOKEN

The if: success() condition ensures the heartbeat only fires when all previous steps passed. If python sync.py exits non-zero, the heartbeat step is skipped, the monitor sees a missing ping, and you get alerted.

Configuring the monitor window

Most heartbeat monitors let you configure a grace period — how long after an expected ping the monitor waits before alerting. For a daily job, a reasonable setting is 30–60 minutes. This absorbs GitHub's scheduler delay variance without generating false positives.

For a weekly job (0 3 * * 1), set the grace period to 1–2 hours. The longer the schedule interval, the more margin you need for scheduler variance.

Validating your cron expression

Before deploying a monitored workflow, verify the cron expression fires when you expect it to. GitHub Actions uses UTC — a job at 0 9 * * * is 9 AM UTC, which may be 1 AM or 2 AM Pacific depending on daylight saving. Use the cron expression parser to preview the next 10 run times before committing.


What to monitor beyond the heartbeat

Once a heartbeat is in place, there are two additional signals worth capturing.

Job duration

A job that completes but takes 10× longer than usual is a signal — an API is slow, a dataset grew unexpectedly, or a resource is constrained. GitHub Actions records run duration in the workflow UI, but doesn't alert on anomalous duration. Some external monitors let you set a maximum expected duration alongside the grace period, alerting when a run takes too long even if it eventually succeeds.

Output validation

A job that succeeds structurally but produces wrong output — 0 rows synced, empty report, empty S3 object — is a silent failure that heartbeat monitoring won't catch. Add an explicit assertion step before the heartbeat ping:

- name: Validate output
  run: |
    ROW_COUNT=$(python -c "import json; d=json.load(open('output.json')); print(len(d['rows']))")
    if [ "$ROW_COUNT" -lt "1" ]; then
      echo "Output validation failed: 0 rows"
      exit 1
    fi

- name: Heartbeat
  if: success()
  run: curl -s https://your-monitor-url/heartbeat/TOKEN

With this pattern, a zero-row sync fails the job explicitly, skips the heartbeat ping, and triggers the monitor alert — all without any separate alerting infrastructure.


Putting it together

The minimum viable monitoring setup for a GitHub Actions scheduled workflow:

  1. Sign up for a heartbeat monitoring service — CronWatch, healthchecks.io, or similar. Create a monitor for your job with the expected schedule and a 30–60 minute grace period.
  2. Add one step to your workflow — curl -s https://your-monitor-url/heartbeat/TOKEN with if: success().
  3. Add a Slack failure step — as a secondary alert for explicit failures.
  4. Validate your cron expression — use the cron expression parser to confirm the schedule fires when expected in UTC.
  5. Add output assertions before the heartbeat — catch silent logic failures that produce code 0 but wrong results.

With this setup, you get alerted within an hour whether the workflow was frozen, failed, skipped, or ran but produced bad output. That covers the vast majority of real-world failure modes with less than 20 lines of additional YAML.

📟 Monitor all your cron jobs — including GitHub Actions

CronWatch monitors your scheduled workflows across GitHub Actions, crontab, AWS EventBridge, Kubernetes CronJob, and more. Get alerted in Slack or email within minutes of a missed or failed run. One heartbeat URL. Every platform.

Join the CronWatch waitlist → Parse a cron expression

← All articles  ·  CronWatch  ·  Cron parser tool