From 40c3ca18bbcad6549c863f21de5420928e8af90c Mon Sep 17 00:00:00 2001 From: Nasit Sarwar Sony Date: Fri, 22 May 2026 16:47:58 -0700 Subject: [PATCH 1/4] Add temporal schedule list-matching-times command Implements the ListScheduleMatchingTimes RPC in the CLI. Allows users to preview when a schedule will fire within a given time range without executing workflows. Closes #1030 --- internal/temporalcli/commands.gen.go | 30 +++++++++++++++++++++++ internal/temporalcli/commands.schedule.go | 28 +++++++++++++++++++++ internal/temporalcli/commands.yaml | 27 ++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index 03424647a..88206bd25 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -2103,6 +2103,7 @@ func NewTemporalScheduleCommand(cctx *CommandContext, parent *TemporalCommand) * s.Command.AddCommand(&NewTemporalScheduleDeleteCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalScheduleDescribeCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalScheduleListCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalScheduleListMatchingTimesCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalScheduleToggleCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalScheduleTriggerCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalScheduleUpdateCommand(cctx, &s).Command) @@ -2270,6 +2271,35 @@ func NewTemporalScheduleListCommand(cctx *CommandContext, parent *TemporalSchedu return &s } +type TemporalScheduleListMatchingTimesCommand struct { + Parent *TemporalScheduleCommand + Command cobra.Command + ScheduleIdOptions + StartTime cliext.FlagTimestamp + EndTime cliext.FlagTimestamp +} + +func NewTemporalScheduleListMatchingTimesCommand(cctx *CommandContext, parent *TemporalScheduleCommand) *TemporalScheduleListMatchingTimesCommand { + var s TemporalScheduleListMatchingTimesCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "list-matching-times [flags]" + s.Command.Short = "List matching times for a Schedule" + s.Command.Long = "List the times a Schedule would fire within a given time range.\nUse this command to preview when a Schedule will trigger Workflow\nExecutions without actually running them.\n\nFor example:\ntemporal schedule list-matching-times \\\n--schedule-id \"YourScheduleId\" \\\n--start-time \"2024-01-01T00:00:00Z\" \\\n--end-time \"2024-01-31T23:59:59Z\"" + s.Command.Args = cobra.NoArgs + s.Command.Flags().Var(&s.StartTime, "start-time", "Start of time range to list matching times. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "start-time") + s.Command.Flags().Var(&s.EndTime, "end-time", "End of time range to list matching times. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "end-time") + s.ScheduleIdOptions.BuildFlags(s.Command.Flags()) + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type TemporalScheduleToggleCommand struct { Parent *TemporalScheduleCommand Command cobra.Command diff --git a/internal/temporalcli/commands.schedule.go b/internal/temporalcli/commands.schedule.go index 5deadb8ec..7da40e779 100644 --- a/internal/temporalcli/commands.schedule.go +++ b/internal/temporalcli/commands.schedule.go @@ -11,6 +11,7 @@ import ( "github.com/temporalio/cli/cliext" "github.com/temporalio/cli/internal/printer" "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/timestamppb" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" @@ -606,3 +607,30 @@ func formatDuration(d time.Duration) string { s = strings.TrimSpace(s) return s } + +func (c *TemporalScheduleListMatchingTimesCommand) run(cctx *CommandContext, args []string) error { + cl, err := dialClient(cctx, &c.Parent.ClientOptions) + if err != nil { + return err + } + defer cl.Close() + + res, err := cl.WorkflowService().ListScheduleMatchingTimes(cctx, &workflowservice.ListScheduleMatchingTimesRequest{ + Namespace: c.Parent.Namespace, + ScheduleId: c.ScheduleId, + StartTime: timestamppb.New(c.StartTime.Time()), + EndTime: timestamppb.New(c.EndTime.Time()), + }) + if err != nil { + return err + } + + cctx.Printer.StartList() + defer cctx.Printer.EndList() + + for _, t := range res.StartTime { + cctx.Printer.Printlnf("%v", t.AsTime()) + } + + return nil +} diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index 78bdf3513..0d7a07196 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -2320,6 +2320,33 @@ commands: - overlap-policy - schedule-id + - name: temporal schedule list-matching-times + summary: List matching times for a Schedule + description: | + List the times a Schedule would fire within a given time range. + Use this command to preview when a Schedule will trigger Workflow + Executions without actually running them. + + For example: + + ``` + temporal schedule list-matching-times \ + --schedule-id "YourScheduleId" \ + --start-time "2024-01-01T00:00:00Z" \ + --end-time "2024-01-31T23:59:59Z" + ``` + options: + - name: start-time + type: timestamp + description: Start of time range to list matching times. + required: true + - name: end-time + type: timestamp + description: End of time range to list matching times. + required: true + option-sets: + - schedule-id + - name: temporal schedule create summary: Create a new Schedule description: | From 48888b879e64eec3c67b8698b602c73ad5c8316c Mon Sep 17 00:00:00 2001 From: Nasit Sarwar Sony Date: Sat, 23 May 2026 08:41:13 -0700 Subject: [PATCH 2/4] Add experimental tag to list-matching-times command --- internal/temporalcli/commands.gen.go | 8 ++++++-- internal/temporalcli/commands.yaml | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index 88206bd25..edc16796a 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -2284,8 +2284,12 @@ func NewTemporalScheduleListMatchingTimesCommand(cctx *CommandContext, parent *T s.Parent = parent s.Command.DisableFlagsInUseLine = true s.Command.Use = "list-matching-times [flags]" - s.Command.Short = "List matching times for a Schedule" - s.Command.Long = "List the times a Schedule would fire within a given time range.\nUse this command to preview when a Schedule will trigger Workflow\nExecutions without actually running them.\n\nFor example:\ntemporal schedule list-matching-times \\\n--schedule-id \"YourScheduleId\" \\\n--start-time \"2024-01-01T00:00:00Z\" \\\n--end-time \"2024-01-31T23:59:59Z\"" + s.Command.Short = "List matching times for a Schedule (Experimental)" + if hasHighlighting { + s.Command.Long = "\nNote: This is an experimental feature and may change in the future.\n\nList the times a Schedule would fire within a given time range.\nUse this command to preview when a Schedule will trigger Workflow\nExecutions without actually running them.\n\nFor example:\n\n\x1b[1m temporal schedule list-matching-times \\\n --schedule-id \"YourScheduleId\" \\\n --start-time \"2024-01-01T00:00:00Z\" \\\n --end-time \"2024-01-31T23:59:59Z\"\x1b[0m" + } else { + s.Command.Long = "\nNote: This is an experimental feature and may change in the future.\n\nList the times a Schedule would fire within a given time range.\nUse this command to preview when a Schedule will trigger Workflow\nExecutions without actually running them.\n\nFor example:\n\n```\n temporal schedule list-matching-times \\\n --schedule-id \"YourScheduleId\" \\\n --start-time \"2024-01-01T00:00:00Z\" \\\n --end-time \"2024-01-31T23:59:59Z\"\n```" + } s.Command.Args = cobra.NoArgs s.Command.Flags().Var(&s.StartTime, "start-time", "Start of time range to list matching times. Required.") _ = cobra.MarkFlagRequired(s.Command.Flags(), "start-time") diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index 0d7a07196..6f9304329 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -2321,8 +2321,11 @@ commands: - schedule-id - name: temporal schedule list-matching-times - summary: List matching times for a Schedule + summary: List matching times for a Schedule (Experimental) description: | + + Note: This is an experimental feature and may change in the future. + List the times a Schedule would fire within a given time range. Use this command to preview when a Schedule will trigger Workflow Executions without actually running them. From d6b2bc5a2a106a2b715b787eb61a8e103b580a2a Mon Sep 17 00:00:00 2001 From: Nasit Sarwar Sony Date: Fri, 29 May 2026 15:38:04 -0700 Subject: [PATCH 3/4] Add functional tests for list-matching-times and fix JSON output --- internal/temporalcli/commands.schedule.go | 6 ++- .../temporalcli/commands.schedule_test.go | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/internal/temporalcli/commands.schedule.go b/internal/temporalcli/commands.schedule.go index 7da40e779..2a1675eb1 100644 --- a/internal/temporalcli/commands.schedule.go +++ b/internal/temporalcli/commands.schedule.go @@ -628,8 +628,12 @@ func (c *TemporalScheduleListMatchingTimesCommand) run(cctx *CommandContext, arg cctx.Printer.StartList() defer cctx.Printer.EndList() + type matchingTime struct { + Time string `json:"time"` + } + for _, t := range res.StartTime { - cctx.Printer.Printlnf("%v", t.AsTime()) + cctx.Printer.PrintStructured(matchingTime{Time: t.AsTime().String()}, printer.StructuredOptions{}) } return nil diff --git a/internal/temporalcli/commands.schedule_test.go b/internal/temporalcli/commands.schedule_test.go index d828e55ea..468faf991 100644 --- a/internal/temporalcli/commands.schedule_test.go +++ b/internal/temporalcli/commands.schedule_test.go @@ -553,3 +553,44 @@ func (s *SharedServerSuite) TestSchedule_Memo_Update() { return j.Schedule.Action.StartWorkflow.Memo.Fields.Bar.Data == "Mg==" }, 10*time.Second, 100*time.Millisecond) } + +func (s *SharedServerSuite) TestSchedule_ListMatchingTimes() { + schedId, _, res := s.createSchedule("--interval", "1h") + s.NoError(res.Err) + + now := time.Now().UTC() + startTime := now.Format(time.RFC3339) + endTime := now.Add(5 * time.Hour).Format(time.RFC3339) + + // text output + res = s.Execute( + "schedule", "list-matching-times", + "--address", s.Address(), + "-s", schedId, + "--start-time", startTime, + "--end-time", endTime, + ) + s.NoError(res.Err) + // should have timestamps in output + s.Contains(res.Stdout.String(), "UTC") + + // json output + res = s.Execute( + "schedule", "list-matching-times", + "--address", s.Address(), + "-s", schedId, + "--start-time", startTime, + "--end-time", endTime, + "-o", "json", + ) + s.NoError(res.Err) + // should parse as JSON array + //var times []string + //s.NoError(json.Unmarshal(res.Stdout.Bytes(), ×)) + + var times []struct { + Time string `json:"time"` + } + s.NoError(json.Unmarshal(res.Stdout.Bytes(), ×)) + s.NotEmpty(times) +} From 385689c8dcb49dd27f7beba3e1c13f443f95bce6 Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Sat, 30 May 2026 05:49:46 -0500 Subject: [PATCH 4/4] fix: improve list-matching-times description, output format, and tests Update command description to reference schedule spec and actions instead of Workflow Executions, and use "(Experimental feature)" to match CLI conventions. Fix JSON output to pass through raw RPC response and text output to use table format with RFC3339 timestamps. Simplify tests with deterministic calendar spec. --- internal/temporalcli/commands.gen.go | 6 +-- internal/temporalcli/commands.schedule.go | 14 ++--- .../temporalcli/commands.schedule_test.go | 52 ++++++++++++------- internal/temporalcli/commands.yaml | 11 ++-- 4 files changed, 49 insertions(+), 34 deletions(-) diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index edc16796a..747d971ec 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -2284,11 +2284,11 @@ func NewTemporalScheduleListMatchingTimesCommand(cctx *CommandContext, parent *T s.Parent = parent s.Command.DisableFlagsInUseLine = true s.Command.Use = "list-matching-times [flags]" - s.Command.Short = "List matching times for a Schedule (Experimental)" + s.Command.Short = "List matching times for a Schedule (Experimental feature)" if hasHighlighting { - s.Command.Long = "\nNote: This is an experimental feature and may change in the future.\n\nList the times a Schedule would fire within a given time range.\nUse this command to preview when a Schedule will trigger Workflow\nExecutions without actually running them.\n\nFor example:\n\n\x1b[1m temporal schedule list-matching-times \\\n --schedule-id \"YourScheduleId\" \\\n --start-time \"2024-01-01T00:00:00Z\" \\\n --end-time \"2024-01-31T23:59:59Z\"\x1b[0m" + s.Command.Long = "\nNote: This is an experimental feature and may change in the future.\n\nList the times a Schedule's spec would match within a given time\nrange. The time range may be in the past or future. Use this\ncommand to preview when a Schedule will take actions without\nactually running them.\n\nFor example:\n\n\x1b[1m temporal schedule list-matching-times \\\n --schedule-id \"YourScheduleId\" \\\n --start-time \"2024-01-01T00:00:00Z\" \\\n --end-time \"2024-01-31T23:59:59Z\"\x1b[0m" } else { - s.Command.Long = "\nNote: This is an experimental feature and may change in the future.\n\nList the times a Schedule would fire within a given time range.\nUse this command to preview when a Schedule will trigger Workflow\nExecutions without actually running them.\n\nFor example:\n\n```\n temporal schedule list-matching-times \\\n --schedule-id \"YourScheduleId\" \\\n --start-time \"2024-01-01T00:00:00Z\" \\\n --end-time \"2024-01-31T23:59:59Z\"\n```" + s.Command.Long = "\nNote: This is an experimental feature and may change in the future.\n\nList the times a Schedule's spec would match within a given time\nrange. The time range may be in the past or future. Use this\ncommand to preview when a Schedule will take actions without\nactually running them.\n\nFor example:\n\n```\n temporal schedule list-matching-times \\\n --schedule-id \"YourScheduleId\" \\\n --start-time \"2024-01-01T00:00:00Z\" \\\n --end-time \"2024-01-31T23:59:59Z\"\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().Var(&s.StartTime, "start-time", "Start of time range to list matching times. Required.") diff --git a/internal/temporalcli/commands.schedule.go b/internal/temporalcli/commands.schedule.go index 2a1675eb1..3a18271bd 100644 --- a/internal/temporalcli/commands.schedule.go +++ b/internal/temporalcli/commands.schedule.go @@ -625,16 +625,18 @@ func (c *TemporalScheduleListMatchingTimesCommand) run(cctx *CommandContext, arg return err } - cctx.Printer.StartList() - defer cctx.Printer.EndList() + if cctx.JSONOutput { + return cctx.Printer.PrintStructured(res, printer.StructuredOptions{}) + } type matchingTime struct { Time string `json:"time"` } - + var rows []matchingTime for _, t := range res.StartTime { - cctx.Printer.PrintStructured(matchingTime{Time: t.AsTime().String()}, printer.StructuredOptions{}) + rows = append(rows, matchingTime{Time: t.AsTime().Format(time.RFC3339)}) } - - return nil + return cctx.Printer.PrintStructured(rows, printer.StructuredOptions{ + Table: &printer.TableOptions{}, + }) } diff --git a/internal/temporalcli/commands.schedule_test.go b/internal/temporalcli/commands.schedule_test.go index 468faf991..d4d089bf8 100644 --- a/internal/temporalcli/commands.schedule_test.go +++ b/internal/temporalcli/commands.schedule_test.go @@ -9,6 +9,7 @@ import ( "io" "math/rand" "regexp" + "strings" "time" "github.com/stretchr/testify/assert" @@ -555,42 +556,53 @@ func (s *SharedServerSuite) TestSchedule_Memo_Update() { } func (s *SharedServerSuite) TestSchedule_ListMatchingTimes() { - schedId, _, res := s.createSchedule("--interval", "1h") + // use a calendar spec with known hours so results are deterministic + schedId, _, res := s.createSchedule("--calendar", `{"hour":"3,6,9"}`) s.NoError(res.Err) - now := time.Now().UTC() - startTime := now.Format(time.RFC3339) - endTime := now.Add(5 * time.Hour).Format(time.RFC3339) - - // text output + // query a full day - should match exactly 3 times res = s.Execute( "schedule", "list-matching-times", "--address", s.Address(), "-s", schedId, - "--start-time", startTime, - "--end-time", endTime, + "--start-time", "2025-01-01T00:00:00Z", + "--end-time", "2025-01-01T23:59:59Z", ) s.NoError(res.Err) - // should have timestamps in output - s.Contains(res.Stdout.String(), "UTC") + // text output should contain parseable RFC3339 timestamps + for _, line := range strings.Split(res.Stdout.String(), "\n") { + line = strings.TrimSpace(line) + if line == "" || line == "Time" { + continue + } + _, err := time.Parse(time.RFC3339, line) + s.NoError(err, "should parse text line as time: %q", line) + } // json output res = s.Execute( "schedule", "list-matching-times", "--address", s.Address(), "-s", schedId, - "--start-time", startTime, - "--end-time", endTime, + "--start-time", "2025-01-01T00:00:00Z", + "--end-time", "2025-01-01T23:59:59Z", "-o", "json", ) s.NoError(res.Err) - // should parse as JSON array - //var times []string - //s.NoError(json.Unmarshal(res.Stdout.Bytes(), ×)) - - var times []struct { - Time string `json:"time"` + var resp struct { + StartTime []string `json:"startTime"` } - s.NoError(json.Unmarshal(res.Stdout.Bytes(), ×)) - s.NotEmpty(times) + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &resp)) + s.Equal(3, len(resp.StartTime)) +} + +func (s *SharedServerSuite) TestSchedule_ListMatchingTimes_NotFound() { + res := s.Execute( + "schedule", "list-matching-times", + "--address", s.Address(), + "-s", "nonexistent-schedule-id", + "--start-time", "2025-01-01T00:00:00Z", + "--end-time", "2025-01-01T23:59:59Z", + ) + s.Error(res.Err) } diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index 6f9304329..a94ffee24 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -2321,14 +2321,15 @@ commands: - schedule-id - name: temporal schedule list-matching-times - summary: List matching times for a Schedule (Experimental) + summary: List matching times for a Schedule (Experimental feature) description: | - + Note: This is an experimental feature and may change in the future. - List the times a Schedule would fire within a given time range. - Use this command to preview when a Schedule will trigger Workflow - Executions without actually running them. + List the times a Schedule's spec would match within a given time + range. The time range may be in the past or future. Use this + command to preview when a Schedule will take actions without + actually running them. For example: