How Do I Run Jobs on a Schedule?

Add a schedule trigger with a cron expression. Scheduled runs fire on the default branch, in UTC, and GitHub gives no guarantee of an exact start minute.

Last verified:

Answer

Add a schedule trigger with a cron expression to the workflow file, and put workflow_dispatch next to it so the same jobs can start by hand. Three behaviors decide whether that schedule does what you meant: GitHub evaluates the cron in UTC, the run fires against the latest commit on the default branch, and the start time is best effort, since GitHub documents that the schedule event can be delayed during periods of high load (events that trigger workflows, checked on 2026-08-13).

name: nightly
on:
  schedule:
    - cron: "17 3 * * *"
  workflow_dispatch:

jobs:
  integration:
    runs-on: warp-ubuntu-latest-x64-4x
    strategy:
      fail-fast: false
      matrix:
        suite: [api, worker, web, migrations]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run test:integration -- --suite=${{ matrix.suite }}
        env:
          RUN_REASON: ${{ github.event_name }}

The cron string is POSIX five-field syntax, read left to right.

FieldRangeValue in the exampleMeaning
Minute0 to 591717 minutes past the hour
Hour0 to 23, UTC303:00 UTC
Day of month1 to 31*every day
Month1 to 12*every month
Day of week0 to 6, Sunday is 0*every weekday and weekend

Two limits are worth knowing before you write the expression. The shortest interval GitHub accepts is once every 5 minutes, and a workflow can carry more than one cron entry under the same schedule key, which is how a weekday cadence and a weekend cadence live in one file (events that trigger workflows, checked on 2026-08-13).

Detail

The three surprises

UTC, with no daylight saving adjustment. The cron is evaluated in UTC and stays there all year. A team in Berlin that writes 0 2 * * * expecting a 02:00 local run gets 04:00 local in summer and 03:00 local in winter. Convert the local time you actually want into UTC once, then write that number and leave it alone. Where the run has to land at a fixed local hour year round, use two cron entries with month ranges, or drive it from an external scheduler that calls the workflow dispatch API.

Default branch only. The schedule event always runs the version of the workflow on the latest commit of the default branch. Adding a cron on a feature branch produces no runs at all, and there is no branch-scoped preview of a schedule change. That is the practical reason workflow_dispatch belongs in the same file: it runs from any ref, so you can prove the jobs work before the cron ever fires. Branching on github.event_name inside a step lets the manual run skip a notification or a deploy that only the nightly path should do.

Delayed and dropped runs. GitHub names the start of every hour as a high load window and recommends scheduling away from it, which is why the example above uses minute 17 rather than 0. Under sustained load a scheduled run can start minutes late or be skipped for that interval, so a job that has to run exactly 288 times a day at exactly 5 minute spacing needs a scheduler outside GitHub Actions. One more schedule-specific detail: for a public repository, GitHub disables scheduled workflows automatically after 60 days with no repository activity, and failure notifications for a scheduled run go to whoever last edited the cron expression rather than to the whole team (events that trigger workflows, checked on 2026-08-13).

What a nightly job costs

The workflow above runs a 4-leg matrix once a night. Assume each leg takes 22 minutes, which is 88 billed minutes per night, and 2,640 minutes across a 30 day month on warp-ubuntu-latest-x64-4x at $0.008 per minute from the cloud runners catalog and the pricing page, checked on 2026-08-13. That is $21.12 a month.

CadenceCronRuns per 30 daysBilled minutes per runTotal minutesRunner and rateMonthly cost
Nightly, 4-leg matrix17 3 * * *30882,640warp-ubuntu-latest-x64-4x, $0.008/min$21.12
Weekdays only, same matrix17 3 * * 1-522881,936warp-ubuntu-latest-x64-4x, $0.008/min$15.49
Hourly smoke test23 * * * *72042,880warp-ubuntu-latest-x64-2x, $0.004/min$11.52
Every 5 minutes, the floor GitHub allows*/5 * * * *8,640217,280warp-ubuntu-latest-x64-2x, $0.004/min$69.12

The last row is the one that surprises people. A 2 minute health check at the shortest interval GitHub permits costs more per month than a 22 minute nightly integration matrix, because frequency multiplies faster than duration.

For the nightly row, the same 2,640 minutes on the nearest GitHub-hosted shape come to $31.68. warp-ubuntu-latest-x64-4x (4 vCPU, 16 GB) costs $0.008 per minute against $0.012 per minute for the 4-core Linux larger runner (4 vCPU, 16 GB): 33 percent lower list price, checked on 2026-08-13 against the GitHub Actions per-minute rate list. Over a year that difference is $126.72 on one nightly workflow.

Confirming the schedule actually ran

A cron that quietly stopped firing looks identical to a cron that never existed, so check the record rather than the file. The reports documentation covers the Jobs section, which carries one row per repository, workflow, and job name with duration P75 and P90 next to queue time percentiles. Filter it to the scheduled workflow name over a 30 day window and the run count tells you whether every interval landed, while the duration percentiles tell you whether the nightly job is drifting longer as the suite grows. Every table exports to CSV with the current filters applied, which makes the run count easy to compare against the number of intervals you expected.

Picking the runner for a scheduled job

Scheduled jobs are usually the least time-critical work in a repository and the easiest place to buy back wall-clock time, since nobody is watching a nightly run at 03:17 UTC. A nightly matrix can pin its iOS leg to macOS and its service tests to Linux inside one workflow, with labels listed in the cloud runners catalog. The guide to speeding up GitHub Actions covers the sizing and caching levers that apply to scheduled jobs the same way they apply to pull request jobs, and the nightly builds guide walks through the full workflow shape.

What time zone does a GitHub Actions cron schedule use?

UTC, always. A cron of 0 9 * * * fires at 09:00 UTC every day of the year, so the local clock time of that run moves by an hour when your region enters or leaves daylight saving. Convert the local time you want into UTC once and write that number down in a comment above the cron. The workflow event definition covers how schedule sits alongside the other triggers.

Why did my scheduled workflow start late or skip a run?

GitHub documents that the schedule event can be delayed during periods of high load and names the start of every hour as one of those windows (events that trigger workflows, checked on 2026-08-13). Move the cron minute away from 0, spread multiple schedules across different minutes, and treat the time as a best-effort start. For public repositories, also check whether 60 days of repository inactivity disabled the schedule.

Can a scheduled workflow run on a branch other than the default?

No. The schedule event runs the workflow as it exists on the latest commit of the default branch, so a cron added on a feature branch produces no runs until it merges. Keep workflow_dispatch in the same file to run the jobs from any ref while you are still iterating, and branch on github.event_name for the steps that only the scheduled path should perform. The base image update workflow guide uses exactly that pairing to open pull requests on a cron.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.