Back to blog

GitHub Actions Scheduled Workflows in Local Time: Use timezone Instead of UTC Math

Run GitHub Actions schedules at a local time with an IANA timezone, handle daylight-saving changes, and avoid the common limits of cron workflows.

A glowing local-time clock aligns a calendar and globe while a second ghost clock drifts away

GitHub Actions schedules used to force you to think in UTC. That was manageable for a job that runs every few hours. It was much less pleasant for a report that should run at 09:00 in Stockholm, a cleanup job tied to a team's local workday, or a weekday task that should stay at the same wall-clock time after daylight-saving changes.

GitHub Actions now accepts an IANA timezone next to the cron expression. That means the schedule can describe the time you actually care about instead of encoding a seasonal UTC conversion by hand. The feature is small, but it removes a class of jobs that quietly drift twice a year.

The basic syntax

Put timezone alongside cron inside the schedule list. The timezone value is an IANA timezone string such as Europe/Stockholm, America/New_York, or Asia/Tokyo. GitHub's changelog announced the feature, and the current workflow syntax reference includes the same shape in its schedule example.GitHub's timezone announcement and the workflow syntax reference are the primary references for the behavior.

name: Morning report
on:
  schedule:
    - cron: '0 9 * * 1-5'
      timezone: 'Europe/Stockholm'
  workflow_dispatch:

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - run: ./scripts/build-report.sh

This runs at 09:00 every weekday in the Europe/Stockholm timezone. The workflow_dispatch entry is optional, but it is useful for testing the job manually without waiting for the next scheduled run.

Stop doing seasonal UTC math

A common workaround is to write one UTC cron for winter, change it for summer, and remember to change it back. That creates a maintenance task that is easy to miss and hard to spot in review because both expressions look plausible. It also becomes awkward when a team spans several regions.

A named timezone makes the intent visible in the workflow. Reviewers can answer “which local clock matters?” without converting the hour in their head. It also lets the scheduling service apply the timezone's daylight-saving rules instead of leaving that conversion to a human.

# Brittle when the local offset changes
on:
  schedule:
    - cron: '0 8 * * 1-5' # Which season is this for?

# Clearer intent
on:
  schedule:
    - cron: '0 9 * * 1-5'
      timezone: 'Europe/Stockholm'

Daylight-saving time still has an edge case

Timezone-aware scheduling keeps a wall-clock time, but some local times do not exist during the spring-forward transition. GitHub's documentation says a schedule set in a daylight-saving timezone that lands in a skipped hour advances to the next valid time. Its example is a 02:30 schedule moving to 03:00.The schedule event documentation spells out that behavior and is worth checking for jobs where the exact minute matters.

on:
  schedule:
    - cron: '30 2 * * *'
      timezone: 'Europe/Stockholm'

Do not use a skipped local hour for a payment cutoff, an irreversible cleanup, or another job where running at exactly the expected time is part of the contract. Pick a stable time such as 03:30, or make the job idempotent and let it check the intended business date before doing its work.

Multiple schedules are useful for mixed regions

A workflow can have more than one schedule entry. Each entry can carry its own cron expression and timezone. That is useful when one workflow needs to run a report for two operating regions, or when you want a local-time schedule alongside a separate UTC maintenance run.

on:
  schedule:
    - cron: '0 9 * * 1-5'
      timezone: 'Europe/Stockholm'
    - cron: '0 9 * * 1-5'
      timezone: 'America/New_York'

jobs:
  regional-report:
    runs-on: ubuntu-latest
    steps:
      - run: ./scripts/build-report.sh

If the job should behave differently for each schedule, inspect the schedule value in the github.event context and branch deliberately. Otherwise, keep the task idempotent so two schedule entries can safely produce the same result. A duplicated notification or deployment is usually a sign that the workflow needs an explicit schedule identity.

The limits did not disappear

A timezone fixes the calendar conversion. It does not turn a scheduled workflow into a precise timer. GitHub documents several limits that still matter: schedules can be delayed during high load, the busiest point is often the start of the hour, and queued jobs can be dropped if load is high enough. The shortest supported interval is once every five minutes.

Scheduled workflows also run only from the default branch. In a public repository, GitHub automatically disables a scheduled workflow after 60 days without repository activity. Keep those rules in mind before using schedule for an SLA, a watchdog, or a task that must run exactly once.

A small checklist before you merge

  1. Choose an IANA timezone that describes the business location, not the runner machine.
  2. Write the cron hour as the local hour you want people to understand.
  3. Avoid a spring-forward hour when the job has an exact-time contract.
  4. Schedule away from minute zero when a small delay is acceptable.
  5. Add workflow_dispatch so you can test the job without waiting for the clock.

Timezone support makes scheduled workflows easier to review and much less fragile. Use it whenever the requirement is expressed as a local time. Keep the job idempotent, plan for daylight-saving edge cases, and remember that GitHub's scheduler still has queueing and default-branch rules.