Learn to schedule any GitHub Actions workflow on a cron schedule. Covers syntax, real-world examples, UTC timezone gotchas, and common mistakes developers make.
⏰ Try the Visual Cron Builder →Scheduled workflows are one of the most powerful features of GitHub Actions. They let you run any automation on a time-based trigger — nightly builds, weekly dependency updates, monthly cleanup jobs, daily health checks, and more — without any external cron service or infrastructure.
This guide covers everything you need to know: the syntax, how to read cron expressions, 12 copy-paste examples for common schedules, and the gotchas that trip up almost every developer the first time.
A scheduled GitHub Actions workflow uses the on: schedule: trigger in your workflow YAML file. Here's the minimal complete example:
# .github/workflows/daily-job.yml
name: Daily job
on:
schedule:
- cron: '0 9 * * *' # Every day at 9:00 AM UTC
jobs:
run-daily:
runs-on: ubuntu-latest
steps:
- name: Run my task
run: echo "Running daily at 9 AM UTC"
The cron: value is a standard 5-field cron expression in quotes. That's all you need to get a scheduled workflow running.
.github/workflows/ at the root of your repository. The filename can be anything ending in .yml or .yaml.
You can specify multiple cron expressions for the same workflow by adding multiple entries under schedule::
on:
schedule:
- cron: '0 9 * * 1-5' # Weekdays at 9 AM UTC
- cron: '0 12 * * 0,6' # Weekends at noon UTC
A GitHub Actions cron expression has exactly 5 space-separated fields. Here's the structure:
┌──────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌────────── day of month (1–31)
│ │ │ ┌──────── month (1–12)
│ │ │ │ ┌───── day of week (0–6, Sunday=0)
│ │ │ │ │
* * * * *
| Field | Allowed values | Special chars | Examples |
|---|---|---|---|
| minute | 0–59 | * , - / |
0 = top of hour, 30 = half past, */15 = every 15 min |
| hour | 0–23 | * , - / |
9 = 9 AM, 14 = 2 PM, */6 = every 6 hours |
| day-of-month | 1–31 | * , - / |
1 = 1st, 15 = 15th, 1,15 = 1st and 15th |
| month | 1–12 | * , - / |
1 = January, 6,12 = June and December |
| day-of-week | 0–6 (Sun=0) | * , - / |
1 = Monday, 1-5 = weekdays, 0,6 = weekends |
* — matches any value ("every minute", "every hour", "every day"), — separates multiple values: 1,3,5 means 1st, 3rd, and 5th- — defines a range: 1-5 means 1 through 5/ — step values: */15 in the minute field means every 15 minutes; */2 in the hour field means every 2 hourson:
schedule:
- cron: '0 0 * * *'
on:
schedule:
- cron: '0 9 * * 1-5'
on:
schedule:
- cron: '0 8 * * 1'
on:
schedule:
- cron: '0 6 1 * *'
on:
schedule:
- cron: '0 */6 * * *'
on:
schedule:
- cron: '*/30 * * * *'
on:
schedule:
- cron: '0 15 * * 1-5' # 11 AM Eastern, 8 AM Pacific
on:
schedule:
- cron: '0 9,17 * * *' # 9 AM and 5 PM UTC
on:
schedule:
- cron: '0 2 * * 0' # Sunday at 2 AM UTC
on:
schedule:
- cron: '0 12 15,28 * *'
on:
schedule:
- cron: '0 9 1 1,4,7,10 *' # Quarterly
on:
schedule:
- cron: '30 * * * *' # Every hour at :30
Paste any expression into the visual builder to get a plain-English description and the next 5 scheduled times in your local timezone.
⏰ Open Visual Cron Builder*/1, */2, */3, or */4 in the minute field will either be rejected or silently limited. For sub-5-minute needs, use a different mechanism (a self-triggering workflow, an external scheduler, etc.).
schedule: trigger is defined there, it will NOT run. GitHub only picks up scheduled workflows from the repository's default branch (usually main or master). Merging to default is required for the schedule to activate.
You can — and often should — combine schedule: with other triggers in the same workflow. The most common patterns:
Adding workflow_dispatch: lets you run the workflow manually from the GitHub Actions UI when you need to trigger it outside the schedule. This is almost always a good idea for scheduled workflows.
on:
schedule:
- cron: '0 2 * * 0' # Auto: every Sunday at 2 AM UTC
workflow_dispatch: # Manual: run anytime from the Actions tab
Run the workflow on both a schedule and on every push to main:
on:
push:
branches: [main]
schedule:
- cron: '0 0 * * *' # Nightly, and also on every push to main
Inside your workflow, use github.event_name to check whether the run was triggered by a schedule or another event:
- name: Check trigger
run: |
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "Running on schedule"
else
echo "Triggered manually or by push"
fi
.github/workflows/your-workflow.yml is merged to main/master. Scheduled workflows on other branches don't run.Go to your repository → Actions tab → click on the specific workflow name in the left sidebar. You'll see all runs including scheduled ones, with their trigger source, duration, and status. The "Trigger" column shows "schedule" for cron-triggered runs.
Add workflow_dispatch: to your triggers (as shown above), then go to Actions → your workflow → Run workflow. This triggers the exact same job steps as the scheduled run, letting you test the logic immediately.
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch: # Add this to test immediately
0 14 * * *.*/1 * * * * (every minute) or */2 * * * * (every 2 minutes) will be silently limited or rejected.schedule: with push:, pull_request:, workflow_dispatch:, and any other trigger in the same on: block. The workflow will run when any of the specified triggers fires. Adding workflow_dispatch: is especially useful for testing scheduled workflows on-demand.0 9 * * 1-5 to run at 9:00 AM UTC Monday through Friday. The last field (1–5) specifies day of week where 1=Monday and 5=Friday. To run at a different time, adjust the first two fields: 30 14 * * 1-5 runs at 2:30 PM UTC on weekdays.0 9 1 * * to run at 9 AM UTC on the 1st of every month. For the 15th, use 0 9 15 * *. For "last day of month" — which varies — a common approximation is the 28th (0 9 28 * *), since every month has at least 28 days. For true last-day logic, add a step that checks if tomorrow is the 1st.run: echo "date=$(date -u +%Y-%m-%d)" >> $GITHUB_OUTPUT in a step to capture the current UTC date, then reference it as ${{ steps.step-id.outputs.date }} in subsequent steps. For a timestamp, use date -u +%Y-%m-%dT%H:%M:%SZ.Stop second-guessing your cron syntax. The visual builder lets you set each field with sliders and chips, then shows the human-readable description and next 5 run times — all before you commit anything.
⏰ Try the Cron Expression Builder →