{"record":{"id":"40bf7fa26fbb843c","repo":"temporalio/temporal","slug":"phase-is-negative","errorCode":null,"errorMessage":"phase is negative","messagePattern":"phase is negative","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"service/worker/scheduler/spec.go","lineNumber":265,"sourceCode":"\t}\n\n\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,","sourceCodeStart":247,"sourceCodeEnd":283,"githubUrl":"https://github.com/temporalio/temporal/blob/bde624efd13fbd3843654058db6d9c716166318b/service/worker/scheduler/spec.go#L247-L283","documentation":"validateInterval rejects a negative Phase on an IntervalSpec. Phase offsets the interval start time; a negative value is meaningless (it would place occurrences before the interval epoch), so canonicalizeSpec fails with this error.","triggerScenarios":"Building an IntervalSpec where Phase is a negative duration — e.g. durationpb.New(-2*time.Hour), often from subtracting timestamps in the wrong order or from a signed integer cast in the calling code.","commonSituations":"Computing phase as (start - anchor) where anchor > start; config files with negative offsets like '-1h'; migration code translating cron offsets with sign errors.","solutions":["Negate or recompute the phase so it is zero or positive, e.g. durationpb.New(2*time.Hour).","Clamp negative computed phases to 0 before constructing the IntervalSpec.","Fix the ordering of timestamp subtraction that produces the phase (later - earlier, not earlier - later).","Validate phase >= 0 in client code before submitting the schedule update."],"exampleFix":"// before\nphase := durationpb.New(anchor.Sub(start)) // negative when start > anchor\n// after\nphase := durationpb.New(start.Sub(anchor))\nif start.Sub(anchor) < 0 {\n    phase = durationpb.New(0)\n}","handlingStrategy":"validation","validationCode":"if p := iv.GetPhase().AsDuration(); p < 0 {\n    return fmt.Errorf(\"phase %s must be >= 0\", p)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Compute phase as later-minus-earlier timestamps to avoid negatives","Clamp computed phases to zero when the anchor precedes the start","Check config files for negative offset values like '-1h'","Unit-test phase computation with edge-order timestamps"],"tags":["go","scheduler","validation","phase"],"backgroundTag":"negative-duration-offset","analyzedSha":"bde624efd13fbd3843654058db6d9c716166318b","analyzedAt":"2026-09-01T07:18:39.080Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}