temporalio/temporal · error

invalid schedule: %s

Error message

invalid schedule: %s

What it means

handleListMatchingTimesQuery answers ListScheduleMatchingTimes queries by iterating the scheduler's parsed calendar spec (s.cspec). If cspec is nil — the schedule workflow started before the spec was loaded or the workflow state is corrupt — the query cannot be answered and the workflow's InvalidScheduleError is surfaced.

Source

Thrown at service/worker/scheduler/workflow.go:1170

func (s *scheduler) handleDescribeQuery() (*schedulespb.DescribeResponse, error) {
	// this is a query handler, don't modify s.Info directly
	infoCopy := common.CloneProto(s.Info)
	infoCopy.FutureActionTimes = s.getFutureActionTimes(false, s.tweakables.FutureActionCount)
	infoCopy.BufferSize = int64(len(s.State.BufferedStarts))

	return &schedulespb.DescribeResponse{
		Schedule:      s.Schedule,
		Info:          infoCopy,
		ConflictToken: s.State.ConflictToken,
	}, nil
}

func (s *scheduler) handleListMatchingTimesQuery(req *workflowservice.ListScheduleMatchingTimesRequest) (*workflowservice.ListScheduleMatchingTimesResponse, error) {
	if req == nil || req.StartTime == nil || req.EndTime == nil {
		return nil, errors.New("missing or invalid query")
	}
	if s.cspec == nil {
		return nil, fmt.Errorf("invalid schedule: %s", s.Info.InvalidScheduleError)
	}

	var out []*timestamppb.Timestamp
	t1 := timestamp.TimeValue(req.StartTime)
	for range maxListMatchingTimesCount {
		// don't need to call GetNextTime in SideEffect because this is just a query
		res, err := s.cspec.GetNextTime(s.jitterSeed(), t1)
		if err != nil {
			// An over-excluded spec won't resolve until it's edited, so return a
			// non-retryable code: retrying would just re-burn the compute bound each call.
			return nil, ErrScheduleSpecLimitHit
		}
		t1 = res.Next
		if t1.IsZero() || t1.After(timestamp.TimeValue(req.EndTime)) {
			break
		}
		out = append(out, timestamppb.New(t1))
	}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Retry the query after the schedule workflow has fully started (brief delay)
  2. Check the schedule with DescribeSchedule and fix the invalid schedule spec reported in InvalidScheduleError
  3. Delete and recreate the schedule with a valid spec if state is unrecoverable

Example fix

// before
resp, err := frontendClient.ListScheduleMatchingTimes(ctx, req) // immediately after CreateSchedule
// after
schedule.Handle(ctx, client, "sched-id", func(s schedule.ScheduleClientHandle) error {
    return require.Eventually-style polling until DescribeSchedule succeeds
})
Defensive patterns

Strategy: try-catch

Validate before calling

// check schedule validity first
_, err := frontendClient.DescribeSchedule(ctx, &workflowservice.DescribeScheduleRequest{ScheduleId: id})
if err != nil { return err } // then inspect Info.InvalidSchedule before querying

Try / catch

resp, err := client.ListScheduleMatchingTimes(ctx, req)
var svcErr *serviceerror.InvalidArgument
if errors.As(err, &svcErr) && strings.Contains(svcErr.Message, "invalid schedule") {
    // wait for workflow start / inspect InvalidScheduleError via DescribeSchedule
}

Prevention

When it happens

Trigger: Sending a ListScheduleMatchingTimes query to a schedule workflow whose cspec is nil, typically right after workflow start before initialization completes, or when the schedule was created invalid/legacy state.

Common situations: Querying a schedule immediately after Describe/Create; schedule workflows recovered from old versions; s.Info.InvalidScheduleError set by an earlier failed spec parse.

Related errors


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