From 1644936e46c444ba9c7a5606cec2b5659599526e Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:56:03 -0500 Subject: [PATCH] Defer schedule catchup window default to server --- CHANGELOG.md | 3 +++ temporalio/client/_schedule.py | 24 +++++++++++++++--------- tests/test_client.py | 12 ++++++++++++ 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a1a92a95..ebd983851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ to include examples, links to docs, or any other relevant information. ### Fixed +- Schedule policies now omit an unspecified catch-up window so the Temporal Server applies its + default (currently one year). + ### Security ## [1.30.0] - 2026-07-01 diff --git a/temporalio/client/_schedule.py b/temporalio/client/_schedule.py index 15946cfe8..9d715c46e 100644 --- a/temporalio/client/_schedule.py +++ b/temporalio/client/_schedule.py @@ -956,9 +956,13 @@ class SchedulePolicy: """Controls what happens when an action is started while another is still running.""" - catchup_window: timedelta = timedelta(days=365) + catchup_window: timedelta | None = None """After a Temporal server is unavailable, amount of time in the past to - execute missed actions.""" + execute missed actions. + + If unset, this is omitted so the Temporal Server applies its default + (currently one year). + """ pause_on_failure: bool = False """Whether to pause the schedule if an action fails or times out. @@ -971,20 +975,24 @@ class SchedulePolicy: def _from_proto(pol: temporalio.api.schedule.v1.SchedulePolicies) -> SchedulePolicy: return SchedulePolicy( overlap=ScheduleOverlapPolicy(int(pol.overlap_policy)), - catchup_window=pol.catchup_window.ToTimedelta(), + catchup_window=( + pol.catchup_window.ToTimedelta() + if pol.HasField("catchup_window") + else None + ), pause_on_failure=pol.pause_on_failure, ) def _to_proto(self) -> temporalio.api.schedule.v1.SchedulePolicies: - catchup_window = google.protobuf.duration_pb2.Duration() - catchup_window.FromTimedelta(self.catchup_window) - return temporalio.api.schedule.v1.SchedulePolicies( + ret = temporalio.api.schedule.v1.SchedulePolicies( overlap_policy=temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( self.overlap ), - catchup_window=catchup_window, pause_on_failure=self.pause_on_failure, ) + if self.catchup_window is not None: + ret.catchup_window.FromTimedelta(self.catchup_window) + return ret @dataclass @@ -1060,8 +1068,6 @@ def _from_proto(sched: temporalio.api.schedule.v1.Schedule) -> Schedule: ) async def _to_proto(self, client: Client) -> temporalio.api.schedule.v1.Schedule: - catchup_window = google.protobuf.duration_pb2.Duration() - catchup_window.FromTimedelta(self.policy.catchup_window) return temporalio.api.schedule.v1.Schedule( spec=self.spec._to_proto(), action=await self.action._to_proto(client), diff --git a/tests/test_client.py b/tests/test_client.py index d611eda3a..d5d984152 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -837,6 +837,15 @@ def test_history_from_json(): ) +def test_schedule_policy_default_catchup_window_is_omitted(): + policy = SchedulePolicy() + assert policy.catchup_window is None + assert not policy._to_proto().HasField("catchup_window") + + policy.catchup_window = timedelta(minutes=5) + assert policy._to_proto().catchup_window.ToTimedelta() == timedelta(minutes=5) + + async def test_schedule_basics( client: Client, worker: ExternalWorker, env: WorkflowEnvironment ): @@ -987,12 +996,15 @@ def update_fail(_input: ScheduleUpdateInput) -> ScheduleUpdate: state=ScheduleState(paused=True), ) assert isinstance(new_schedule.action, ScheduleActionStartWorkflow) + assert new_schedule.policy.catchup_window is None async def update_schedule_basic(_input: ScheduleUpdateInput) -> ScheduleUpdate: return ScheduleUpdate(new_schedule) await handle.update(update_schedule_basic) desc = await handle.describe() + assert desc.schedule.policy.catchup_window == timedelta(days=365) + new_schedule.policy.catchup_window = timedelta(days=365) new_schedule.action.args = await DataConverter.default.encode( new_schedule.action.args )