From 91d50129af9b2b3222b3ee2b2cf4a14b823b08e2 Mon Sep 17 00:00:00 2001 From: Akanksha Trehun Date: Wed, 9 Sep 2026 21:50:21 +0530 Subject: [PATCH] Expose a replay-safe new_guid on WorkflowContext The underlying orchestration context already generates deterministic GUIDs internally (used by the worker itself for task execution ids), but nothing on the public WorkflowContext exposed it to workflow authors, so anyone needing a stable id inside a workflow had to reach for uuid4 and break replay determinism. Adds new_guid as an abstract method on WorkflowContext and implements it on DaprWorkflowContext by delegating to the wrapped context, matching the .NET SDK's NewGuid. Signed-off-by: Akanksha Trehun --- dapr/ext/workflow/dapr_workflow_context.py | 4 ++++ dapr/ext/workflow/workflow_context.py | 17 +++++++++++++++++ .../ext/workflow/test_dapr_workflow_context.py | 7 +++++++ 3 files changed, 28 insertions(+) diff --git a/dapr/ext/workflow/dapr_workflow_context.py b/dapr/ext/workflow/dapr_workflow_context.py index 7ceee0cbf..3cde70d55 100644 --- a/dapr/ext/workflow/dapr_workflow_context.py +++ b/dapr/ext/workflow/dapr_workflow_context.py @@ -15,6 +15,7 @@ from datetime import datetime, timedelta from typing import Any, Callable, List, Optional, TypeVar, Union +from uuid import UUID from dapr.ext.workflow._durabletask import task from dapr.ext.workflow.logger import Logger, LoggerOptions @@ -57,6 +58,9 @@ def set_custom_status(self, custom_status: str) -> None: self._logger.debug(f'{self.instance_id}: Setting custom status to {custom_status}') self.__obj.set_custom_status(custom_status) + def new_guid(self) -> UUID: + return self.__obj.new_guid() + def create_timer(self, fire_at: Union[datetime, timedelta]) -> task.Task: self._logger.debug(f'{self.instance_id}: Creating timer to fire at {fire_at} time') return self.__obj.create_timer(fire_at) diff --git a/dapr/ext/workflow/workflow_context.py b/dapr/ext/workflow/workflow_context.py index e3e98fe9e..956bac326 100644 --- a/dapr/ext/workflow/workflow_context.py +++ b/dapr/ext/workflow/workflow_context.py @@ -18,6 +18,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timedelta from typing import Any, Callable, Generator, Optional, TypeVar, Union +from uuid import UUID from dapr.ext.workflow._durabletask import task from dapr.ext.workflow.propagation import PropagatedHistory, PropagationScope @@ -90,6 +91,22 @@ def set_custom_status(self, custom_status: str) -> None: """Set the custom status.""" pass + @abstractmethod + def new_guid(self) -> UUID: + """Create a new GUID that is safe for replay within a workflow. + + The GUID is deterministically derived from the workflow instance ID, + the current replay-safe time, and a counter that increments on each + call, so calling this repeatedly returns different values within a + single execution while remaining stable across replays. + + Returns + ------- + uuid.UUID + A replay-safe, deterministically generated GUID. + """ + pass + @abstractmethod def create_timer(self, fire_at: Union[datetime, timedelta]) -> task.Task: """Create a Timer Task to fire after at the specified deadline. diff --git a/tests/ext/workflow/test_dapr_workflow_context.py b/tests/ext/workflow/test_dapr_workflow_context.py index 22350dda8..7236a4588 100644 --- a/tests/ext/workflow/test_dapr_workflow_context.py +++ b/tests/ext/workflow/test_dapr_workflow_context.py @@ -16,6 +16,7 @@ import unittest from datetime import datetime from unittest import mock +from uuid import UUID import dapr.ext.workflow._durabletask.internal.protos as pb from dapr.ext.workflow import PropagatedHistory @@ -54,6 +55,9 @@ def set_custom_status(self, custom_status): def get_propagated_history(self): return self._propagated_history + def new_guid(self): + return UUID('12345678-1234-5678-1234-567812345678') + class DaprWorkflowContextTest(unittest.TestCase): def mock_client_activity(ctx: WorkflowActivityContext, input): @@ -83,6 +87,9 @@ def test_workflow_context_functions(self): dapr_wf_ctx.set_custom_status(mock_custom_status) assert fakeContext.custom_status == mock_custom_status + new_guid_result = dapr_wf_ctx.new_guid() + assert new_guid_result == UUID('12345678-1234-5678-1234-567812345678') + def test_get_propagated_history_proxies_inner_context(self): with mock.patch( 'dapr.ext.workflow._durabletask.worker._RuntimeOrchestrationContext',