UTC cron and daylight saving in schedules
Schedules match five-field cron against UTC. A local posting time pinned once will drift twice a year unless you rewrite the expression.
schedule_workflow does not know what 9am means in your city. It stores a five-field cron string and matches it against UTC minutes, UTC hours, UTC date, UTC month, and UTC weekday. A cadence you described as "weekdays at 9am Eastern" is only 9am Eastern for the half of the year whose offset you translated. When the clocks move, the UTC expression does not.
If you want the series to keep landing at the same local hour, you rewrite the cron when the offset changes. There is no timezone field on the tool.
The matcher is UTC, field by field
The scheduler ticks every minute, finds rows with schedule_active and next_run_at <= now, runs them, and advances next_run_at with the same expression. The matcher reads:
minute hour day-of-month month day-of-week
It uses getUTCMinutes(), getUTCHours(), getUTCDate(), getUTCMonth() + 1, and getUTCDay() (0 = Sunday through 6 = Saturday, with 7 accepted as Sunday). 0 14 * * 1-5 is 14:00 UTC, Monday through Friday, every month. It is not 14:00 in the browser's local zone, and it is not 14:00 on the account profile.
next_run_at is computed at schedule-create time as the first UTC minute strictly after now that matches. Changing the cron without going through the schedule call leaves the old next_run_at sitting on the row until something rewrites it. The tool is the rewrite.
The agent is instructed to translate your wording into that five-field UTC string itself. "Every day at 10am" becomes 0 10 * * * if 10am is already UTC, or if nobody asked which 10am you meant. The instruction is: ask for a preferred local time when it is unclear; treat unspecified as UTC. If you say "9am" and do not name a zone, you get 09:00 UTC. In August that is 5am Eastern, 10am London, 7pm Sydney. Name the zone.
The syntax primer, including 1,3,5 for Mon/Wed/Fri and */2 on the day-of-month, is in cron schedules for recurring series. This post is the timezone half of the same expression.
Translate a local posting time
Pick the local hour you actually want on the platform, convert it to UTC using the offset in force on the date the expression will run, and write that hour into field two.
US Eastern, weekdays at 9:00 local.
| Season | Offset | 9:00 local is | Cron |
|---|---|---|---|
| Standard (EST, UTC−5) | −5 | 14:00 UTC | 0 14 * * 1-5 |
| Daylight (EDT, UTC−4) | −4 | 13:00 UTC | 0 13 * * 1-5 |
US clocks spring forward on the second Sunday in March and fall back on the first Sunday in November. In 2026 that is 8 March and 1 November.
London, weekdays at 9:00 local.
| Season | Offset | 9:00 local is | Cron |
|---|---|---|---|
| GMT (UTC+0) | +0 | 09:00 UTC | 0 9 * * 1-5 |
| BST (UTC+1) | +1 | 08:00 UTC | 0 8 * * 1-5 |
EU clocks change on the last Sunday in March and the last Sunday in October. In 2026 that is 29 March and 25 October.
Sydney, daily at 9:00 local. Australia moves the other way: daylight time in the southern summer (roughly October to April).
| Season | Offset | 9:00 local is | Cron |
|---|---|---|---|
| AEST (UTC+10) | +10 | 23:00 UTC previous calendar day | 0 23 * * * |
| AEDT (UTC+11) | +11 | 22:00 UTC previous calendar day | 0 22 * * * |
The weekday field is UTC's weekday. A 9am Sydney job that converts to 23:00 UTC runs on the previous UTC date. "Every Monday 9am Sydney" is not 0 23 * * 1. 9:00 Monday AEDT is 22:00 Sunday UTC, so the day-of-week is 0. Get this wrong and the series posts on Sunday evening UTC, which is already Monday morning in Sydney for the offset but on the wrong local Monday if you also pin 1. For a daily Sydney 9am the * in day-of-week saves you. For a weekday-only Sydney 9am, compute the UTC weekday for a real date before you write the field.
Worked request that does the conversion out loud:
"Run this workflow every weekday at 9:00 America/New_York. Right now we are on EDT, so that is 13:00 UTC. Use
0 13 * * 1-5. On 1 November 2026 switch it to0 14 * * 1-5."
The recurring video series capability is the video form of this. The recurring slideshow series uses the same five-field UTC rule. Neither one stores America/New_York.
What happens when the clocks move
Nothing in the scheduler notices. The expression still matches the same UTC hour. Local time around that instant is what moves.
Take 0 14 * * 1-5, written in January because 9:00 EST is 14:00 UTC.
- Through 6 March 2026 (Friday, still EST): fires 09:00 Eastern. Correct.
- From 9 March 2026 (Monday, now EDT): 14:00 UTC is 10:00 Eastern. The series is an hour late, every weekday, until you edit it.
- From 2 November 2026 (Monday, back on EST): 14:00 UTC is 09:00 Eastern again. If you had patched it to
0 13 * * 1-5for summer and forgotten to patch back, it is now an hour early.
Spring-forward also has a hole: 02:00–02:59 local does not exist that night. A cron that had been targeting the UTC equivalent of 02:30 local will simply keep firing at that UTC minute, which is 03:30 local after the jump. Fall-back has a repeated hour. UTC still ticks each minute once, so you do not get a double run from the cron matcher. You get a local time that is not the one on the content calendar.
If the series auto-posts, the drift is public. Scheduled workflows that auto-post is the guardrail piece. The timezone piece is: do not let 1 November be the day you discover the caption still says "this morning" at 10am.
A one-off scheduled_at on a social post is a different mechanism. That is an ISO-8601 timestamp, not a five-field cron. Put the Z on it and you are done. Manage scheduled posts is that path. Do not use cron for "publish this once tomorrow at 9."
How to fix a schedule that drifted
- Confirm what is stored. Ask the agent for the current
schedule_cronandnext_run_at.next_run_atis UTC. Convert it to local yourself. If local is not the hour you wanted, the expression is the bug. - Rewrite the cron for the offset in force now. Same minute and weekday fields. New hour field. Call
schedule_workflowagain with that expression sonext_run_atis recomputed. Do not edit a database row. - Calendar the next clock change. US 2026: 8 March and 1 November. EU 2026: 29 March and 25 October. Put a reminder the Friday before. The job is a one-line cron swap, not a rebuild of the saved workflow.
- Or pause.
schedule_workflowwithactive=false(orset_workflow_modetomanual) stops future runs and keeps the last cron on the row. Unpause after you have written the new expression. Use this when the transition week is also a week you do not want to publish.
The workflow has to already have scenes and an asset map before a schedule will attach. That prerequisite is unchanged by timezone. Build and save first, then attach a cron you have actually converted. The workflows library is where those saved formats live.
Do not "fix" drift by adding a second schedule. You will get two fires. Do not compensate by moving the hour one more step every month. Offsets change twice a year, not twelve times.
FAQ
Can I pass a timezone name to schedule_workflow?
No. The parameters are workflow_id, schedule_cron, schedule_label, series_premise, active, and the auto-post fields. schedule_cron is a five-field UTC expression. If you need America/New_York, you do the conversion and you redo it when the offset changes.
I said "9am" and it runs at the wrong hour. Is the cron wrong?
Usually the cron is exactly what you said, interpreted as UTC. Ask for schedule_cron. If it is 0 9 * * * and you are in Eastern daylight time, that is 05:00 local. Give the zone next time, or give the UTC hour you already computed.
Will I get a double post on the night clocks fall back?
Not from this matcher. Each UTC minute is considered once. The failure mode on that night is a local clock reading that no longer matches the hour you had in mind, not two rows in the run list. Spring-forward cannot skip a UTC minute either; it can only make the local reading jump.
How do I stop a series without losing the cron?
Call the same scheduling tool with active=false. Future runs stop. The expression stays on the row so turning it back on is one call, not a rewrite. If you also need a new hour for the new season, pass the new schedule_cron when you reactivate, so next_run_at is computed from the new expression rather than the stale one.