temporalio/temporal · error

interval is nil

Error message

interval is nil

What it means

validateInterval rejects an IntervalSpec that is nil. A nil interval means the interval portion of the schedule spec was never populated, so the scheduler cannot compute occurrences; canonicalizeSpec calls this during spec compilation and fails fast with this error.

Source

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

	checkRanges(scs.Hour, "Hour", 0, 23)
	checkRanges(scs.DayOfMonth, "DayOfMonth", 1, 31)
	checkRanges(scs.Month, "Month", 1, 12)
	checkRanges(scs.Year, "Year", minCalendarYear, maxCalendarYear)
	checkRanges(scs.DayOfWeek, "DayOfWeek", 0, 6)

	if len(scs.Comment) > maxCommentLen {
		errs = append(errs, "comment is too long")
	}

	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)
	}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Ensure the IntervalSpec is fully constructed (both Interval and Phase durations) before including it in the spec.
  2. Check for nil before appending to spec.Interval and return a clearer config error to the user.
  3. If an interval schedule was intended, set e.g. i.Interval = durationpb.New(24*time.Hour).
  4. If a calendar or cron-based spec was intended, remove the empty interval entry entirely instead of sending a nil.

Example fix

// before
var iv *schedulepb.IntervalSpec // left nil
spec.Interval = []*schedulepb.IntervalSpec{iv}
// after
iv := &schedulepb.IntervalSpec{Interval: durationpb.New(24 * time.Hour)}
spec.Interval = []*schedulepb.IntervalSpec{iv}
Defensive patterns

Strategy: validation

Validate before calling

if iv == nil {
    return errors.New("interval spec must be set before creating schedule")
}
if iv.Interval == nil || iv.Interval.AsDuration() <= 0 {
    return errors.New("interval duration must be positive")
}

Type guard

func hasInterval(spec *schedulepb.ScheduleSpec) bool {
    return len(spec.GetInterval()) > 0 && spec.GetInterval()[0] != nil && spec.GetInterval()[0].GetInterval() != nil
}

Prevention

When it happens

Trigger: Submitting a ScheduleSpec whose Interval list contains a nil *schedulepb.IntervalSpec — typically when building the protobuf message with an unpopulated pointer, or an update request that clears the interval field.

Common situations: Constructing schedulepb.ScheduleSpec in Go where an interval variable was declared but not assigned; JSON/YAML deserialization omitting the interval object; partial updates that nil out Interval accidentally.

Related errors


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