Monitor a GitHub Actions scheduled workflow
A scheduled workflow can stop without anyone noticing. GitHub may delay or skip a run, disable the schedule, or the job may start failing in a repository nobody watches. A heartbeat check catches all of these. The Pingstack Heartbeat action reports each run: Pingstack alerts right away when a run fails, and when the next report doesn’t arrive on time.
By the end of this recipe you’ll have a heartbeat check that expects your workflow’s schedule, a repository secret holding its ping URL, and a final workflow step that reports each run.
1. Create a heartbeat check that matches the schedule
Section titled “1. Create a heartbeat check that matches the schedule”Find the cron expression in your workflow:
on: schedule: - cron: '0 2 * * *'Create a heartbeat check and set its
schedule to Cron with the same expression. GitHub Actions schedules run in
UTC unless the schedule sets a timezone with an IANA name, such as
America/New_York. Choose the same timezone on the check: UTC, or the
schedule’s timezone. See
schedules and timezones for how cron expectations
work.
Set a generous grace period. GitHub starts scheduled runs late when its runners are busy, and the job itself takes time. Allow for both: a grace period of 30 to 60 minutes suits most daily jobs. Increase it if your job regularly runs longer.
Attach a notification group so a missed run reaches someone.
2. Store the ping URL as a secret
Section titled “2. Store the ping URL as a secret”Copy the location’s ping URL from the check in Pingstack. Keep it private, because anyone who has it can report a run.
In GitHub, open the repository’s Settings › Secrets and variables ›
Actions, choose New repository secret, and save it as
PINGSTACK_PING_URL. GitHub masks secret values in workflow logs.
3. Report each run as the final step
Section titled “3. Report each run as the final step”Add a last step to the job you’re monitoring:
- name: Report to Pingstack if: always() uses: pingstack-io/heartbeat-action@v1 with: ping-url: ${{ secrets.PINGSTACK_PING_URL }}if: always() makes the step run even when an earlier step failed. The action
reads the job’s result: a successful job sends a heartbeat, a failed one
reports a failure with a link to
the run, and a cancelled one sends nothing. Without if: always(), a failed
run sends nothing and Pingstack alerts only when the deadline passes.
Here is a complete workflow:
name: Nightly export
on: schedule: - cron: '0 2 * * *' workflow_dispatch:
jobs: export: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run the export run: ./scripts/export.sh - name: Report to Pingstack if: always() uses: pingstack-io/heartbeat-action@v1 with: ping-url: ${{ secrets.PINGSTACK_PING_URL }}workflow_dispatch lets you start the workflow by hand, which you’ll use to
test it.
The action masks the ping URL in logs, only sends to pingstack.io, retries
network errors and Pingstack outages, and never fails your job because a
report couldn’t be delivered. Set fail-on-error: true if you want it to.
See the action’s README
for every input.
4. Confirm the first report
Section titled “4. Confirm the first report”In GitHub, open the Actions tab, select the workflow and choose Run workflow. When the run finishes, open the check in Pingstack and confirm the location received a heartbeat and shows a next deadline.
A manual run counts as a check-in like any other. After it, the check expects the next scheduled occurrence.
Optional: report a number
Section titled “Optional: report a number”If the job produces a meaningful count, such as rows exported or files backed up, send it as a numeric value and set warning and critical thresholds on the check. A run that succeeds but exports nothing can then still raise an alert.
- name: Run the export id: export run: echo "rows=$(./scripts/export.sh --count)" >> "$GITHUB_OUTPUT"- name: Report to Pingstack if: always() uses: pingstack-io/heartbeat-action@v1 with: ping-url: ${{ secrets.PINGSTACK_PING_URL }} value: ${{ steps.export.outputs.rows }}Several workflows or jobs
Section titled “Several workflows or jobs”Give each scheduled workflow its own check, with its own secret. If one
workflow has several jobs that must all succeed, report from a final job that
needs the others, so one successful job can’t hide another’s failure:
report: needs: [export, backup] if: always() runs-on: ubuntu-latest steps: - uses: pingstack-io/heartbeat-action@v1 with: ping-url: ${{ secrets.PINGSTACK_PING_URL }} status: ${{ contains(needs.*.result, 'failure') && 'failure' || contains(needs.*.result, 'cancelled') && 'cancelled' || 'success' }}Without the action
Section titled “Without the action”If you’d rather not use a third-party action, a curl step does the same
with a little more YAML:
- name: Report to Pingstack if: always() run: | if [ "$JOB_STATUS" = success ]; then curl --fail --silent --show-error --max-time 10 --retry 3 "$PINGSTACK_PING_URL" elif [ "$JOB_STATUS" = failure ]; then curl --fail --silent --show-error --max-time 10 --retry 3 "$PINGSTACK_PING_URL/fail" fi env: PINGSTACK_PING_URL: ${{ secrets.PINGSTACK_PING_URL }} JOB_STATUS: ${{ job.status }}Passing the secret through env keeps it out of the command that GitHub
prints.
Why a healthy workflow might still alert
Section titled “Why a healthy workflow might still alert”- Timezone mismatch. The check’s timezone must match the schedule’s
timezone, or UTC when the schedule doesn’t set one. - Delayed runs. GitHub can start scheduled runs well after the cron time under load. If you see late alerts that recover on their own, lengthen the grace period.
- Disabled schedules. In public repositories, GitHub disables scheduled workflows after 60 days without repository activity. Re-enable the workflow from the Actions tab.
- Default branch only. Schedules run from the workflow file on the default branch. A schedule added on another branch never runs.
- Missing secret. Forks and pull requests from forks don’t receive repository secrets, so the report step has no URL there. The action warns and sends nothing.
If alerts still don’t match what you expect, see troubleshooting monitoring.