GitHub Actions schedule in your local timezone
For years a scheduled workflow ran in UTC and nothing else, and a workflow meant for 09:00 in Paris ran at 09:00 for half the year and at 08:00 or 10:00 for the other half. That has changed: GitHub's documentation now describes a timezone setting on each schedule. If the time zone was your only problem, the fix is one line, and you do not need anything on this page beyond the first section.
The rest is about what a schedule on GitHub still does not promise, and how to start the workflow from outside at a local time when that matters.
The fix inside GitHub: timezone
From GitHub's events that trigger workflows, checked on 15 October 2026: "By default, scheduled workflows run in UTC. You can optionally specify a timezone using an IANA timezone string." The setting sits next to cron, on each schedule:
on:
schedule:
- cron: '0 9 * * 1-5'
timezone: 'Europe/Paris'
That runs on weekdays at 09:00 in Paris, summer and winter. On the night the clocks go forward, GitHub says a schedule in the missing hour "advances to the next valid time", so a 02:30 job runs at 03:00. The documentation says nothing about the night the clocks go back, when 02:00–03:00 happens twice, so a job in that hour may run once or twice. Keep scheduled times out of 02:00–03:00 and the question does not come up.
What a schedule still does not promise
The same page lists what has not changed with timezone:
- Runs can be late, or dropped. "The
scheduleevent can be delayed during periods of high loads of GitHub Actions workflow runs. High load times include the start of every hour. If the load is sufficiently high enough, some queued jobs may be dropped." GitHub's own advice is to schedule away from the top of the hour. - Every five minutes at most. "The shortest interval you can run scheduled workflows is once every 5 minutes."
- The default branch only. "Scheduled workflows will only run on the default branch."
- Public repositories go quiet. "In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days."
For a nightly test run none of this matters much. For a workflow that has to start at 09:00 on the dot — a report a team reads at 09:05, a release window, a data export someone else picks up — a run that is half an hour late or never happens is the problem, and the time zone was only half of it.
Start the workflow from outside
GitHub can start a workflow on request: a POST to the workflow dispatch endpoint starts a run of a workflow that listens for workflow_dispatch. A scheduler outside GitHub makes that call at the time you choose, and GitHub starts the run then, not when it has capacity for scheduled work.
1. Let the workflow be dispatched
name: Morning digest
on:
workflow_dispatch:
inputs:
triggered_by:
description: What started this run
type: string
default: manual
jobs:
digest:
runs-on: ubuntu-latest
steps:
- name: Send the digest
env:
TRIGGERED_BY: ${{ inputs.triggered_by }}
run: echo "Started by $TRIGGERED_BY"
The file has to be on the default branch. Remove the schedule: block if it had one, or the workflow runs twice. The Run workflow button in the Actions tab now starts it by hand, which is a good first test.
2. A token that can do one thing
Create a fine-grained personal access token with access to this repository only and one permission: Actions: Read and write. That is what the dispatch endpoint requires, according to GitHub's permissions table. Give it an expiry date and put a reminder in your calendar.
3. Create the job
With Clockwork, the scheduled request is a cron job in your time zone. You need an API key with the clockwork scope; see API Keys.
curl -X POST https://api.vernesoft.com/v1/clockwork/jobs \
-H "Authorization: Bearer $VERNE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Morning digest workflow",
"schedule": "0 9 * * 1-5",
"timezone": "Europe/Paris",
"url": "https://api.github.com/repos/OWNER/REPO/actions/workflows/morning-digest.yml/dispatches",
"method": "POST",
"headers": {
"Authorization": "Bearer '"$GH_DISPATCH_TOKEN"'",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28"
},
"body": "{\"ref\": \"main\", \"inputs\": {\"triggered_by\": \"clockwork\"}}"
}'
Replace OWNER/REPO with your repository, and main with the branch the run should use; unlike a schedule, a dispatch can name any branch or tag once the workflow has run once. There is no need to add a Content-Type header: Clockwork sends Content-Type: application/json with every request that has a body, which is what GitHub expects here. A Content-Type you put in the job's headers replaces that default.
At 09:00 Paris time on weekdays, Clockwork sends that POST with your headers, the body, a User-Agent (GitHub refuses requests without one), and its own X-Clockwork-Execution-Id and X-Clockwork-Attempt. GitHub answers 204 No Content and the run appears in the Actions tab with triggered_by set to clockwork.
4. When it fails
Every attempt is in the job's run history, with GitHub's status code and answer. What happens next depends on the code:
| GitHub answers | Usually means | Clockwork |
|---|---|---|
401 | The token expired or was revoked | Not retried. Create a new token and PATCH the job's headers. |
404 | Wrong OWNER/REPO or file name, or the token cannot see the repository | Not retried |
422 | The branch does not exist, the workflow does not listen for workflow_dispatch, or the body has an input the workflow does not declare | Not retried |
5xx, or no answer within 30 seconds | A problem on GitHub's side | Retried after one minute and after five minutes |
A retry is the same request again. If GitHub started the run but the answer was lost, the retry starts a second run, so a workflow that must not run twice should check for itself, for example by looking for today's output before doing the work. See Retries.
A word on the token
The token is stored in the job's headers, and the job list returns headers as they are. Anyone who can read your Clockwork jobs — your dashboard, or an API key with the clockwork scope — can read the token. That is why it should open one repository and start workflows there, and nothing more.
Or with repository_dispatch
The other door is repository_dispatch: POST /repos/OWNER/REPO/dispatches with an event_type, which starts every workflow listening for that type. It needs a token with Contents: Read and write, which can also push code. Unless you want one call to start several workflows, workflow_dispatch asks for less.
Where to go next
- Run a cron job in your timezone, with daylight saving — what happens on the two nights a year the clocks change, and how to check a job before it runs.
- Timezones and daylight saving in the API reference.
- Vercel cron on the Hobby plan: limits and an alternative.
Scheduled jobs are part of Nautilus by Verne Software, sign-in and user management for SaaS teams; the same scheduler runs its automatic deletion of inactive accounts. One plan, priced by active users, hosted in France. Create a free account or see the plans.