# feat\(scheduler\): run fixed-time tasks in a chosen time zone · PR \#23911 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23911)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: Unavailable fork

Source branch: upstream-pr/scheduler-time-zone

Target branch: main

Head commit: 986158533aebad5757667af1ac4efa5ee8b7e9fa

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/23911)

[Commits](https://git.cafe/pingdotgg/t3code/pull/23911/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/23911/diffs?format=markdown)

## Description

---

## Problem

A fixed-time scheduled task runs on the server's local clock. When the server is in a different time zone from the person using it, "09:00" means 09:00 in the server's zone. This happens with a cloud machine or a home server used remotely. Today the user has to convert the time by hand. They then have to redo it whenever daylight saving starts or ends in either place.

## Change

- `fixed_time` schedules take an optional IANA `timeZone`, such as `America/Chicago`. The server checks it with Intl when a task is saved.
- The next run is worked out on that zone's clock.
  - A time skipped when clocks move forward runs that much later: 02:30 runs at 03:30.
  - A time that repeats when clocks move back runs once.
- Without a zone, nothing changes. Runs use the server's local time, as before.
- A save that leaves out `timeZone` keeps the stored zone, so a client that predates the field can't clear it by editing a task. `null` removes it. This matches how webhook `filters` and `dedupe` are kept.
- Stored zones are read without the Intl check, so a client whose time zone data lacks a zone still lists the task. Its forms leave an unchanged zone out of a save, so other edits still work.
- Web settings: a **Time zone** field under **At a time**, with this browser's zone first in the suggestions. The task list row shows the zone.
- Mobile settings: a **Time zone** field, and the zone in the task list.
- The agent tools (`schedule_task`, `update_scheduled_task`) and the agent instructions describe the field. The user guide covers it.

## Scope and approval

There was no prior discussion. This is a configuration option for an existing capability: fixed-time schedules.

It only decides which clock a fixed-time task's time of day is read on. It adds no schedule type. It doesn't change intervals, webhooks, missed-run handling, or how a run starts.

Without the option, tasks behave exactly as before, and stored tasks read back unchanged. There is no database migration: the zone lives in the schedule JSON the server already stores.

## Verification

- Focused tests pass: 9 files, 123 tests.

  ```sh
  vp test run apps/server/src/scheduledTasks/Schedule.test.ts \
    apps/server/src/scheduledTasks/ScheduledTaskService.schedule.test.ts \
    apps/server/src/scheduledTasks/ScheduledTaskService.test.ts \
    apps/server/src/scheduledTasks/ScheduledTaskService.webhook.test.ts \
    apps/server/src/mcp/toolkits/orchestrator/tools.test.ts \
    apps/server/src/provider/CodexDeveloperInstructions.test.ts \
    packages/contracts/src/scheduledTask.test.ts \
    apps/web/src/components/settings/scheduledTasksSettings.logic.test.ts \
    apps/mobile/src/features/settings/scheduledTaskDraft.test.ts
  ```

  New tests cover:
  - the next run in a named zone, not the server's;
  - the spring-forward day (March 8, 2026) and the fall-back day (November 1, 2026) in `America/Chicago`, including a skipped time and a repeated time, and a 30-minute change on Lord Howe Island;
  - weekdays counted in the zone's calendar;
  - no next run for a zone the runtime doesn't know;
  - the Intl check when a task is saved, and no check when one is read;
  - a save without `timeZone` keeps the stored zone, and `null` clears it;
  - the web and mobile forms: round trip, blank clears the zone, a newly typed unknown zone is refused while a stored one is kept, and names are stored in their standard spelling.
- `vp run check:changed` passes: format, lint, knip, graphql, typecheck of 32 packages, and the tests beside the changed files.
- Manual check on a scratch server running this branch, on a machine whose clock is US Eastern:
  - I created a weekday 09:00 task with `timeZone: "America/Chicago"` through the MCP `schedule_task` tool. It was 01:30 UTC on October 23, which is Thursday evening in Chicago.
  - Its stored next run was `2026-10-23T14:00:00.000Z`, which is Friday 09:00 in Chicago.
  - The same task without a zone on `main` stores `13:00:00.000Z`, which is 09:00 Eastern.
- Screenshots are from the web app built from `main` and from this branch, each against its own scratch server. In the list row, the next run moves an hour later because the scratch server's clock is Eastern.

**Task editor**

| Before | After |
| --- | --- |
| ![Task editor before](https://raw.githubusercontent.com/willsmanley/t3-code/65d742697cbbe725daa6dfae987e09cd7f60fbef/before-editor.png) | ![Task editor after, with America/Chicago](https://raw.githubusercontent.com/willsmanley/t3-code/65d742697cbbe725daa6dfae987e09cd7f60fbef/after-editor.png) |

**Task list**

| Before | After |
| --- | --- |
| ![Task list row before](https://raw.githubusercontent.com/willsmanley/t3-code/65d742697cbbe725daa6dfae987e09cd7f60fbef/before-list-row.png) | ![Task list row after, showing the zone](https://raw.githubusercontent.com/willsmanley/t3-code/65d742697cbbe725daa6dfae987e09cd7f60fbef/after-list-row.png) |

- I did not check the mobile screen on a device or simulator. Its form logic is covered by the draft tests and typecheck only.
- I did not capture the time zone suggestion list. It is a native browser popup and doesn't appear in headless screenshots.

Model: Claude Opus 5.5 (Claude Code)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

