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 3bf0dd8ccaa56514857bf60df09804ebc9c03f88 Mon Sep 17 00:00:00 2001 From: Nasit Sarwar Sony Date: Sat, 23 May 2026 09:59:19 -0700 Subject: [PATCH 3/4] Fix typed search attributes not printing in text mode for schedules Convert SearchAttributes using CustomJSONMarshalOptions before assigning to printableSchedule, ensuring typed fields serialize correctly through standard json.Marshal. Fixes #590 --- internal/temporalcli/commands.schedule.go | 28 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/internal/temporalcli/commands.schedule.go b/internal/temporalcli/commands.schedule.go index 7da40e779..354d0b6af 100644 --- a/internal/temporalcli/commands.schedule.go +++ b/internal/temporalcli/commands.schedule.go @@ -1,6 +1,7 @@ package temporalcli import ( + "encoding/json" "errors" "fmt" "regexp" @@ -16,6 +17,7 @@ import ( commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" schedpb "go.temporal.io/api/schedule/v1" + "go.temporal.io/api/temporalproto" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" ) @@ -48,8 +50,8 @@ type printableSchedule struct { LastUpdateAt time.Time `cli:",cardOmitEmpty"` // describe only ActionCounts *actionCounts `cli:",cardOmitEmpty"` // describe only // SearchAttributes, Memo - SearchAttributes *commonpb.SearchAttributes `cli:",cardOmitEmpty"` - Memo *commonpb.Memo `cli:",cardOmitEmpty"` + SearchAttributes map[string]interface{} `cli:",cardOmitEmpty"` + Memo *commonpb.Memo `cli:",cardOmitEmpty"` } type actionCounts struct { @@ -69,7 +71,7 @@ func describeResultToPrintable(id string, desc *client.ScheduleDescription) *pri // ID, SearchAttributes, Memo out := &printableSchedule{ ScheduleId: id, - SearchAttributes: desc.SearchAttributes, + SearchAttributes: searchAttributesToMap(desc.SearchAttributes), Memo: desc.Memo, } // Schedule.Action @@ -114,7 +116,7 @@ func listEntryToPrintable(ent *client.ScheduleListEntry) *printableSchedule { Paused: ent.Paused, Notes: ent.Note, Action: struct{ Workflow string }{Workflow: ent.WorkflowType.Name}, - SearchAttributes: ent.SearchAttributes, + SearchAttributes: searchAttributesToMap(ent.SearchAttributes), Memo: ent.Memo, } specToPrintable(out, ent.Spec) @@ -634,3 +636,21 @@ func (c *TemporalScheduleListMatchingTimesCommand) run(cctx *CommandContext, arg return nil } + +func searchAttributesToMap(sa *commonpb.SearchAttributes) map[string]interface{} { + // Step 1 — handle nil + if sa == nil { + return nil + } + // Step 2 — marshal to JSON bytes using proto marshaler + b, err := temporalproto.CustomJSONMarshalOptions{}.Marshal(sa) + if err != nil { + return nil + } + // Step 3 — unmarshal bytes into plain map + var m map[string]interface{} + if err := json.Unmarshal(b, &m); err != nil { + return nil + } + return m +} From fb9eb91c2518227ead3dad1a32493ea7cc2d04c1 Mon Sep 17 00:00:00 2001 From: Nasit Sarwar Sony Date: Sat, 23 May 2026 11:02:22 -0700 Subject: [PATCH 4/4] Prefix dev server cluster ID with 'dev-server-' and add env var override - Prefix generated cluster IDs with 'dev-server-' for better identification - Add TEMPORAL_DEV_SERVER_CLUSTER_ID env var to allow override Fixes #609 --- internal/temporalcli/commands.server.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/temporalcli/commands.server.go b/internal/temporalcli/commands.server.go index 90c5b2fe3..4c5ad8216 100644 --- a/internal/temporalcli/commands.server.go +++ b/internal/temporalcli/commands.server.go @@ -3,6 +3,7 @@ package temporalcli import ( "encoding/json" "fmt" + "os" "strings" "github.com/google/uuid" @@ -188,10 +189,15 @@ func persistentClusterID() string { // If there is not a database file in use, we want a cluster ID to be the same // for every re-run, so we set it as an environment config in a special env // file. We do not error if we can neither read nor write the file. + + if id := os.Getenv("TEMPORAL_DEV_SERVER_CLUSTER_ID"); id != "" { + return id + } + file := defaultDeprecatedEnvConfigFile("temporalio", "version-info") if file == "" { // No file, can do nothing here - return uuid.NewString() + return "dev-server-" + uuid.NewString() } // Try to get existing first env, _ := readDeprecatedEnvConfigFile(file) @@ -199,7 +205,7 @@ func persistentClusterID() string { return id } // Create and try to write - id := uuid.NewString() + id := "dev-server-" + uuid.NewString() _ = writeDeprecatedEnvConfigFile(file, map[string]map[string]string{"default": {"cluster-id": id}}) return id }