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

  1. Negate or recompute the phase so it is zero or positive, e.g. durationpb.New(2*time.Hour).
  2. Clamp negative computed phases to 0 before constructing the IntervalSpec.
  3. Fix the ordering of timestamp subtraction that produces the phase (later - earlier, not earlier - later).
  4. 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

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


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/40bf7fa26fbb843c. Report an issue: GitHub.