Skip to content

feat(tasks): timezone-aware scheduling — pass timezone to the Trigger.dev schedule (chat#1881 3c) - #780

Merged
sweetmantech merged 3 commits into
mainfrom
feat/3c-timezone-weekly-scheduling
Jul 22, 2026
Merged

feat(tasks): timezone-aware scheduling — pass timezone to the Trigger.dev schedule (chat#1881 3c)#780
sweetmantech merged 3 commits into
mainfrom
feat/3c-timezone-weekly-scheduling

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Item 3c (api) — Timezone-aware weekly scheduling

Part of recoupable/chat#1881.

Problem

Weekly-report emails fire on a UTC cron (0 9 * * 1 = 9am UTC), which is the middle of the US night. The goal is to make the send land at 9am local in the customer's IANA timezone (e.g. America/New_York), DST-aware.

What this PR ships (un-blocked, TDD'd core)

  • lib/tasks/timezone/getNextUtcRunForLocalCron.ts — pure function: given an M H * * D cron, an IANA timezone, and a from instant, returns the next UTC instant the schedule fires at, interpreting the cron in local wall-clock time. DST-aware (uses Intl offsets, refines across boundaries).
  • lib/tasks/timezone/isValidTimeZone.ts — IANA timezone validation.
  • 12 tests (red→green), covering:
    • New York winter Monday (EST, UTC-5 → 14:00Z) vs summer Monday (EDT, UTC-4 → 13:00Z)
    • UTC and Asia/Tokyo
    • week rollover when this week's local 9am already passed
    • daily-wildcard day-of-week
    • invalid tz / malformed cron throwing

pnpm test lib/tasks/timezone → 12 passed. eslint clean.

🚧 BLOCKER — needs a database migration first

Accepting and persisting the IANA timezone on a task requires a new column that does not exist today:

  • scheduled_actions has no timezone column (confirmed in types/database.types.ts and in database/supabase/migrations/ — nothing adds one).

Per the issue's guardrail I did not invent a migration. A prerequisite database PR must add:

ALTER TABLE scheduled_actions ADD COLUMN timezone text; -- IANA identifier, nullable; NULL = UTC

Follow-up wiring (lands after the column exists)

Once scheduled_actions.timezone is available:

  1. Accept optional timezone in the create/update contracts (createTaskSchemas.ts / updateTaskSchemas.ts), validated with isValidTimeZone.
  2. Persist it via insertScheduledAction / updateScheduledAction.
  3. Pass timezone to Trigger.dev — createSchedule already accepts it; thread it through updateSchedule + syncTriggerSchedule.
  4. Use getNextUtcRunForLocalCron to populate the existing next_run column so the API reflects the correct local send time.

Related

  • Chat-side timezone picker is a sibling PR (separate frontend work).

🤖 Generated with Claude Code


Summary by cubic

Adds timezone-aware scheduling so weekly/daily tasks run at the user’s local time (IANA, DST-aware), e.g. 9am local instead of 9am UTC. Part of #1881 item 3c.

  • New Features

    • Accept optional timezone in task create/update; validated with isValidTimeZone.
    • Timezone lives only on the Trigger.dev schedule: createTask/syncTriggerSchedule pass it through; cron-only edits preserve the existing timezone via retrieveScheduleTimezone from @trigger.dev/sdk.
    • Removed local UTC conversion helper; the Trigger.dev schedule is the source of truth.
  • Bug Fixes

    • Skip scheduled_actions write on a timezone-only update to prevent a 500 from an empty DB update; return the unchanged row instead.

Written for commit f5a7d83. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added optional timezone support when creating and updating scheduled tasks.
    • Validates timezones using recognized IANA timezone identifiers.
    • Applies timezone settings to schedules, including timezone-only updates.
    • Preserves the existing schedule timezone when no replacement is provided.
    • Added DST-aware schedule handling.

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 22, 2026 9:40pm

Request Review

@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b01f4b2b-12d2-4fdf-84c1-d123cfdbd852

📥 Commits

Reviewing files that changed from the base of the PR and between 5d311e3 and f5a7d83.

⛔ Files ignored due to path filters (1)
  • lib/tasks/__tests__/updateTask.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (1)
  • lib/tasks/updateTask.ts
📝 Walkthrough

Walkthrough

Task creation and updates now accept validated IANA timezones, avoid persisting them in scheduled actions, and propagate them to Trigger.dev schedule creation or synchronization. Existing schedule timezones are retrieved when updates omit a replacement.

Changes

Timezone-aware task scheduling

Layer / File(s) Summary
Timezone validation contracts
lib/tasks/timezone/isValidTimeZone.ts, lib/tasks/createTaskSchemas.ts, lib/tasks/updateTaskSchemas.ts
Adds shared IANA timezone validation and exposes optional timezone fields across task creation and update schemas.
Task handler timezone wiring
lib/tasks/createTask.ts, lib/tasks/updateTask.ts
Keeps timezone out of scheduled-action persistence, passes it to schedule operations, and treats timezone-only updates as schedule changes.
Trigger.dev timezone synchronization
lib/trigger/retrieveScheduleTimezone.ts, lib/trigger/syncTriggerSchedule.ts, lib/trigger/updateSchedule.ts
Creates schedules with supplied timezones and preserves or updates timezone values during schedule synchronization.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant updateTask
  participant syncTriggerSchedule
  participant TriggerSchedules as Trigger.dev schedules API
  updateTask->>syncTriggerSchedule: pass schedule and optional timezone
  syncTriggerSchedule->>TriggerSchedules: retrieve existing timezone when omitted
  TriggerSchedules-->>syncTriggerSchedule: return timezone
  syncTriggerSchedule->>TriggerSchedules: update schedule with resolved timezone
Loading

Poem

Timezones march through the tasking stream,
DST bends the cron-like dream.
Stored actions stay clean and bright,
Trigger schedules keep time right.
A missing zone? The old one stays—
Schedules dance through changing days.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning FAIL: updateTask.ts duplicates TASK_ACCESS_DENIED_MESSAGE from deleteTask.ts, and createTask/updateTask/syncTriggerSchedule all exceed the 20-line function guideline. Extract the shared access-denied constant into one module, and split the task/schedule orchestration into smaller helpers so each function has one clear responsibility.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/3c-timezone-weekly-scheduling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files

Confidence score: 5/5

  • Safe to merge after the addressed issues were fixed.
Architecture diagram
sequenceDiagram
    participant Frontend as Frontend UI
    participant API as API Route
    participant Validator as isValidTimeZone
    participant CronCore as getNextUtcRunForLocalCron
    participant Intl as Intl.DateTimeFormat
    participant DB as Database
    participant TriggerDev as Trigger.dev

    Note over Frontend,TriggerDev: NEW: Timezone-Aware Weekly Scheduling Flow

    Frontend->>API: POST /scheduled_actions (timezone: "America/New_York")
    API->>Validator: Validate IANA timezone
    Validator-->>API: true/false

    alt Invalid Timezone
        API-->>Frontend: 400 Bad Request
    else Valid Timezone
        API->>CronCore: getNextUtcRunForLocalCron("0 9 * * 1", "America/New_York", from)
        
        Note over CronCore: Resolve UTC offset for local cron time
        
        CronCore->>Intl: formatToParts(instant) for DST offset
        Intl-->>CronCore: UTC offset in ms
        
        loop For next 8 days
            CronCore->>CronCore: Check if weekday matches cron DOW
            alt Weekday matches
                CronCore->>Intl: zonedWallTimeToUtc(year, month, day, hour, minute)
                Intl-->>CronCore: UTC candidate instant
                alt candidate > from
                    CronCore-->>API: Next UTC run time
                end
            end
        end
        
        API->>DB: INSERT scheduled_actions (..., timezone, next_run)
        DB-->>API: Created
        
        API->>TriggerDev: createSchedule(id, cron, timezone)
        TriggerDev-->>API: Schedule created
        
        API-->>Frontend: 201 Created + task details
    end

    Note over Frontend,TriggerDev: Follow-Up: Update Schedule Flow

    Frontend->>API: PUT /scheduled_actions/:id (new timezone)
    API->>Validator: Validate new timezone
    API->>CronCore: Recalculate next_run with new timezone
    API->>DB: UPDATE scheduled_actions (timezone, next_run)
    API->>TriggerDev: updateSchedule(id, cron, timezone)
    TriggerDev-->>API: Updated
    API-->>Frontend: 200 OK

    Note over Frontend,TriggerDev: DST Transition Handling (automatic)
    
    CronCore->>Intl: getTimeZoneOffsetMs(instant)
    alt Offset differs from previous calculation
        Note over CronCore: DST boundary crossed
        CronCore->>Intl: Refine with corrected instant
        Intl-->>CronCore: Final DST-aware UTC offset
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread lib/tasks/timezone/getNextUtcRunForLocalCron.ts Outdated
sweetmantech and others added 2 commits July 22, 2026 15:41
…sks (#1881 item 3c)

Weekly-report emails currently fire on a UTC cron (`0 9 * * 1` = 9am UTC),
which is the middle of the US night. Item 3c makes the send land at 9am in
the customer's *local* timezone.

This commit adds the pure, DST-aware core the feature is built on:
- getNextUtcRunForLocalCron(cron, ianaTz, from): computes the next UTC
  instant for an `M H * * D` cron interpreted in an IANA timezone. Handles
  EST/EDT (and any zone) correctly across DST boundaries.
- isValidTimeZone(tz): IANA validation via Intl.

TDD red→green: 12 tests including winter (UTC-5) vs summer (UTC-4) New York
Mondays, UTC/Tokyo, week rollover, and daily-wildcard cases.

BLOCKER: persisting the timezone is blocked on a `database` migration adding
`scheduled_actions.timezone` (no such column exists today). Contract
acceptance, persistence, and schedule wiring follow once that lands. See PR
body for details. Chat-side timezone picker is a sibling PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p local UTC conversion (chat#1881)

Trigger.dev's schedule already owns the timezone: schedules.create/update({ cron,
timezone }) interprets the cron in that IANA zone (DST-aware) and IS the source
of truth for when a task fires. So instead of computing UTC ourselves and storing
a copy of the timezone in scheduled_actions (both of which can diverge from the
schedule), we just thread the timezone through:

- create/update task contracts accept an optional IANA `timezone` (validated via
  isValidTimeZone).
- createTask → createSchedule({ timezone }); the timezone is stripped before the
  Supabase insert (not a scheduled_actions column).
- updateTask → syncTriggerSchedule({ timezone }); a timezone-only change re-syncs
  the schedule, and a cron-only edit preserves the current timezone by reading it
  back from Trigger.dev (retrieveScheduleTimezone) rather than resetting to UTC.
- Removed getNextUtcRunForLocalCron (redundant with Trigger.dev's native tz).
- database#48 (scheduled_actions.timezone) closed — Trigger.dev is the single
  source of truth; no denormalized copy to maintain.

Tests: syncTriggerSchedule 4/4 (create/update/preserve/delete), createTask 3/3
(passes tz to schedule, strips from insert, UTC default). Full lib/tasks +
lib/trigger green; tsc + eslint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sweetmantech
sweetmantech force-pushed the feat/3c-timezone-weekly-scheduling branch from e1dbe17 to 5d311e3 Compare July 22, 2026 21:01
@sweetmantech sweetmantech changed the title feat: timezone-aware weekly scheduling — DST-aware next-run core (#1881 item 3c) feat(tasks): timezone-aware scheduling — pass timezone to the Trigger.dev schedule (chat#1881 3c) Jul 22, 2026
@sweetmantech

Copy link
Copy Markdown
Contributor Author

Reworked — Trigger.dev owns the timezone (no local conversion, no Supabase copy)

Per the chat#1881 3c design discussion: Trigger.dev's schedule already interprets the cron in an IANA timezone, DST-aware (schedules.create/update({ cron, timezone })), and is the source of truth for when the task fires. So this PR no longer computes UTC ourselves or stores a copy of the timezone — it just threads the timezone through to the schedule.

Changes (force-pushed):

  • Contract: POST/PATCH /api/tasks (and the MCP tools) accept an optional IANA timezone, validated by isValidTimeZone.
  • Create: createTaskcreateSchedule({ timezone }); the timezone is stripped before the Supabase insert (it's not a scheduled_actions column).
  • Update: updateTasksyncTriggerSchedule({ timezone }). A timezone-only change re-syncs the schedule; a cron-only edit preserves the current timezone by reading it back from Trigger.dev (retrieveScheduleTimezone) instead of resetting to UTC.
  • Removed getNextUtcRunForLocalCron (redundant with Trigger.dev's native timezone support).
  • Closed database#48 — no scheduled_actions.timezone column; a denormalized copy would have to be kept in sync and can diverge from the schedule.

Tests: syncTriggerSchedule 4/4 (create passes tz · update applies provided tz · cron-only update preserves via retrieve · disabled deletes) · createTask 3/3 (passes tz to schedule · does not persist tz on the row · UTC default). Full lib/tasks + lib/trigger green; tsc + eslint clean.

Follow-up: the docs task contract (OpenAPI CreateTaskRequest/UpdateTaskRequest) should document the optional timezone field — small docs PR. The chat-side timezone picker is chat#1883; it should send the IANA tz on create/update and (since we don't store it) read the current tz from the Trigger.dev schedule when editing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
lib/tasks/createTaskSchemas.ts (1)

36-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize the duplicated timezone contract.

Both schemas independently define the same refinement, error message, and documentation. Extract one shared timezoneSchema and use it in both contracts.

  • lib/tasks/createTaskSchemas.ts#L36-L42: replace the inline schema with the shared timezone schema.
  • lib/tasks/updateTaskSchemas.ts#L25-L31: import and reuse the same shared schema.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/tasks/createTaskSchemas.ts` around lines 36 - 42, Extract the duplicated
timezone refinement, error message, and documentation into a shared
timezoneSchema, then replace the inline schema in lib/tasks/createTaskSchemas.ts
lines 36-42 and import and reuse it in lib/tasks/updateTaskSchemas.ts lines
25-31.

Sources: Coding guidelines, Path instructions

lib/tasks/createTask.ts (1)

14-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Reduce the size of the orchestration functions.

These functions combine several responsibilities and exceed the repository’s small-function guidance:

  • lib/tasks/createTask.ts#L14-L49: separate persistence from schedule registration and compensation.
  • lib/tasks/updateTask.ts#L19-L70: extract authorization/patch construction and schedule synchronization.
  • lib/trigger/syncTriggerSchedule.ts#L27-L62: extract the delete, update, and create operations.

As per coding guidelines, flag functions longer than 20 lines and keep functions small and focused; as per path instructions, domain functions must stay under 50 lines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/tasks/createTask.ts` around lines 14 - 49, Reduce orchestration size by
extracting focused helpers: in lib/tasks/createTask.ts lines 14-49, separate
scheduled-action persistence, Trigger.dev schedule registration, and
compensation; in lib/tasks/updateTask.ts lines 19-70, extract
authorization/patch construction and schedule synchronization; in
lib/trigger/syncTriggerSchedule.ts lines 27-62, extract the delete, update, and
create operations. Keep each domain function under 50 lines and the extracted
functions focused and small.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/tasks/createTask.ts`:
- Around line 22-32: Update the task-creation flow surrounding
insertScheduledAction and createSchedule so a Trigger.dev failure cannot leave
an unscheduled persisted task: add an appropriate idempotency, compensating
deletion, or outbox/reconciliation mechanism. Ensure retries do not create
duplicate tasks or schedules, and preserve the existing validation of the
created task ID.

In `@lib/tasks/timezone/isValidTimeZone.ts`:
- Around line 13-15: Update isValidTimeZone to reject offset-shaped identifiers
such as +01:00, -02:30, +0100, and -23 before invoking Intl.DateTimeFormat;
retain the existing Intl-based validation for named IANA timezones and return
false for rejected offsets.

In `@lib/tasks/updateTask.ts`:
- Around line 39-49: Preserve timezone updates across disabled schedule deletion
and recreation: in lib/tasks/updateTask.ts lines 39-49, retain the supplied
timezone as pending state or reject timezone-only updates when no schedule
exists; in lib/trigger/syncTriggerSchedule.ts lines 31-38, do not discard that
timezone on the disabled/delete path; and in lines 51-57, use the retained
timezone when recreating the schedule.

---

Nitpick comments:
In `@lib/tasks/createTask.ts`:
- Around line 14-49: Reduce orchestration size by extracting focused helpers: in
lib/tasks/createTask.ts lines 14-49, separate scheduled-action persistence,
Trigger.dev schedule registration, and compensation; in lib/tasks/updateTask.ts
lines 19-70, extract authorization/patch construction and schedule
synchronization; in lib/trigger/syncTriggerSchedule.ts lines 27-62, extract the
delete, update, and create operations. Keep each domain function under 50 lines
and the extracted functions focused and small.

In `@lib/tasks/createTaskSchemas.ts`:
- Around line 36-42: Extract the duplicated timezone refinement, error message,
and documentation into a shared timezoneSchema, then replace the inline schema
in lib/tasks/createTaskSchemas.ts lines 36-42 and import and reuse it in
lib/tasks/updateTaskSchemas.ts lines 25-31.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3396c91d-aff9-484a-b78b-88612913013a

📥 Commits

Reviewing files that changed from the base of the PR and between 4df9683 and 5d311e3.

⛔ Files ignored due to path filters (3)
  • lib/tasks/__tests__/createTask.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/tasks/timezone/__tests__/isValidTimeZone.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/trigger/__tests__/syncTriggerSchedule.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (8)
  • lib/tasks/createTask.ts
  • lib/tasks/createTaskSchemas.ts
  • lib/tasks/timezone/isValidTimeZone.ts
  • lib/tasks/updateTask.ts
  • lib/tasks/updateTaskSchemas.ts
  • lib/trigger/retrieveScheduleTimezone.ts
  • lib/trigger/syncTriggerSchedule.ts
  • lib/trigger/updateSchedule.ts

Comment thread lib/tasks/createTask.ts
Comment on lines 22 to 32
// Insert the task into the database
const tasks = await insertScheduledAction(validatedInput);
const tasks = await insertScheduledAction(scheduledActionInput);
const created = tasks[0];

if (!created || !created.id) {
throw new Error("Failed to create task: missing Supabase id for scheduling");
}

// Create the Trigger.dev schedule
// Create the Trigger.dev schedule (it interprets the cron in `timezone`,
// DST-aware; defaults to UTC when omitted).
const triggerSchedule = await createSchedule({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make task creation recoverable across the DB/Trigger.dev boundary.

The database insert completes before createSchedule. If Trigger.dev fails, this method throws after leaving a persisted task without a schedule. Add idempotency, compensating cleanup, or an outbox/reconciliation flow to prevent partial task creation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/tasks/createTask.ts` around lines 22 - 32, Update the task-creation flow
surrounding insertScheduledAction and createSchedule so a Trigger.dev failure
cannot leave an unscheduled persisted task: add an appropriate idempotency,
compensating deletion, or outbox/reconciliation mechanism. Ensure retries do not
create duplicate tasks or schedules, and preserve the existing validation of the
created task ID.

Comment on lines +13 to +15
try {
new Intl.DateTimeFormat("en-US", { timeZone });
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the target file without executing repository code.
if [ -f lib/tasks/timezone/isValidTimeZone.ts ]; then
  echo "== file exists =="
  wc -l lib/tasks/timezone/isValidTimeZone.ts
  cat -n lib/tasks/timezone/isValidTimeZone.ts
else
  echo "target file not found"
  fd -a 'isValidTimeZone' . || true
fi

echo "== usages of isValidTimeZone =="
rg -n "isValidTimeZone|timeZone|UTC|timezone" lib/tasks/timezone lib 2>/dev/null | head -200 || true

echo "== package/runtime Intl behavior probe =="
node - <<'JS'
const candidates = [
  "+01:00",
  "+0100",
  "+01",
  "-02:30",
  "UTC",
  "Etc/UTC",
  "America/New_York",
  "0",
  "+12:15:45",
  "-23",
  "",
];
for (const tz of candidates) {
  let ok, msg;
  try {
    new Intl.DateTimeFormat("en-US", { timeZone: tz });
    ok = true;
  } catch (e) {
    ok = false;
    msg = e && e.message ? e.message : String(e);
  }
  console.log(JSON.stringify({value: tz, accepted: ok, message: msg}));
}
JS

Repository: recoupable/api

Length of output: 13897


Reject offset identifiers before relying on Intl validation.

isValidTimeZone is only used as an IANA timezone validator, but Intl.DateTimeFormat also accepts offset identifiers like +01:00, -02:30, +0100, and -23; those values would pass this check. Add a precheck for offset-shaped input or validate against a named timezone set so the schema reflects the contract.

Suggested narrow fix
   if (typeof timeZone !== "string" || timeZone.length === 0) {
     return false;
   }
+  if (/^[+-]\d{2}(?::?\d{2})?$/.test(timeZone)) {
+    return false;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
new Intl.DateTimeFormat("en-US", { timeZone });
return true;
if (/^[+-]\d{2}(?::?\d{2})?$/.test(timeZone)) {
return false;
}
try {
new Intl.DateTimeFormat("en-US", { timeZone });
return true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/tasks/timezone/isValidTimeZone.ts` around lines 13 - 15, Update
isValidTimeZone to reject offset-shaped identifiers such as +01:00, -02:30,
+0100, and -23 before invoking Intl.DateTimeFormat; retain the existing
Intl-based validation for named IANA timezones and return false for rejected
offsets.

Comment thread lib/tasks/updateTask.ts
Comment on lines +39 to +49
// `timezone` lives on the Trigger.dev schedule, not scheduled_actions.
if (key === "id" || key === "resolvedAccountId" || key === "timezone") return false;
return true;
}),
) as Partial<TablesUpdate<"scheduled_actions">>;

const finalEnabled = enabled !== undefined ? enabled : (existingTask.enabled ?? true);
const cronExpression = schedule ?? existingTask.schedule;
const scheduleChanged = schedule !== undefined;
// A timezone-only change still needs the schedule re-synced (the cron falls
// back to the existing expression), so treat it as a schedule change.
const scheduleChanged = schedule !== undefined || timezone !== undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not lose timezone changes when no active schedule exists.

The task layer strips timezone from persistence, while the synchronization layer deletes or skips schedules when disabled. Re-enabling without a new timezone therefore creates a UTC schedule instead of preserving the accepted value. (trigger.dev)

  • lib/tasks/updateTask.ts#L39-L49: retain pending timezone state or reject timezone-only updates when no schedule exists.
  • lib/trigger/syncTriggerSchedule.ts#L31-L38: do not discard the supplied timezone on the disabled/delete path.
  • lib/trigger/syncTriggerSchedule.ts#L51-L57: use the retained timezone when recreating the schedule.
📍 Affects 2 files
  • lib/tasks/updateTask.ts#L39-L49 (this comment)
  • lib/trigger/syncTriggerSchedule.ts#L31-L38
  • lib/trigger/syncTriggerSchedule.ts#L51-L57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/tasks/updateTask.ts` around lines 39 - 49, Preserve timezone updates
across disabled schedule deletion and recreation: in lib/tasks/updateTask.ts
lines 39-49, retain the supplied timezone as pending state or reject
timezone-only updates when no schedule exists; in
lib/trigger/syncTriggerSchedule.ts lines 31-38, do not discard that timezone on
the disabled/delete path; and in lines 51-57, use the retained timezone when
recreating the schedule.

…chat#1881)

Preview verification caught a 500 on a timezone-only PATCH: the timezone lives on
the Trigger.dev schedule, not scheduled_actions, so `updateData` was empty and
updateScheduledAction({ id }) failed ("multiple (or no) rows returned") on the
empty update. The Trigger.dev schedule DID update correctly — only the trailing
Supabase write errored. Skip the DB write when there are no column changes and
return the unchanged row. Adds updateTask tests (tz-only passes tz to sync +
skips the DB write; real column changes still persist).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Auto-approved: Adds optional IANA timezone support to task scheduling, passing it to Trigger.dev for DST-aware local-time firing. No database changes, no schema migration, no breaking contract changes – the feature is opt-in and defaults to UTC. Well-tested and bounded.

Re-trigger cubic

@sweetmantech

Copy link
Copy Markdown
Contributor Author

✅ Preview verification — timezone flows to the Trigger.dev schedule (+ found & fixed a tz-only 500)

Tested against the preview with a Privy Bearer, creating/patching a real task and reading the schedule back from Trigger.dev directly (schedules.retrieve, prod project). Verified the write side end-to-end and cleaned up after.

# Case Result
1 POST /api/tasks { timezone: "America/Bogota" } ✅ 200; the created Trigger.dev schedule carries timezone: America/Bogota (cron 0 9 * * 1, externalId = task id). Confirmed via schedules.retrieve and by @sweetmantech in the Trigger.dev dashboard. Response body has no timezone (not stored on scheduled_actions) — as designed.
2 POST with invalid tz (Not/AZone) ✅ 400 "timezone must be a valid IANA time zone" (rejected before Trigger.dev)
3 timezone-only PATCH 🐞→✅ initially 500, now fixed — see below; re-verified 200, schedule re-synced to the new tz
4 cron-only PATCH (no tz) ✅ 200; schedule cron changed to 0 8 * * 1, timezone preserved America/Bogota (read back from Trigger.dev, not reset to UTC)
5 DELETE ✅ 200; Trigger.dev schedule removed ("Schedule not found")

Bug found + fixed (f5a7d835)

A timezone-only PATCH returned 500 "Failed to update scheduled action: JSON object requested, multiple (or no) rows returned". Cause: the timezone lives on the Trigger.dev schedule, not scheduled_actions, so updateData was empty and updateScheduledAction({ id }) failed on the empty Supabase update. The Trigger.dev schedule did update correctly — only the trailing DB write errored. Fix: skip the DB write when there are no column changes and return the unchanged row. Added updateTask tests (tz-only passes tz to sync + skips the DB write; real column changes still persist).

Verdict: write side verified end-to-end — ready to merge (docs#275 contract already on main). The read path (surface the schedule's tz on GET) + chat picker remain as separate tracked items (3c·api-read, 3c·chat). Tracking: chat#1881 3c.

@sweetmantech
sweetmantech merged commit a2d9510 into main Jul 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant