Why a GitHub Actions schedule can stop without telling you
GitHub's docs say a scheduled run can start late or be dropped, and that a schedule can turn itself off:
The schedule event can be delayed during periods of high loads of GitHub Actions workflow runs. High load times include the start of every hour. If the load is sufficiently high enough, some queued jobs may be dropped. To decrease the chance of delay, schedule your workflow to run at a different time of the hour.
In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days.
A run that's dropped or turned off never starts, so GitHub has no failure to email you about. When a run does fail, GitHub's docs say “Notifications for scheduled workflows are sent to the user who last modified the cron syntax in the workflow file.” That may be someone else.
Sereno knows when each run is due. When no report arrives in time, it marks the run missed and alerts you.
Before you start
- A check in Sereno for this job, with the cron from the workflow's schedule. GitHub's docs say “By default, scheduled workflows run in UTC.” They also say “You can optionally specify a timezone using an IANA timezone string for timezone-aware scheduling.” So set the check's time zone to UTC, or to the zone your schedule names on its
timezone:line. Make it with New check on your dashboard, which takes the same five-field cron expression, or let your AI agent set it up. - A grace period of 1 hour plus the job's longest run time. Grace is how late a run's report can come before the run counts as missed, and the report line below sends its report only when the job ends.
- Your project's report address, the value for ALERTS_URL. It looks like
https://asksereno.com/k/PROJECT_KEY, with no check slug at the end. The check's page shows it under This project's report address, ready to copy.
Add the report line
Save this project's report address as a repository secret named ALERTS_URL (Settings → Secrets and variables → Actions), or with the GitHub CLI, which asks for it.
gh secret set ALERTS_URLgh secret set ALERTS_URL
Run that line inside your repository's folder, or add -R OWNER/REPO to name the repository.
Add this as the last step of the scheduled job: it always runs, and reports how the job went. job.status covers only the job the step is in, so in a workflow with several jobs, add it to each job you want watched, each with its own check. Runs started another way (by hand, or on a push) report too, and a failed one alerts. Scheduled workflows run only from the default branch, and often start late. GitHub runs schedules in UTC unless a timezone: line under the workflow's cron line names another zone. Give the check the same time zone.
- name: Report to Sereno if: always() env: ALERTS_URL: ${{ secrets.ALERTS_URL }} STATUS: ${{ job.status }} RID: ${{ github.run_id }}-${{ github.run_attempt }} shell: bash run: | if [ "$STATUS" = success ]; then kind=ok; else kind=fail; fi curl -fsS -m 10 --retry 5 --retry-connrefused --retry-delay 10 --retry-max-time 60 -o /dev/null --data-binary "job $STATUS: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" "${ALERTS_URL%/}/CHECK_SLUG/$kind?rid=$RID"- name: Report to Sereno if: always() env: ALERTS_URL: ${{ secrets.ALERTS_URL }} STATUS: ${{ job.status }} RID: ${{ github.run_id }}-${{ github.run_attempt }} shell: bash run: | if [ "$STATUS" = success ]; then kind=ok; else kind=fail; fi curl -fsS -m 10 --retry 5 --retry-connrefused --retry-delay 10 --retry-max-time 60 -o /dev/null --data-binary "job $STATUS: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" "${ALERTS_URL%/}/CHECK_SLUG/$kind?rid=$RID"
Put the step at the end of the job your schedule runs, indented like its other steps. if: always() runs it even when an earlier step failed, so a failed run reports fail, with a link to the run.
In a matrix job, every copy shares one run id, so only the first copy's report counts. Put the step in its own last job with needs: JOB_ID and if: always(), and set STATUS to ${{ needs.JOB_ID.result }}, with the matrix job's id in place of JOB_ID.
While Sereno can't be reached, this step keeps trying for about a minute. If it still can't, the step fails, and the run shows as failed in GitHub even when the job worked.
CHECK_SLUG stands for your check's slug, which Sereno makes from the check's name. The report line adds it to ALERTS_URL itself, so the variable holds only your project's report address. On the check's page, these lines have the slug filled in.
Send a test report
From a terminal on your own computer, run this. The check's page in Sereno shows this line with your project's key and your check's slug filled in, under Check it can reach us.
curl -fsS -m 10 https://asksereno.com/k/PROJECT_KEY/CHECK_SLUG/testcurl -fsS -m 10 https://asksereno.com/k/PROJECT_KEY/CHECK_SLUG/test
It prints OK. On the check's page, Test report gets a tick. A test report never changes the check's status.
That proves the address and the slug are right. GitHub's side shows on the first scheduled run: Real run gets a tick, and the check shows OK, or Failing if the run failed. To see it sooner, start the workflow by hand: open the repository's Actions tab, pick the workflow in the left sidebar and click Run workflow. GitHub's docs say “To run a workflow manually, the workflow must be configured to run on the workflow_dispatch event.” Add it next to your schedule, and keep your own cron line:
on: schedule: - cron: "17 3 * * *" workflow_dispatch:on: schedule: - cron: "17 3 * * *" workflow_dispatch:
A run you start this way is a real run. It can change the check's status, and one close to the next scheduled time can count as that run.
If it doesn't work
The test fails
- The test prints
curl: (22) The requested URL returned error: 404. Sereno doesn't know that address. Open the project's page in Sereno: if the test shows under Reports for checks that don't exist, the slug is wrong. If it doesn't show there, the project key is wrong. - The test prints error 429. That check got more than 60 reports in a minute: wait a minute and try again. An error of 500 or above means Sereno couldn't answer just then, so try again in a few minutes. Every answer a report address gives is in the docs.
The test works, but scheduled runs don't report
- The Report to Sereno step fails with error 404. The slug in the step, or the address in ALERTS_URL, is wrong. Open the project's page in Sereno: if the report shows under Reports for checks that don't exist, fix CHECK_SLUG in the step. If it doesn't, set the ALERTS_URL secret again.
- The Report to Sereno step fails with a curl error about the URL. The ALERTS_URL secret is missing or empty: set it again with the first line above.
- No run at all at the scheduled time. GitHub's docs say “Scheduled workflows will only run on the default branch.” Check that the workflow file is on that branch.
- The schedule turned itself off after 60 days without activity. To turn it on again, open the repository's Actions tab, pick the workflow in the left sidebar and click Enable workflow, as GitHub's steps show. In a fork, GitHub's docs also say “When a public repository is forked, scheduled workflows are disabled by default.”
- Sereno counted a run as missed, then its report came late. Raise the check's grace period in its Settings, and move the schedule off the start of the hour, as GitHub's docs advise above.
The short version of this setup, with the other platforms', is in Sereno's setup recipes.
GitHub's docs quoted as read on October 26, 2026.
More guides
- Ask your AI agent why a cron job failed
- Monitor Vercel cron jobs
- Monitor Supabase cron jobs that call Edge Functions
- Get an alert when a cron job fails or doesn't run
- Healthchecks.io alternative with AI agent setup
- Cronitor alternative with 200 free checks
New here? Sign up with your email. Free: up to 200 checks, 50 alert emails a day, and phone alerts through ntfy.