Repository navigation
Name a zone on every schedule and return when it runs next - #363
Merged
Merged
Conversation
The backend decides when a routine fires, but the API never said when the next run is, so a client had to rebuild the cron and timezone math itself, and a schedule saved without a zone ran on the server's local clock, which nobody could see. The scheduler now fires by one occurrence rule, the engine the hosted product runs (cron.ts nextOccurrence). The tick fires a schedule in a minute that rule names (isOccurrence, one minute's check however rare the cron) unless it fired it there already, and schedules/repo.ts nextRunAt names the next such minute, which the automations API returns as next_due_at and a bot's routines as nextDueAt. A search passes the days a cron rules out by calendar arithmetic, so a rare cron costs a fraction of a millisecond rather than a walk through years. Across a DST change a fixed time now fires once at its first reading of a repeated hour and right after a skipped one, where before it fired twice in a fall-back and never in a spring-forward gap. Every schedule names its zone. Migration 0114 writes the zone the backend's clock runs in (the pool names it in useagent.backend_time_zone) into every schedule saved without one, so each keeps firing at the times it did, and makes the column NOT NULL; applied without that setting it refuses rather than guess. The previous backend serves until the swap and still saves a schedule sent without a zone with none: a trigger gives that write the same zone. The migration takes its table lock first, so it never deadlocks with a writer that already holds a row lock. The engine no longer reads a server-local clock, and a zone Intl cannot read names no run. A create that sends no zone (an API client, an agent's tool call) runs in UTC, stored and returned; the web app sends the creator's browser zone. A PATCH may change the zone; one that names none (a client still holding a schedule from before the backfill sends null) leaves it as it is.
abhishekgahlot2
force-pushed
the
routine-next-run-zones
branch
from
October 18, 2026 01:09
80fb3c9 to
359e8bb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The backend decides when a routine fires, but the API never said when the next run is, so a client had to rebuild the cron and timezone math itself. A schedule saved without a zone also ran on the server's local clock, which nobody could see. This change gives the backend one schedule engine, returns its next run on the API, and gives every schedule an explicit zone. Backend only; the web app is unchanged.
What changes:
backend/src/schedules/cron.tsgetsnextOccurrence, the engine the hosted product runs, andisOccurrence, the same rule for a single minute. The tick fires a schedule in a minuteisOccurrencenames, unless it already fired it in that minute.schedules/repo.tsnextRunAtnames the next such minute, and the API returns it:next_due_aton/api/automations(and/api/schedules),nextDueAton/api/bots/:id/routines, andnext_due_atin the agent's automation tools. It isnullwhile a schedule is paused or when its cron names no date. A client can show the next run without computing it.*in the minute or hour field) now fires once, at the first reading of a time a fall-back repeats, and at the first minute after a time a spring-forward skips. Before, it fired twice in a fall-back and never in the gap. A periodic expression keeps real time. Lord Howe's half-hour change is covered.0114_schedule_zoneswrites the zone the backend's clock runs in into every schedule saved without one, so each keeps firing at the times it did, and makes the columnNOT NULL. The pool passes that zone in asuseagent.backend_time_zone. Applied without it (a scratch database, a tool), the migration refuses any schedule that needs a zone rather than guess one. The engine no longer reads a server-local clock, and a zoneIntlcannot read names no run.UTC, stored and returned; the web app already sends the creator's browser zone. APATCHmay change the zone, and one that names none (a client still holding a schedule from before the backfill sendsnull) leaves it as it is.Tests (each failed before the change):
isOccurrencematchingnextOccurrenceminute by minute across DST windows;UTCon a zone-less create, and a zone-lessPATCHkeeping the zone.Need help on this PR? Tag
@codesmith-botwith what you need.