Skip to content

Scheduled Workflows


You’ve already seen schedule fire a workflow on a timer. Now it’s worth slowing down on the syntax itself, because cron expressions are notoriously easy to get subtly wrong, and GitHub’s scheduler has a few behaviors that catch people off guard even once the cron string itself is correct.

Cron Syntax Basics

A cron expression has five fields, in order: minute, hour, day of month, month, day of week.

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6, Sunday = 0)
│ │ │ │ │
* * * * *

Some common patterns:

0 3 * * *        # every day at 03:00 UTC
0 */6 * * *      # every 6 hours
0 9 * * 1-5      # 09:00 UTC, Monday through Friday
30 2 1 * *       # 02:30 UTC on the 1st of every month

Try one directly:

mkdir gha-cron-demo && cd gha-cron-demo
git init
mkdir -p .github/workflows
cat > .github/workflows/scheduled.yml << 'EOF'
name: Cron Demo
on:
  schedule:
    - cron: '*/10 * * * *'

jobs:
  announce:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Scheduled run fired at:"
          date -u
EOF
git add .
git commit -m "Add cron demo workflow"
git branch -M main

Push this to GitHub’s default branch and let it sit — every ten minutes or so, a new run should appear in the Actions tab.

Warning

All cron schedules on GitHub Actions run in UTC, always. There’s no timezone field. If you want “9am Eastern,” you have to do the offset arithmetic yourself and remember it shifts with daylight saving time.

Multiple Schedules

You can list more than one cron entry under schedule to combine different cadences in one workflow.

on:
  schedule:
    - cron: '0 6 * * 1-5'   # weekday mornings
    - cron: '0 12 * * 0,6'  # weekend midday

Both fire the same workflow, at their respective times — GitHub doesn’t merge or deduplicate them, it just triggers the workflow twice a day on weekdays and once a day on weekends, per this example.

The Quirks

GitHub Actions cron is not guaranteed to run at the exact minute specified. Under high load on GitHub’s shared infrastructure, scheduled runs can be delayed — sometimes by several minutes, occasionally longer during peak usage. If your job absolutely must run at a precise wall-clock time, GitHub Actions schedule is the wrong tool; you’d want something like an AWS EventBridge rule invoking a Lambda instead.

The shortest supported interval is every 5 minutes — */1 * * * * isn’t rejected outright, but GitHub effectively won’t run it more often than every 5 minutes regardless of what you write.

Scheduled workflows are automatically disabled after 60 days of repository inactivity. If nobody pushes a commit to the repo for two months, GitHub quietly turns off the schedule. You’ll get an email warning first, but if you’re relying on a schedule in a low-traffic repo, this is a real failure mode worth knowing about ahead of time rather than discovering when a nightly job silently stops running.

As mentioned earlier, scheduled workflows only read the workflow file from the default branch — editing the cron expression on a feature branch has zero effect until merged.

Note

Cron expressions don’t support the exact literal words “midnight” or step notation beyond the standard */n form supported by most cron implementations — GitHub uses POSIX cron syntax under the hood, so anything that works there works here. That site is genuinely useful for sanity-checking an expression before committing it.

Combining Schedule With Other Triggers

schedule almost never appears alone in a real workflow — it’s usually paired with workflow_dispatch so a human can manually fire an off-schedule run for testing, without waiting for the timer.

on:
  schedule:
    - cron: '0 3 * * *'
  workflow_dispatch:

This is close to a default pattern: automated nightly runs, plus a manual override for whenever you need to verify the job works without waiting until 3am UTC.

How Real DevOps Engineers Use This

Scheduled workflows are the standard place for anything that needs to happen “eventually” rather than “in response to a specific event”: nightly builds that catch integration issues before anyone’s awake, dependency vulnerability scans, database backup verification, cleaning up stale container images or old CI artifacts, and rotating short-lived credentials. Because GitHub’s scheduler isn’t precision-timed, engineers reserve it for jobs where “sometime in this hour” is acceptable, and reach for actual cloud-native schedulers (AWS EventBridge, CloudWatch Events) when exact timing genuinely matters — for instance, a job that must run within seconds of market open for a trading system has no business depending on GitHub Actions cron.

A very common real pattern is a scheduled workflow that checks for outdated dependencies, opens a pull request automatically if it finds any, and lets the normal pull_request CI checks validate the update before a human merges it — turning routine maintenance into a review-gated, mostly-automated process instead of a manual chore someone has to remember to do.

Clean up:

cd .. && rm -rf gha-cron-demo
Last updated on