Complete guide to scheduled GitHub Actions workflows — 15+ real YAML examples, UTC timezone explained, common mistakes, and production-ready workflow templates.
GitHub Actions uses standard 5-field cron syntax in the schedule trigger under on:. Schedules run on the default branch only and always in UTC.
name: Scheduled Job
on:
schedule:
- cron: '0 9 * * 1-5' # 9:00 AM UTC, Monday–Friday
workflow_dispatch: # Always add this for manual testing
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run scheduled task
run: echo "Running at $(date)"
minute(0-59) hour(0-23) day-of-month(1-31) month(1-12) day-of-week(0-7)This is the most common source of confusion with GitHub Actions schedules. All schedule triggers fire in UTC — there is no timezone setting. If your team is in a different timezone, you must manually convert.
| Desired Local Time | Timezone | UTC Offset | Cron Expression (UTC) |
|---|---|---|---|
| 9:00 AM Mon–Fri | UTC | +0 | 0 9 * * 1-5 |
| 9:00 AM Mon–Fri | New York (EST) | -5 | 0 14 * * 1-5 |
| 9:00 AM Mon–Fri | New York (EDT) | -4 | 0 13 * * 1-5 |
| 9:00 AM Mon–Fri | London (GMT) | +0 | 0 9 * * 1-5 |
| 9:00 AM Mon–Fri | London (BST) | +1 | 0 8 * * 1-5 |
| 9:00 AM Mon–Fri | India (IST) | +5:30 | 30 3 * * 1-5 |
| 9:00 AM Mon–Fri | Singapore (SGT) | +8 | 0 1 * * 1-5 |
| 9:00 AM Mon–Fri | Tokyo (JST) | +9 | 0 0 * * 1-5 |
| Midnight every day | Sydney (AEDT) | +11 | 0 13 * * * |
name: Nightly Build
on:
schedule:
- cron: '0 2 * * *' # 2:00 AM UTC daily
workflow_dispatch:
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run full test suite
run: npm run test:full
- name: Build
run: npm run build
- name: Notify on failure
if: failure()
uses: actions/github-script@v7
with:
script: |
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Nightly build failed',
body: 'Run: ' + context.serverUrl + '/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId
})
name: Weekly Dependency Check
on:
schedule:
- cron: '0 8 * * 1' # Monday 8:00 AM UTC
workflow_dispatch:
jobs:
update-deps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Check for outdated packages
run: npm outdated || true
- name: Run audit
run: npm audit --audit-level=high
name: Monthly Release
on:
schedule:
- cron: '0 10 1 * *' # 1st of every month, 10:00 AM UTC
workflow_dispatch:
inputs:
version_bump:
description: 'Version bump type'
required: true
default: 'patch'
type: choice
options: [patch, minor, major]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Bump version
run: |
BUMP="${{ inputs.version_bump || 'patch' }}"
npm version $BUMP --no-git-tag-version
- name: Commit and tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
VERSION=$(node -p "require('./package.json').version")
git add package.json
git commit -m "chore: release v$VERSION"
git tag "v$VERSION"
git push && git push --tags
0 9 * * 1-5 expecting 9 AM in your local time. GitHub runs in UTC. For 9 AM IST (UTC+5:30), use 30 3 * * 1-5.
schedule: only trigger from the default branch. A schedule in a feature branch PR will never fire.
workflow_dispatch:, you cannot manually test your scheduled workflow. You'd have to wait for the schedule or change it temporarily. Always add workflow_dispatch:.
Adding workflow_dispatch alongside your schedule gives you a "Run workflow" button in the GitHub Actions UI. This is essential for:
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch: # No config needed — just add it
inputs: # Optional: add inputs for parameterised manual runs
environment:
description: 'Target environment'
required: false
default: 'staging'
type: choice
options: [staging, production]
A single workflow can have multiple schedule entries. All of them trigger the same workflow:
on:
schedule:
- cron: '0 8 * * 1-5' # Weekdays at 8 AM UTC
- cron: '0 12 * * 0' # Sundays at noon UTC
workflow_dispatch:
To know which schedule triggered the run, check github.event.schedule in your job steps:
- name: Check which schedule triggered
run: |
echo "Triggered by: ${{ github.event.schedule || 'manual' }}"
if [ "${{ github.event.schedule }}" = "0 8 * * 1-5" ]; then
echo "Weekday morning run"
fi
schedule trigger. Convert your desired local time to UTC before writing the cron expression. CronRead's timezone calculator does this automatically.*/5 * * * *). Schedules faster than this are technically unsupported and may be throttled or skipped.workflow_dispatch: to your on: block. This adds a "Run workflow" button in the Actions UI. Best practice is to include it on every scheduled workflow.${{ github.event.schedule }} in your workflow steps. It contains the cron string that triggered the run, or is empty for manual (workflow_dispatch) runs.schedule trigger under on: runs on GitHub's own servers, so there's nothing to host or pay for beyond your normal Actions minutes. It's a solid choice for nightly builds, dependency updates, and periodic API calls, as long as you can tolerate the occasional 15-30 minute delay and the 5-minute minimum interval.