{"record":{"id":"8b2c5ced41990b7a","repo":"temporalio/temporal","slug":"phase-cannot-be-greater-than-interval","errorCode":null,"errorMessage":"phase cannot be greater than Interval","messagePattern":"phase cannot be greater than Interval","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"service/worker/scheduler/spec.go","lineNumber":267,"sourceCode":"\tif len(errs) > 0 {\n\t\treturn errors.New(\"invalid calendar spec: \" + strings.Join(errs, \", \"))\n\t}\n\treturn nil\n}\n\nfunc validateInterval(i *schedulepb.IntervalSpec) error {\n\tif i == nil {\n\t\treturn errors.New(\"interval is nil\")\n\t}\n\t// TODO: use timestamp.ValidateAndCapProtoDuration after switching to state machine based implementation.\n\t// \tNot adding it to workflow based implementation to avoid potential non-determinism errors.\n\tiv, phase := timestamp.DurationValue(i.Interval), timestamp.DurationValue(i.Phase)\n\tif iv < time.Second {\n\t\treturn errors.New(\"interval is too small\")\n\t} else if phase < 0 {\n\t\treturn errors.New(\"phase is negative\")\n\t} else if phase >= iv {\n\t\treturn errors.New(\"phase cannot be greater than Interval\")\n\t}\n\treturn nil\n}\n\nfunc (b *SpecBuilder) loadTimezone(spec *schedulepb.ScheduleSpec) (*time.Location, error) {\n\tif spec.TimezoneData != nil {\n\t\treturn time.LoadLocationFromTZData(spec.TimezoneName, spec.TimezoneData)\n\t}\n\n\tif cached, ok := b.locationCache.Get(spec.TimezoneName).(*locationAndError); ok {\n\t\treturn cached.loc, cached.err\n\t}\n\tloc, err := time.LoadLocation(spec.TimezoneName)\n\tb.locationCache.Put(spec.TimezoneName, &locationAndError{\n\t\tloc: loc,\n\t\terr: err,\n\t})\n\treturn loc, err","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/temporalio/temporal/blob/bde624efd13fbd3843654058db6d9c716166318b/service/worker/scheduler/spec.go#L249-L285","documentation":"validateInterval requires Phase to be strictly less than Interval. If phase >= interval, every occurrence would be pushed past the interval boundary, collapsing the phase offset into the next period; the scheduler rejects such specs during canonicalizeSpec because they cannot be represented unambiguously.","triggerScenarios":"Submitting an IntervalSpec where Phase duration is equal to or larger than the Interval duration — e.g. Interval=1h with Phase=1h or Phase=90m.","commonSituations":"Misunderstanding phase semantics and setting it as an absolute time-of-day duration (e.g. 13h) on a small interval; spec-building code copying the same duration into both fields; editing interval down without adjusting phase.","solutions":["Reduce the phase to a value in [0, interval), e.g. Interval=24h, Phase=13h is valid but Interval=1h, Phase=13h is not.","Model absolute offsets by increasing the interval instead (e.g. make Interval daily and keep Phase as the time-of-day offset).","Zero the phase if no offset is needed.","Add a client-side check phase < interval before submitting the update."],"exampleFix":"// before\niv := &schedulepb.IntervalSpec{Interval: durationpb.New(time.Hour), Phase: durationpb.New(13 * time.Hour)}\n// after\niv := &schedulepb.IntervalSpec{Interval: durationpb.New(24 * time.Hour), Phase: durationpb.New(13 * time.Hour)}","handlingStrategy":"validation","validationCode":"ivD, phD := iv.GetInterval().AsDuration(), iv.GetPhase().AsDuration()\nif phD >= ivD {\n    return fmt.Errorf(\"phase %s must be < interval %s\", phD, ivD)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep phase strictly less than interval when editing specs","Model absolute time-of-day offsets by enlarging the interval (e.g. 24h) rather than inflating phase","Never copy the interval duration into the phase field","Re-check phase whenever the interval is shortened"],"tags":["go","scheduler","validation","phase","interval"],"backgroundTag":"interval-phase-out-of-range","analyzedSha":"bde624efd13fbd3843654058db6d9c716166318b","analyzedAt":"2026-09-01T07:18:39.080Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}