A GitHub Actions scheduled workflow uses the schedule event with a five-field POSIX cron expression, runs in UTC unless you set a timezone, and executes only from the default branch. The shortest interval is 5 minutes, but runs can start late at busy times, such as the top of the hour, and in public repositories they are disabled automatically after 60 days without repository activity.
Who this is for and what you will have at the end
This guide is for developers, administrators and automation builders who want a job to run on a timetable, such as a nightly report, a weekly cleanup or a periodic API sync, and who need to understand why a run was late or missing.
At the end you will have:
- A working scheduled workflow with the right cron syntax and time zone.
- A manual trigger so you can test without waiting.
- Protection against overlapping runs.
- A checklist for diagnosing late, skipped or disabled schedules.
Prerequisites
- A GitHub repository where you can push to the default branch.
- Actions enabled for the repository.
- Familiarity with the YAML structure of a workflow file in
.github/workflows/.
Step 1: Create the scheduled workflow
Create .github/workflows/nightly.yml on the default branch:
name: Nightly report
on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run the job
run: echo "Running at $(date -u)"The schedule event triggers a workflow at the time you specify. Two details matter immediately:
- The workflow file must exist on the default branch for the schedule to trigger. A schedule defined only on a feature branch does nothing.
GITHUB_SHAfor a scheduled run is the last commit on the default branch, andGITHUB_REFis the default branch. The job always sees the latest default-branch code.
The workflow_dispatch entry adds a Run workflow button on the Actions tab. It also only receives events when the workflow file is on the default branch.
Step 2: Write the cron expression
GitHub Actions uses POSIX cron syntax with five fields:
┌───────── minute (0 - 59)
│ ┌─────── hour (0 - 23)
│ │ ┌───── day of month (1 - 31)
│ │ │ ┌─── month (1 - 12 or JAN-DEC)
│ │ │ │ ┌─ day of week (0 - 6 or SUN-SAT)
│ │ │ │ │
* * * * *The supported operators are * (any value), , (a list), - (a range) and / (a step). Non-standard shortcuts such as @daily or @reboot are not supported.
| Expression | Meaning |
|---|---|
*/15 * * * * | Every 15 minutes |
17 3 * * * | Every day at 03:17 |
30 5 * * 1-5 | 05:30 on weekdays |
0 9 1 * * | 09:00 on the first day of each month |
15 4,5 * * * | 04:15 and 05:15 every day |
The 5-minute minimum
The shortest interval you can run scheduled workflows is once every 5 minutes. */5 * * * * is the fastest valid setting, and expressions such as * * * * * don't give you per-minute runs.
Several schedules in one workflow
A single workflow can have more than one schedule entry. Use the github.event.schedule context to see which entry triggered the run:
on:
schedule:
- cron: "17 3 * * 1-5"
- cron: "43 7 * * 6"
jobs:
report:
runs-on: ubuntu-latest
steps:
- name: Which schedule fired
run: echo "${{ github.event.schedule }}"Step 3: Set the time zone
Scheduled workflows run in UTC by default. To use another zone, add a timezone key with an IANA time zone name:
on:
schedule:
- cron: "30 5 * * 1-5"
timezone: "America/New_York"This runs at 5:30 AM Eastern on weekdays. For zones that observe daylight saving time, an hour skipped during the spring-forward change moves to the next valid time. The documentation's example is a 2:30 AM schedule that advances to 3:00 AM. If you stay in UTC instead, remember that the local time of your run shifts by an hour when your region changes its clocks.
Step 4: Prevent overlapping runs
If a run takes longer than the interval, the next one starts while the first is still going. A concurrency group controls this:
concurrency:
group: nightly-report
cancel-in-progress: falseBy default, a concurrency group allows one running and one pending run: a newly queued run replaces any existing pending run. Setting cancel-in-progress: true cancels the running job when a new one starts, which suits jobs where only the latest result matters but not jobs that must finish, such as a data sync. Group names are case-insensitive.
concurrency can be set at the workflow level or on a job.
Step 5: Test it without waiting
- Commit the workflow to the default branch.
- Open the repository's Actions tab, choose the workflow in the left sidebar and select Run workflow. The run is a
workflow_dispatchrun, so it tests the steps, though not the cron timing. - For the schedule itself, set a cron a few minutes ahead, wait, and check the run list. Expect that the run might start late.
You can also start a run from the command line with the GitHub CLI:
gh workflow run nightly.yml
gh run list --workflow nightly.yml --limit 5Why scheduled runs are late or skipped
High load delays
The schedule event can be delayed during periods of high load of GitHub Actions workflow runs. High load times include the start of every hour. If the load is sufficiently high, queued jobs may be dropped. To reduce the chance of delay, schedule your workflow to run at a different time of the hour. That is why the examples here use minutes such as 17 and 43 rather than 0.
Treat a schedule as "about this time", not as an exact timer. If your task needs exact timing, trigger it from a service built for that and use Actions for the work itself, for instance by calling the workflow_dispatch API from an external scheduler.
The workflow is not on the default branch
Scheduled workflows run on the latest commit on the default branch, and only if the workflow file exists on that branch. If you renamed the default branch or merged the workflow into a different branch, it will not trigger.
Automatic disabling after 60 days
In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days. The fix is to enable the workflow again:
- In the web UI, open the Actions tab, select the disabled workflow in the left sidebar and select Enable workflow.
- With the CLI, run
gh workflow enable WORKFLOW, whereWORKFLOWis the name, ID or file name.
gh workflow enable nightly.ymlGitHub's documentation says this rule applies to public repositories. To keep a quiet public repository's schedule alive, you need real repository activity within the window, or you re-enable the workflow when it stops.
A different actor than you expect
Some repository events change the actor for scheduled runs. For example, a user who changes the default branch becomes the actor, and if a user with write access reactivates a disabled scheduled workflow by committing a change to the cron value, that user becomes the actor. Notifications about the workflow go to the user who last modified the cron syntax in the workflow file. On Enterprise Managed Users accounts, the actor's account must be active, and deprovisioning the account in the identity provider stops the run.
If failure emails are going to someone who has left, have a current maintainer make a small edit to the cron line so notifications go to them.
Limits worth knowing
| Limit | Value |
|---|---|
| Shortest schedule interval | 5 minutes |
| Job execution time on GitHub-hosted runners | 6 hours; longer jobs are terminated and fail |
| Workflow run time | 35 days including waiting and approval time |
| Trigger events | 1,500 per 10 seconds per repository |
workflow_dispatch inputs | Up to 25 top-level properties, payload under 65,535 characters |
GITHUB_TOKEN API requests | 1,000 per hour per repository |
Total concurrent jobs on GitHub-hosted standard runners depend on the plan: 20 for Free, 40 for Pro, 60 for Team and 500 for Enterprise.
Troubleshooting
| Symptom | What to check |
|---|---|
| The workflow never ran on its schedule | Is the file on the default branch? Is the cron valid with five fields? Is the workflow disabled in the Actions tab? |
| It ran, but 10 or more minutes late | Likely high load. Move the schedule off minute 0 and to a quieter minute. |
| It stopped running after weeks of working | In a public repository, check for automatic disabling after 60 days with no repository activity, and enable it again. |
| It runs at the wrong local time | The default is UTC. Add timezone or convert the time yourself. |
| Two runs overlap | Add a concurrency group. |
@daily is rejected or ignored | Non-standard cron shortcuts aren't supported. Use five fields. |
| Run fails with a long-running job | Jobs on GitHub-hosted runners stop at 6 hours. Split the job. |
For a worked example of a scheduled pipeline that publishes content, see /blog/automate-blog-social-media-posting-claude-github-actions. If the scheduled job signs in to Azure, see /blog/workload-identity-federation-github-actions-azure to avoid storing long-lived secrets.
Checklist
- The workflow file is on the default branch.
- The cron has five fields and avoids minute 0 unless exact timing is unimportant.
- A
timezoneis set if you think in local time. workflow_dispatchis included for manual testing.- A
concurrencygroup prevents overlap. - In a public repository, someone knows about the 60-day rule and checks the schedule is still enabled.
- Whoever last edited the cron line is the right person to receive failure notifications.
References
- Events that trigger workflows (schedule): https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows
- Workflow syntax for GitHub Actions: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
- Disabling and enabling a workflow: https://docs.github.com/en/actions/how-tos/manage-workflow-runs/disable-and-enable-workflows
- Actions limits: https://docs.github.com/en/actions/reference/limits
Comments
Questions and tips are welcome. Comments are checked by the EpiFive team before they appear; please do not share passwords, phone numbers or other private details.