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 after 60 days of repository 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.
Updated
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 automatically — during a spring-forward transition, a schedule that lands in the skipped hour advances to the next valid time. So “9 AM Pacific” stays 9 AM through both PST and PDT with a single entry.
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
GitHub disables scheduled workflows in repositories with no activity for 60+ days. The disabling is silent — no email, no PR comment, no Actions log entry. Your workflow just stops running.
“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:
- 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).
- 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.
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.
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.