beginner 8 min read

How to Set Up Cron Jobs in GitHub Actions (Scheduled Workflows)

Learn how to create scheduled workflows in GitHub Actions using cron expressions. Covers syntax, limitations, timezone handling, debugging, and common pitfalls.

Prerequisites

  • A GitHub repository
  • Basic understanding of GitHub Actions YAML

Scheduled Workflows in GitHub Actions

GitHub Actions supports the schedule event trigger, which runs workflows on a cron schedule. This is useful for recurring tasks like dependency updates, report generation, data syncing, or cleanup.

Scheduled workflows run on the default branch (usually main) and use UTC time. They may be delayed by up to several minutes during high-load periods.

Quick Start

Create .github/workflows/scheduled.yml:

yaml
name: Scheduled Task
on:
  schedule:
    - cron: '0 9 * * 1-5'  # Weekdays at 9 AM UTC
  workflow_dispatch:  # Allow manual trigger for testing

jobs:
  run-task:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run scheduled task
        run: echo "Running at $(date)"

Important: Always add workflow_dispatch so you can manually trigger the workflow for testing.

Schedule Syntax and Limits

GitHub Actions uses standard POSIX 5-field cron syntax:

minute hour day-of-month month day-of-week

Limitations:

  • Minimum interval is 5 minutes (*/5 * * * * is the shortest)
  • Schedules run in UTC only — no timezone configuration
  • Workflows may be delayed during peak times (not guaranteed to run exactly on time)
  • Scheduled workflows are disabled after 60 days of repository inactivity
  • You can define multiple schedules for a single workflow
yaml
on:
  schedule:
    - cron: '0 9 * * 1-5'   # Weekdays at 9 AM UTC
    - cron: '0 0 1 * *'     # First day of month at midnight UTC

Common Cron Expressions

All times are in UTC:

  • */5 * * * * — Every 5 minutes (minimum interval)
  • 0 * * * * — Every hour
  • 0 0 * * * — Daily at midnight UTC
  • 0 9 * * 1-5 — Weekdays at 9 AM UTC
  • 0 0 * * 0 — Every Sunday at midnight UTC
  • 0 0 1 * * — First day of every month

Timezone conversion tip: If you want 9 AM Eastern (UTC-5), use 0 14 * * * (9 AM + 5 hours = 2 PM UTC). Remember to adjust for daylight saving time.

Every weekday at 9:00 AM

Next runs (UTC):

Mon, May 18, 2026 09:00

Tue, May 19, 2026 09:00

Wed, May 20, 2026 09:00

Working with Timezones

GitHub Actions schedules are always in UTC. If you need timezone-aware scheduling, you have two options:

Option 1: Convert to UTC manually

9 AM US Eastern = 2 PM UTC (EST) or 1 PM UTC (EDT)

Option 2: Check timezone in the workflow

yaml
jobs:
  run-task:
    runs-on: ubuntu-latest
    steps:
      - name: Check if it's business hours in EST
        run: |
          HOUR=$(TZ=America/New_York date +%H)
          if [ "$HOUR" -lt 9 ] || [ "$HOUR" -gt 17 ]; then
            echo "Outside business hours, skipping"
            exit 0
          fi
      - name: Run task
        run: echo "Running during business hours"

Debugging Scheduled Workflows

Workflow not running?

  • Check the Actions tab for any disabled workflows
  • Scheduled workflows only run on the default branch — merging a schedule change to a feature branch won't work
  • After 60 days of no repository activity, GitHub disables scheduled workflows. Push a commit or manually re-enable them
  • Check for YAML syntax errors in the cron expression

Manual trigger for testing:

Add workflow_dispatch to your trigger:

yaml
on:
  schedule:
    - cron: '0 9 * * *'
  workflow_dispatch:

Then trigger manually from the Actions tab or:

bash
gh workflow run scheduled.yml

Check recent runs:

bash
gh run list --workflow=scheduled.yml

Common Patterns

Dependency updates:

yaml
name: Update Dependencies
on:
  schedule:
    - cron: '0 6 * * 1'  # Mondays at 6 AM UTC
jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm update
      - uses: peter-evans/create-pull-request@v6
        with:
          title: 'chore: update dependencies'
          branch: deps/weekly-update

Stale issue cleanup:

yaml
name: Close Stale Issues
on:
  schedule:
    - cron: '0 0 * * *'  # Daily at midnight
jobs:
  stale:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/stale@v9
        with:
          days-before-stale: 60
          days-before-close: 7

Production Tips

Don't rely on exact timing. GitHub Actions schedules can be delayed by minutes during high load. Don't use them for time-critical operations.

Idempotent tasks: Since scheduled runs may be delayed or run twice (rare), make your tasks idempotent — safe to run multiple times.

Concurrency control:

yaml
concurrency:
  group: scheduled-task
  cancel-in-progress: true

Notifications on failure:

yaml
- name: Notify on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: '{"text":"Scheduled workflow failed!"}'
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Frequently Asked Questions