cronuru
Guide

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 wantWrite
Hourly0 * * * *
Daily at midnight0 0 * * *
Weekly on Sunday0 0 * * 0
First of month0 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:

  1. Have actual development activity. Repositories that ship regularly never hit the limit.
  2. Add a no-op keepalive workflow that pushes a small commit every 50 days (e.g., updating a last-active.txt file or bumping a version tag).
  3. 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 run or 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_dispatch to 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?
No. GitHub Actions enforces a minimum schedule interval of 5 minutes. `* * * * *` is technically valid syntax but will not trigger — the fastest valid expression is `*/5 * * * *`.
Why didn't my scheduled workflow run on time?
GitHub explicitly warns that scheduled workflows can be delayed during periods of high Actions load. Delays of 5–30 minutes are common; longer delays happen during peak usage. There's no SLA on scheduled run timing.
Why did my scheduled workflow stop running?
Most likely your repository has been inactive for 60+ days. GitHub disables scheduled workflows in inactive repositories to conserve resources. Push any commit to re-activate, or re-enable via the GitHub API.
How do I schedule a workflow in my local timezone?
Since March 2026 you can add a `timezone:` field (an IANA name) next to the cron entry, and GitHub adjusts for daylight saving automatically: `- cron: '0 9 * * 1-5'` with `timezone: 'America/Los_Angeles'` on the following line runs at 9 AM Pacific year-round. Before this feature GitHub Actions was UTC-only and you had to offset the hour manually with two cron entries — that workaround is no longer needed.
Can I trigger a scheduled workflow manually for testing?
Yes — add `workflow_dispatch:` alongside `schedule:` and the workflow gets a 'Run workflow' button in the Actions tab. Pair this with every scheduled workflow.