GitHub Actions Guide

GitHub Actions Cron: Schedule
Workflows Step by Step

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 →

Table of contents

  1. Basic syntax: on: schedule
  2. Understanding the 5-field cron format
  3. 12 real-world cron examples
  4. Critical gotchas you must know
  5. Combining schedule with other triggers
  6. Debugging scheduled workflows
  7. FAQ

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.

1. Basic syntax: on: schedule

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.

ℹ️
Where does this file go? Workflow files must be in .github/workflows/ at the root of your repository. The filename can be anything ending in .yml or .yaml.

Multiple schedules

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

2. Understanding the 5-field cron format

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

Special characters

💡
Quick cheat: To turn any cron expression into plain English, use the visual cron expression builder — it shows the human-readable description and the next 5 scheduled run times in your local timezone.

3. 12 real-world GitHub Actions cron examples

Every day at midnight UTC 0 0 * * *
Classic nightly build trigger. Runs once per day at exactly midnight UTC. Good for nightly test suites, database backups, or report generation.
on:
  schedule:
    - cron: '0 0 * * *'
Every weekday at 9 AM UTC 0 9 * * 1-5
Business hours trigger. Runs Monday through Friday at 9:00 AM UTC. Common for morning standup summaries, business-hours health checks, or Slack notifications.
on:
  schedule:
    - cron: '0 9 * * 1-5'
Every Monday morning 0 8 * * 1
Weekly trigger, runs once on Monday at 8 AM UTC. Great for weekly dependency update checks (Dependabot does this automatically, but a custom workflow gives more control), weekly reports, or team digests.
on:
  schedule:
    - cron: '0 8 * * 1'
First day of every month 0 6 1 * *
Monthly trigger at 6 AM UTC on the 1st of every month. Used for monthly analytics summaries, rotating API keys, archiving old data, or generating monthly invoices.
on:
  schedule:
    - cron: '0 6 1 * *'
Every 6 hours 0 */6 * * *
Runs at midnight, 6 AM, noon, and 6 PM UTC every day. Good for polling external APIs, syncing data from third-party services, or health checks that need more frequent runs than daily but don't justify every hour.
on:
  schedule:
    - cron: '0 */6 * * *'
Every 30 minutes */30 * * * *
Runs every 30 minutes, 48 times per day. Note the minimum interval on GitHub Actions is 5 minutes — attempting to run more frequently will be throttled or ignored.
on:
  schedule:
    - cron: '*/30 * * * *'
Weekdays at 3 PM UTC (US East 11 AM) 0 15 * * 1-5
A reminder that GitHub Actions cron is always UTC. 3 PM UTC = 11 AM Eastern / 8 AM Pacific. If your team is US-based and you want a "midday" trigger, use hour 17 (noon Eastern) or 20 (noon Pacific).
on:
  schedule:
    - cron: '0 15 * * 1-5'  # 11 AM Eastern, 8 AM Pacific
Twice a day (9 AM and 5 PM UTC) 0 9,17 * * *
Use comma-separated values in the hour field to trigger at multiple specific times. This runs at 9 AM and 5 PM UTC daily — good for morning/evening report generation or end-of-business-day summaries across different regions.
on:
  schedule:
    - cron: '0 9,17 * * *'  # 9 AM and 5 PM UTC
Every Sunday at 2 AM UTC (weekly maintenance) 0 2 * * 0
Sunday early morning is the classic low-traffic maintenance window. Good for database vacuum/optimization, SSL certificate renewal checks, or rebuilding caches. Day-of-week 0 = Sunday.
on:
  schedule:
    - cron: '0 2 * * 0'  # Sunday at 2 AM UTC
15th and last day of month (approximately) 0 12 15,28 * *
Runs on the 15th and 28th of every month at noon UTC. Note: there's no cron syntax for "last day of month" since months have different lengths. The 28th is the closest reliable approximation (all months have at least 28 days). For true last-day logic, add a step that checks if tomorrow is the 1st.
on:
  schedule:
    - cron: '0 12 15,28 * *'
Every quarter (Jan, Apr, Jul, Oct) 0 9 1 1,4,7,10 *
Runs on the 1st of January, April, July, and October at 9 AM UTC — a quarterly trigger. Use for quarterly reports, major version bumps, annual dependency audits split into quarters, or QBR automation.
on:
  schedule:
    - cron: '0 9 1 1,4,7,10 *'  # Quarterly
Every hour on the half hour 30 * * * *
Runs at :30 past every hour — midnight:30, 1:30 AM, 2:30 AM, etc. The minute field is 30, the hour field is * (every hour). This is distinct from "every 30 minutes" — it runs at fixed half-hour marks, not relative to when the workflow was last run.
on:
  schedule:
    - cron: '30 * * * *'  # Every hour at :30

Not sure about your cron expression?

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

4. Critical gotchas you must know

⚠️
All times are UTC. GitHub Actions has no timezone setting. Every cron schedule runs in UTC, period. If you need 9 AM New York time (ET/EST), that's 14:00 UTC in winter (UTC−5) and 13:00 UTC in summer (UTC−4 during EDT). There's no automatic daylight saving time handling — you'd need two schedule entries if that matters.
⚠️
Minimum interval is 5 minutes. GitHub won't run a workflow more often than every 5 minutes. Any expression with */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.).
⚠️
Scheduled runs can be delayed or skipped. GitHub explicitly says schedules are "best effort" — they can be delayed by 5–30 minutes during peak load, and in rare cases skipped entirely. Never build systems that depend on scheduled workflows running at an exact time. Use them for approximate triggers only.
⚠️
Scheduled workflows only run on the default branch. If your workflow file is in a feature branch and the 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.
⚠️
Inactive repos get their schedules disabled. If a repository has no activity for 60 days, GitHub automatically disables scheduled workflows. You'll get an email warning before it happens. To re-enable, go to Actions → the disabled workflow → click "Enable workflow".

5. Combining schedule with other triggers

You can — and often should — combine schedule: with other triggers in the same workflow. The most common patterns:

Schedule + workflow_dispatch (manual override)

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

Schedule + push (also runs on deploy)

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

Checking how the workflow was triggered

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

6. Debugging scheduled workflows

My workflow isn't running — checklist

  1. Is the workflow file on the default branch? Check that .github/workflows/your-workflow.yml is merged to main/master. Scheduled workflows on other branches don't run.
  2. Is the cron expression valid? A malformed expression (wrong number of fields, invalid values) will cause the workflow to fail silently. Validate it here.
  3. Is the repository inactive? Go to Actions → check if the workflow shows as "disabled". Inactive repos have schedules automatically disabled after 60 days.
  4. Is it just delayed? GitHub schedules can run 5–30 minutes late. Check if the run appeared eventually.
  5. Are there YAML syntax errors? A YAML error anywhere in the file will prevent the workflow from being registered. Check Actions → the workflow name for parse errors.

Viewing scheduled run history

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.

Testing a scheduled workflow without waiting

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

7. Frequently asked questions

What timezone does GitHub Actions cron use?
GitHub Actions cron schedules always run in UTC. There is no way to set a different timezone in the workflow file itself. If you need a schedule relative to a different timezone, convert the local time to UTC manually. For example, 9:00 AM Eastern Time (UTC−5) is 14:00 UTC, so you'd use 0 14 * * *.
What is the minimum interval for GitHub Actions scheduled workflows?
The minimum interval is 5 minutes. You cannot schedule a workflow to run more frequently than every 5 minutes. Expressions like */1 * * * * (every minute) or */2 * * * * (every 2 minutes) will be silently limited or rejected.
Will my scheduled workflow always run exactly on time?
No. GitHub explicitly documents that scheduled workflows can be delayed during periods of high infrastructure load. Delays of 5–30 minutes are common during peak hours (Monday morning UTC is especially prone to delays). Do not use scheduled workflows for tasks requiring exact timing.
Why is my GitHub Actions scheduled workflow not running?
Common reasons: (1) The workflow file is not on the default branch. (2) The repository has been inactive for 60+ days — GitHub disables scheduled workflows on inactive repos. (3) There's a syntax error in the cron expression or YAML file. (4) GitHub's infrastructure is experiencing delays. (5) The workflow is disabled in the Actions tab.
Can I use a cron schedule with other GitHub Actions triggers?
Yes. You can combine 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.
How do I run a GitHub Actions workflow every weekday?
Use 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.
How do I schedule a workflow to run monthly?
Use 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.
How do I read or use the current date inside a scheduled workflow?
Use 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.

Build your cron expression visually

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 →