Skip to content

CronScheduleSchema.timezone in packages/spec is an unvalidated z.string() — the authoring tier for a job cron zone does not use the membership predicate its own package exports #16292

Description

@claude

Found while implementing #15872 (valueDomain: 'iana_time_zone' on sys_job.timezone and sys_report_schedule.timezone). Retrofitting the AUTHORING tier was outside that card's scope — it is about two object columns — so it is carded here rather than widened into that PR. Unassigned; for triage.

The reading

Measured on origin/main at dacb73f4f.

packages/spec/src/system/job.zod.ts:

export const CronScheduleSchema = lazySchema(() => z.object({
  type: z.literal('cron'),
  expression: CronExpressionInputSchema.describe(...),
  timezone: z.string().optional().default('UTC').describe('Timezone for cron execution (e.g., "America/New_York")'),
}));

A bare z.string(). After #15872 the platform validates this exact concept in four places — sys_business_unit.timezone, sys_organization.timezone (#14238), and now sys_job.timezone and sys_report_schedule.timezone — all through one predicate, isValueDomainMember('iana_time_zone', value), exported from @objectstack/spec/shared and living in the same package as the schema above. The authoring door for a background job's cron zone does not use it.

Consequence, measured rather than assumed

This is NOT a silent outage: the value is carried to the boundary by runtime/job-schedule.ts (toBoundaryJobSchedule / timezoneOf), reaches CronJobAdapter.schedule, and croner constructed WITH a callback throws on a non-member zone. AppPlugin catches that per job and logs it loudly:

[AppPlugin] Background job FAILED TO SCHEDULE — it will never run   (error level + jobScheduleFailuresTotal)
[AppPlugin] Some background jobs are declared but NOT scheduled

So the failure mode is a job that does not run, reported at boot, per job. What is missing is the earlier and cheaper refusal: defineJob / JobSchema.parse accepts timezone: 'UTC+8' at authoring and build time and says nothing, and the author learns about it from a boot log in whatever environment first starts the app.

Why this is not automatic once the columns are validated

sys_job.timezone is a WRITE-ONLY mirror — DbJobAdapter.upsertJobRow writes it and nothing reads it back — so the column's new valueDomain never judges the value the scheduler actually honours. The two tiers are genuinely separate doors, which is the whole reason this needs its own card rather than riding on #15872.

Proposed shape (for the implementing seat to verify, not a ruling)

Refine the timezone field with the predicate the same package already exports, so a non-member fails at parse with a located Zod issue instead of at boot. The interesting questions for the seat, and why this is not a one-liner:

  • Is it breaking? It narrows what defineJob accepts, so an app whose job carries a zone the platform cannot honour stops BUILDING instead of booting-and-not-running. That is the intended direction, but it is a behaviour change on a published schema and wants the changeset and review tier that implies.
  • The probe is environment-sensitive. Intl.DateTimeFormat membership depends on the runtime's ICU. A build machine and a deployment target that disagree would produce a build-time refusal for a zone the deployment could honour, or vice versa — worth a deliberate answer rather than an accident.
  • Does the interval / once branch need anything? No: only the cron branch carries a zone.
  • sys_job.timezone also has no valueDomain counterpart on the boundary type (JobSchedule.timezone in packages/spec/src/contracts/job-service.ts is a plain string?). Whether the boundary should also be judged, or only the authoring tier, is part of the same decision.

Re-check

git show origin/main:packages/spec/src/system/job.zod.ts | sed -n '14,18p'
git grep -n "isValueDomainMember" -- packages/spec/src/shared/value-domain.zod.ts
git grep -n "timezoneOf" -- packages/runtime/src/job-schedule.ts

Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions