temporalio/temporal · error
phase is negative
Error message
phase is negative
What it means
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.
Source
Thrown at service/worker/scheduler/spec.go:265
}
if len(errs) > 0 {
return errors.New("invalid calendar spec: " + strings.Join(errs, ", "))
}
return nil
}
func validateInterval(i *schedulepb.IntervalSpec) error {
if i == nil {
return errors.New("interval is nil")
}
// TODO: use timestamp.ValidateAndCapProtoDuration after switching to state machine based implementation.
// Not adding it to workflow based implementation to avoid potential non-determinism errors.
iv, phase := timestamp.DurationValue(i.Interval), timestamp.DurationValue(i.Phase)
if iv < time.Second {
return errors.New("interval is too small")
} else if phase < 0 {
return errors.New("phase is negative")
} else if phase >= iv {
return errors.New("phase cannot be greater than Interval")
}
return nil
}
func (b *SpecBuilder) loadTimezone(spec *schedulepb.ScheduleSpec) (*time.Location, error) {
if spec.TimezoneData != nil {
return time.LoadLocationFromTZData(spec.TimezoneName, spec.TimezoneData)
}
if cached, ok := b.locationCache.Get(spec.TimezoneName).(*locationAndError); ok {
return cached.loc, cached.err
}
loc, err := time.LoadLocation(spec.TimezoneName)
b.locationCache.Put(spec.TimezoneName, &locationAndError{
loc: loc,
err: err,View on GitHub (pinned to bde624efd1)
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.
Example fix
// before
phase := durationpb.New(anchor.Sub(start)) // negative when start > anchor
// after
phase := durationpb.New(start.Sub(anchor))
if start.Sub(anchor) < 0 {
phase = durationpb.New(0)
} Defensive patterns
Strategy: validation
Validate before calling
if p := iv.GetPhase().AsDuration(); p < 0 {
return fmt.Errorf("phase %s must be >= 0", p)
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Computing phase as (start - anchor) where anchor > start; config files with negative offsets like '-1h'; migration code translating cron offsets with sign errors.
Related errors
- phase cannot be greater than Interval
- out of range
- conflicting timezone names
- ${errs}
- CronString has time zone but missing fields
AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01).
Data as JSON: /api/errors/40bf7fa26fbb843c.
Report an issue: GitHub.