GitHub Actions Scheduled Workflows (Cron): Complete Guide
GitHub Actions lets you run workflows on a cron schedule. It is convenient and free for public repositories, but it comes with limitations that catch people off guard: UTC-only scheduling, execution delays, and automatic disabling after 60 days of inactivity. This guide covers everything you need to know to use it effectively.
The Schedule Trigger
To run a workflow on a schedule, use the schedule event in your workflow YAML file. The schedule uses standard cron expression syntax with five fields:
# .github/workflows/scheduled-task.yml
name: Daily Data Sync
on:
schedule:
# Run every day at 8:00 AM UTC
- cron: '0 8 * * *'
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run sync script
run: python scripts/sync_data.py
env:
API_KEY: ${{ secrets.DATA_API_KEY }}The cron value must be a quoted string (single quotes in YAML). The five fields are identical to standard cron: minute, hour, day of month, month, day of week.
Multiple Schedules
You can define multiple cron schedules for the same workflow:
on:
schedule:
# Every 6 hours
- cron: '0 */6 * * *'
# Also on the 1st of every month at midnight
- cron: '0 0 1 * *'Combining Schedule with Manual Triggers
In practice, you almost always want to combine schedule with workflow_dispatch so you can run the workflow manually during development and debugging:
name: Weekly Report
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9 AM UTC
workflow_dispatch: # Allow manual trigger from the Actions tab
inputs:
force_full_report:
description: 'Generate full report instead of incremental'
required: false
default: 'false'
type: boolean
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate report
run: |
if [ "${{ github.event.inputs.force_full_report }}" = "true" ]; then
python scripts/report.py --full
else
python scripts/report.py
fi
env:
REPORT_EMAIL: ${{ secrets.REPORT_EMAIL }}With workflow_dispatch, you can click "Run workflow" in the Actions tab of your repository. This is invaluable for testing without waiting for the next scheduled run.
UTC-Only: No Timezone Support
GitHub Actions schedules are always in UTC. There is no timezone configuration, no CRON_TZ variable, and no plans from GitHub to add timezone support.
If you need a workflow to run at 9 AM in a specific timezone, you must manually convert:
| Target Time | UTC Equivalent | Cron Expression |
|---|---|---|
| 9 AM EST (UTC-5) | 2 PM UTC | 0 14 * * * |
| 9 AM CET (UTC+1) | 8 AM UTC | 0 8 * * * |
| 9 AM JST (UTC+9) | 12 AM UTC | 0 0 * * * |
During daylight saving time transitions, your workflow will shift by one hour relative to local time. There is no workaround within GitHub Actions itself. For a deeper look at this problem and solutions, see our guide on cron job timezone issues.
Use the Cron Expression Generator to quickly build and verify your UTC schedule before committing it to your workflow file.
Common Workflow Patterns
Dependency Update Check
name: Check for Dependency Updates
on:
schedule:
- cron: '0 6 * * 1' # Every Monday at 6 AM UTC
jobs:
check-updates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm outdated || true
- name: Create issue if updates available
uses: actions/github-script@v7
with:
script: |
const { execSync } = require('child_process');
const outdated = execSync('npm outdated --json || true').toString();
const deps = JSON.parse(outdated || '{}');
if (Object.keys(deps).length > 0) {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Dependency updates available (${Object.keys(deps).length} packages)`,
body: '```json\n' + JSON.stringify(deps, null, 2) + '\n```',
labels: ['dependencies']
});
}Stale Issue Cleanup
name: Close Stale Issues
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight UTC
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: >
This issue has been automatically marked as stale
because it has not had recent activity.
days-before-stale: 60
days-before-close: 14Scheduled Security Scan
name: Security Audit
on:
schedule:
- cron: '0 4 * * *' # Daily at 4 AM UTC
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm audit --production
- name: Run Snyk security check
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}Data Pipeline / ETL
name: ETL Pipeline
on:
schedule:
- cron: '0 */4 * * *' # Every 4 hours
jobs:
etl:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- name: Extract
run: python etl/extract.py
env:
SOURCE_DB_URL: ${{ secrets.SOURCE_DB_URL }}
- name: Transform
run: python etl/transform.py
- name: Load
run: python etl/load.py
env:
TARGET_DB_URL: ${{ secrets.TARGET_DB_URL }}
- name: Notify on failure
if: failure()
run: |
curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-H 'Content-type: application/json' \
-d '{"text":"ETL pipeline failed. See: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'Secrets and Conditional Execution
Secrets work normally in scheduled workflows. Reference them with ${{ secrets.YOUR_SECRET }} as you would in any other workflow.
You can run different logic depending on whether the workflow was triggered by a schedule or manually:
- name: Full sync on schedule, incremental on manual
run: |
if [ "${{ github.event_name }}" = "schedule" ]; then
python sync.py --mode full
else
python sync.py --mode incremental
fiKnown Limitations and Gotchas
GitHub Actions scheduled workflows have several important limitations that are not always obvious from the documentation:
1. Minimum interval: 5 minutes
The shortest schedule you can set is */5 * * * * (every 5 minutes). GitHub will silently ignore schedules more frequent than this. In practice, for public repositories on the free tier, GitHub recommends schedules no more frequent than every 15 minutes.
2. Execution delays are normal
Scheduled workflows do not run at the exact time specified. During periods of high demand on GitHub Actions, delays of 10-30 minutes are common. Delays of over an hour have been reported during peak usage. GitHub does not guarantee execution timing.
3. Auto-disabled after 60 days of inactivity
If a repository has no commits, pull requests, or issues for 60 consecutive days, GitHub automatically disables all scheduled workflows. You will receive an email notification, but if you miss it, your scheduled tasks stop running silently. To prevent this, ensure some activity happens on the repository at least every 60 days.
4. Only runs on the default branch
Scheduled workflows only trigger on the default branch (usually main or master). You cannot schedule workflows on feature branches. Use workflow_dispatch to test on other branches.
5. Free tier minute limits
Free accounts get 2,000 minutes per month for private repositories. Public repositories are unlimited. A workflow that runs every 15 minutes and takes 2 minutes each will consume about 5,760 minutes per month, well over the free tier limit for private repos.
6. No built-in failure alerting
GitHub does not send notifications when a scheduled workflow fails. You must implement alerting yourself using Slack webhooks, email APIs, or third-party services. Without this, failed scheduled workflows can go unnoticed for weeks.
Monitoring Scheduled Runs
Since GitHub does not notify you about scheduled workflow failures, build monitoring into the workflow itself:
jobs:
task:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run the task
id: main_task
run: python scripts/process.py
# Alert on failure
- name: Notify Slack on failure
if: failure()
run: |
curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-H 'Content-type: application/json' \
-d '{
"text": "Scheduled workflow failed",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Workflow:* ${{ github.workflow }}\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
}
}
]
}'
# Heartbeat on success (dead man's switch)
- name: Send heartbeat
if: success()
run: curl -s "https://cronjobpro.com/api/heartbeat/${{ secrets.HEARTBEAT_ID }}" > /dev/nullWhen GitHub Actions Is Not Enough
GitHub Actions scheduled workflows are a reasonable choice for tasks that meet all of these criteria:
- The task is tightly coupled to the repository (depends on repo code)
- Execution delays of 10-30 minutes are acceptable
- The repository will remain active (commits at least every 60 days)
- UTC-only scheduling is acceptable
- You do not need automatic retries
If any of those criteria do not fit, consider alternatives:
| Requirement | Better Alternative |
|---|---|
| Exact timing (no delays) | CronJobPro, GCP Cloud Scheduler, AWS EventBridge |
| Timezone-aware scheduling | CronJobPro, GCP Cloud Scheduler |
| Automatic retries on failure | CronJobPro, Celery, any HTTP scheduler with retry support |
| Intervals shorter than 5 min | CronJobPro (1-min intervals), self-hosted cron |
| Inactive/archived repository | Any external scheduler (will not auto-disable) |
For HTTP-based tasks (calling an API endpoint, triggering a webhook, pinging a health check), an external scheduler like CronJobPro is a more reliable choice. It will not auto-disable, it retries on failure, it runs on time, and it sends alerts immediately when something goes wrong.
Key Takeaways
- Use the
scheduleevent with standard cron syntax in your workflow YAML. Always addworkflow_dispatchfor manual testing. - All schedules are in UTC with no timezone configuration. Convert your target local time manually.
- Expect 10-30 minute delays during peak usage. Do not use GitHub Actions for time-critical tasks.
- Scheduled workflows auto-disable after 60 days of repository inactivity. Monitor for this.
- Build failure alerting into every scheduled workflow. GitHub does not notify you when scheduled runs fail.
- For precise timing, timezone support, retries, and monitoring, use a dedicated scheduling service instead.
Related Articles
Schedule serverless functions on Vercel with cron expressions.
AWS Lambda Scheduled EventsSchedule Lambda functions with EventBridge cron and rate expressions.
Cron Expression Generator GuideBuild and validate cron expressions with visual tools.
Cron Job Not Running? How to Fix ItTroubleshoot common cron job failures across platforms.
Need reliable scheduled execution?
CronJobPro runs on time, retries on failure, and never auto-disables. Set up timezone-aware HTTP scheduling in under two minutes.