GitHub Actions Scheduled Workflows — A Complete Guide
GitHub Actions' `on.schedule.cron` trigger is the easiest way to run a workflow on a recurring schedule, but it has two real constraints — a 5-minute minimum interval, and silent disabling of public-repository schedules after 60 days of inactivity — that catch every engineer at least once. Schedules default to UTC, though since March 2026 an optional per-schedule `timezone:` field lets you pin a local zone. Every spec claim on this page is quoted from GitHub's own documentation and linked to its source, verified 2026-07-24.
Updated
The spec, precisely
Every row here is what GitHub’s own documentation states, with the primary source linked. Where GitHub is silent, this table says so rather than guessing — the gaps matter as much as the rules.
| Property | What GitHub documents |
|---|---|
| Syntax | POSIX cron syntax, five fields |
| Fields | minute hour day-of-month month day-of-week |
| Seconds field | None. Not part of the 5-field format |
| Operators | Exactly four: * (any value), , (list separator), - (range), / (step) |
| Nicknames | Not supported — @yearly, @monthly, @weekly, @daily, @hourly, @reboot |
L, W, #, ? | Not documented, not supported |
| Minimum interval | 5 minutes |
| Default timezone | UTC |
timezone: key | Optional IANA timezone string, per schedule entry (since March 2026) |
| DST spring-forward | Schedules in skipped hours advance to the next valid time |
| DST fall-back | Not documented — see below |
| Branch | Runs on the latest commit on the default branch only |
| Inactivity disable | Public repositories, after 60 days with no repository activity |
| Timing guarantee | None. May be delayed under load, and queued jobs may be dropped |
Four of these are worth the exact wording, because they’re the ones most often stated wrong elsewhere.
On the minimum interval — the docs say simply:
The shortest interval you can run scheduled workflows is once every 5 minutes.
Note what this does not say: there’s no documented statement that a shorter expression is rejected, errors, or is coerced. * * * * * is valid POSIX cron and a workflow file containing it validates and commits cleanly. It just won’t run every minute. Treat 5 minutes as the floor and don’t write an interval you won’t get.
On nicknames, the docs are unusually explicit:
GitHub Actions does not support the non-standard syntax
@yearly,@monthly,@weekly,@daily,@hourly, and@reboot.
So @daily is not a shorter way to write 0 0 * * * here — it’s a schedule that never fires.
On DST, this is the sentence people are looking for:
For schedules that set
timezoneto a time zone that observes daylight saving time (DST), during DST spring-forward transitions, scheduled workflows in skipped hours advance to the next valid time. For example, a 2:30 AM schedule advances to 3:00 AM.
Spring-forward is therefore defined behaviour. Fall-back is not. GitHub documents nothing about what happens when the local hour repeats, so a schedule inside that hour has no documented guarantee of running once rather than twice. If a job is not idempotent and lands between 1 and 3 AM local, either move it outside the transition window or omit timezone: and schedule it in UTC.
On the inactivity rule, the qualifier is load-bearing:
In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days.
This applies to public repositories. Widely repeated as a blanket rule for every repository, it isn’t one — private repositories are not covered by that statement.
Primary sources: Events that trigger workflows — schedule, Workflow syntax — on.schedule, and Disabling and enabling a workflow. Verified 2026-07-24.
The schedule trigger
The minimal scheduled workflow:
name: nightly-report
on:
schedule:
- cron: '0 0 * * *'
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/generate-report.sh
The schedule trigger accepts an array of cron entries, each one a standard 5-field Unix cron expression. See the Unix cron dialect reference for the full syntax — fields are minute, hour, day-of-month, month, and day-of-week.
GitHub Actions doesn’t support the @hourly / @daily / @weekly / @monthly / @yearly shortcuts. Write the explicit expression every time:
| What you want | Write |
|---|---|
| Hourly | 0 * * * * |
| Daily at midnight | 0 0 * * * |
| Weekly on Sunday | 0 0 * * 0 |
| First of month | 0 0 1 * * |
@reboot isn’t supported either (there’s no notion of a “system boot” in a CI runner).
5-minute minimum interval
* * * * * does not work on GitHub Actions. The platform enforces a 5-minute minimum schedule interval. Cron expressions that would fire more often than every 5 minutes are technically accepted by the YAML parser, but GitHub will simply not trigger the workflow at those times.
The fastest valid schedule is */5 * * * * (every 5 minutes). If you need sub-5-minute precision, GitHub Actions is the wrong tool:
- Cloudflare Workers cron triggers go down to 1 minute
- AWS EventBridge Scheduler goes down to 1 minute
- A self-hosted runner with a local cron daemon (every minute, calls back into GitHub)
- A long-running service (Heroku, Render, Fly) with an in-process scheduler
Timezones (UTC default + timezone field)
Cron expressions in on.schedule default to UTC. Since March 2026, you can pin a specific zone by adding an optional timezone: field — an IANA name — as a sibling of cron: in the same list item:
on:
schedule:
- cron: '0 9 * * 1-5'
timezone: 'America/Los_Angeles' # runs 9 AM Pacific year-round
GitHub handles daylight saving on the spring-forward side, and documents it precisely: “during DST spring-forward transitions, scheduled workflows in skipped hours advance to the next valid time. For example, a 2:30 AM schedule advances to 3:00 AM.” So “9 AM Pacific” stays 9 AM through both PST and PDT with a single entry.
Fall-back is not documented. GitHub says nothing about the autumn transition, when the local hour repeats — so a schedule landing inside the repeated hour has no documented guarantee that it fires once rather than twice. If the job isn’t idempotent and sits between 1 and 3 AM local, either move it outside that window or drop timezone: and schedule it in UTC, where the ambiguity doesn’t exist.
Before this feature GitHub Actions was UTC-only, and the standard workaround was two cron entries (one for PST, one for PDT) gated with a date-range check on github.event.schedule. If you see that pattern in older workflows or blog posts, it’s now obsolete — replace it with a single cron: + timezone: pair.
If you don’t need local-time alignment, omit timezone: and everything runs in UTC as before.
Multiple schedules
A workflow can have multiple cron entries:
on:
schedule:
- cron: '0 9 * * 1-5' # Weekdays at 9 AM UTC
- cron: '0 0 * * *' # Every day at midnight UTC
Inside the workflow, github.event.schedule tells you which one fired:
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Morning standup
if: github.event.schedule == '0 9 * * 1-5'
run: ./scripts/standup.sh
- name: Nightly cleanup
if: github.event.schedule == '0 0 * * *'
run: ./scripts/cleanup.sh
This pattern lets you run different work on different schedules from a single workflow file. Useful for: morning + evening reports, weekday + weekend operations, different timezone variants of the same job.
Inactive repositories
In a public repository, GitHub disables scheduled workflows when no repository activity has occurred in 60 days. The disabling is silent — no email, no PR comment, no Actions log entry. Your workflow just stops running.
The public repository qualifier is part of the documented rule and is routinely dropped when this gets repeated. GitHub’s wording is “In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days” — private repositories are not covered by that statement. If you’re on a private repo, this is not the reason your schedule stopped; look at reliability and delays or a disabled workflow instead.
“Activity” means a push, a release, a PR merge — anything that modifies the repository. Issue comments and stars don’t count.
To prevent this:
- Have actual development activity. Repositories that ship regularly never hit the limit.
- Add a no-op keepalive workflow that pushes a small commit every 50 days (e.g., updating a
last-active.txtfile or bumping a version tag). - Use a different scheduler. EventBridge Scheduler, Cloudflare Workers cron, or Vercel cron don’t have this restriction. Have them dispatch the GitHub workflow via
gh workflow runor the REST API.
You can re-enable a disabled workflow via the Actions tab UI or with gh workflow enable <workflow-name> from the CLI.
Reliability and delays
GitHub’s own docs explicitly warn:
Scheduled workflows may be delayed during periods of high loads of GitHub Actions workflow runs. High load times include the start of every hour.
In practice this means:
- At the top of every hour, scheduled workflows are commonly delayed by 5–30 minutes.
- At :30 of every hour, delays are typically much shorter.
- During GitHub-wide incidents, workflows can be delayed by hours or skipped entirely.
If precise timing matters (an audit, a regulatory report, a downstream system with a tight SLA), don’t use GitHub Actions cron. Use a more reliable scheduler — EventBridge Scheduler, Cloudflare Workers cron, or a self-hosted runner with local cron — and have it dispatch the GitHub workflow.
A trick to reduce average delay: schedule at minute 7, 13, 23, etc. instead of 0 or 30. The platform processes a wave of scheduled work at predictable times and off-peak minutes get serviced faster.
Pair with workflow_dispatch
Every scheduled workflow should also be manually triggerable. Add workflow_dispatch: to the on: block:
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
This adds a “Run workflow” button in the Actions tab. Use it to:
- Test the workflow without waiting for the next scheduled fire
- Re-run after a transient failure
- Trigger an emergency execution outside the schedule
You can also trigger it programmatically:
gh workflow run nightly-report.yml
Or via the REST API (good for paired schedulers like EventBridge dispatching into GitHub).
For workflows that take parameters when manually triggered:
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
dry-run:
description: 'Run without making changes'
type: boolean
default: false
jobs:
report:
runs-on: ubuntu-latest
steps:
- run: |
if [ "${{ github.event.inputs.dry-run }}" = "true" ]; then
echo "Dry run mode"
else
./scripts/generate-report.sh
fi
Debugging
When a scheduled workflow isn’t behaving:
- Confirm the workflow is on the default branch. GitHub’s docs are unambiguous: “Scheduled workflows run on the latest commit on the default branch.” A
schedule:trigger sitting on a feature branch will never fire, no matter how correct the cron is — and because the workflow file is otherwise valid, nothing warns you. This is the single most common cause of “my new scheduled workflow does nothing,” since the natural way to add one is on a branch behind a PR. It only starts running once merged. - Verify the cron expression parses. Paste it into the parser with the GitHub Actions dialect selected. Check the next-run times match your intent (and remember they’ll be in UTC unless you set
timezone:). - Check the Actions tab. Filter by event type “schedule” to see only scheduled runs. Compare run timestamps with the schedule to spot delays.
- Check repository inactivity. Go to Actions tab → if the workflow is grayed out or marked as disabled, that’s the inactive-repo trap.
- Add a smoke step at the start. A first step that just
echo "Schedule: ${{ github.event.schedule }} at $(date -u)"makes it easy to see in logs which schedule fired and when. - Manually trigger via
workflow_dispatchto confirm the workflow itself works. If it runs manually but not on schedule, the schedule is the issue (delay, disabled, wrong cron).
For workflows that absolutely must run on time, set up a separate canary alert: a different system pings a webhook every time the scheduled workflow completes successfully, and alerts if 90+ minutes elapse without one. GitHub Actions’ own status page is too coarse to catch per-workflow degradation.
Cron on other platforms
Running scheduled work on a different platform? Each handles cron a little differently:
- Cloudflare Workers — Cron Triggers on the edge
- Vercel — cron jobs via vercel.json
- Kubernetes — CronJob objects
- AWS EventBridge — Scheduler for Lambda & 270+ services
- systemd — timer units on Linux hosts
Frequently asked questions
Can I run a GitHub Actions workflow every minute?
Why didn't my scheduled workflow run on time?
Why did my scheduled workflow stop running?
How do I schedule a workflow in my local timezone?
Can I trigger a scheduled workflow manually for testing?
Related
Every 5 Minutes
`*/5 * * * *` — the fastest schedule GitHub Actions allows.
PatternDaily at Midnight
`0 0 * * *` — runs at 00:00 UTC every day.
ToolCron Parser
Paste any expression to verify it parses correctly for GitHub Actions.
GuideSub-Minute Scheduling
Actions floors at five minutes. The full per-runtime minimum-interval matrix.
GuideCron Dialect Comparison
How GitHub Actions cron differs from Unix, Quartz, Kubernetes, and others.
GuideHow Cron Expressions Work
Field-by-field guide to the 5-field cron syntax GitHub Actions uses.