temporalio/temporal · error

phase cannot be greater than Interval

Error message

phase cannot be greater than Interval

What it means

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.

Source

Thrown at service/worker/scheduler/spec.go:267

	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,
	})
	return loc, err

View on GitHub (pinned to bde624efd1)

Solutions

  1. Reduce the phase to a value in [0, interval), e.g. Interval=24h, Phase=13h is valid but Interval=1h, Phase=13h is not.
  2. Model absolute offsets by increasing the interval instead (e.g. make Interval daily and keep Phase as the time-of-day offset).
  3. Zero the phase if no offset is needed.
  4. Add a client-side check phase < interval before submitting the update.

Example fix

// before
iv := &schedulepb.IntervalSpec{Interval: durationpb.New(time.Hour), Phase: durationpb.New(13 * time.Hour)}
// after
iv := &schedulepb.IntervalSpec{Interval: durationpb.New(24 * time.Hour), Phase: durationpb.New(13 * time.Hour)}
Defensive patterns

Strategy: validation

Validate before calling

ivD, phD := iv.GetInterval().AsDuration(), iv.GetPhase().AsDuration()
if phD >= ivD {
    return fmt.Errorf("phase %s must be < interval %s", phD, ivD)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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