How to Monitor a GitHub Actions Scheduled Workflow

GitHub has a quirk that catches almost everyone eventually: a scheduled workflow gets automatically disabled if the repository has no pushed commits for 60 days. There's no dramatic failure, no red X in the Actions tab, the workflow just stops appearing in the run history at all, because GitHub stopped triggering it. On top of that, scheduled workflows aren't guaranteed to fire at the exact time in the cron expression, GitHub's own documentation notes that runs can be delayed during periods of high load on their infrastructure, sometimes by a significant margin, with no notification that it happened.

Both of these failure modes share the same property: nothing in your repository looks wrong. The workflow file is correct, the YAML is valid, and yet the job you were counting on didn't run when you expected. A heartbeat monitor sidesteps the whole problem by watching for the one thing that actually matters, whether the job completed, rather than trying to infer that from GitHub's own scheduling behavior. Here's how to add one using the Downdar API.

What You Will Need

A Downdar API key, and a repository with a scheduled workflow already defined under on: schedule. You'll add the API key as a repository secret, GitHub Actions won't let you reference it directly in a workflow file otherwise.

Creating a Heartbeat Monitor

The /api/v1/heartbeats/create endpoint accepts a POST request with a name and returns the full heartbeat object, including the unique url your workflow will call on completion. This is a one-time setup step, and since a workflow file is just YAML and shell commands, it's often simplest to create the heartbeat directly from your Downdar dashboard and copy the URL rather than scripting the API call from inside a workflow.

If you do want to script it, a plain curl request works from any shell:

# One-off setup, run locally or from any shell. curl -X POST https://downdar.com/api/v1/heartbeats/create \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Nightly dependency audit"}'

The response looks like this:

{ "ok": true, "heartbeat": { "guid": "550e8400-e29b-41d4-a716-446655440000", "name": "Nightly dependency audit", "url": "https://downdar.com/heartbeats/550e8400-e29b-41d4-a716-446655440000", "paused": false }, "capacity": { "limit": 10, "used": 1, "remaining": 9 } }

Add the url as a repository secret named something like DOWNDAR_HEARTBEAT_URL, alongside your API key, under Settings → Secrets and variables → Actions.

Pinging the Heartbeat From Your Workflow

Add a final step to the workflow that curls the heartbeat URL. GitHub Actions already won't run a step if an earlier one in the same job failed, so placing the ping last means it only fires when everything before it succeeded, no extra conditional logic required.

# .github/workflows/dependency-audit.yml name: Nightly dependency audit   on: schedule: - cron: '0 4 * * *'   jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4   - name: Run dependency audit run: npm audit --audit-level=high   - name: Ping Downdar heartbeat if: ${{ success() }} run: curl -fsS -m 10 ${{ secrets.DOWNDAR_HEARTBEAT_URL }}

The if: ${{ success() }} is technically redundant here since it's the default behavior for a step following others in the same job, but writing it explicitly makes the intent obvious to anyone reading the workflow later: this step is the monitoring signal, and it's deliberately conditional on everything above it having worked. If npm audit exits non-zero, the ping step is skipped, and the missing check-in is exactly the alert you want.

This same pattern catches both of the failure modes mentioned earlier. If GitHub silently disables the schedule after 60 days of inactivity, the workflow never runs at all, so the ping never fires, and Downdar notices the gap. If a run is delayed past your grace period during a period of high GitHub Actions load, that shows up too, since the heartbeat is watching for a ping on schedule, not for a green checkmark in your Actions tab that you'd have to go looking for.

Checking Heartbeat Status Programmatically

If you want to check a heartbeat's status from outside GitHub, an internal dashboard, for instance, the /api/v1/heartbeats/status endpoint returns just the essentials.

curl -X POST https://downdar.com/api/v1/heartbeats/status \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"guid": "550e8400-e29b-41d4-a716-446655440000"}'   # Response includes "status": "unknown", "up", "warn", "down", or "paused"

That's enough to display "last audit: healthy" somewhere your team already looks, without anyone needing to dig through a repository's Actions history to check.

A Note on API Key Security

Never put your API key directly in a workflow file, even a private repository's workflow files are visible to anyone with read access, and Actions logs can leak values that aren't handled as secrets. Store both the API key and the heartbeat URL as repository secrets instead:

# Settings -> Secrets and variables -> Actions -> New repository secret DOWNDAR_API_KEY DOWNDAR_HEARTBEAT_URL   # Reference in a workflow as: ${{ secrets.DOWNDAR_API_KEY }}

GitHub automatically masks secret values in workflow logs, so referencing them this way, rather than pasting a key directly into a run step, keeps it out of your build output as well as your repository history.

Get Started

Add the ping step to whichever scheduled workflow would be most likely to go unnoticed if it silently stopped running, then forget about checking the Actions tab for it. That's the point.