Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions temporalio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3147,6 +3147,17 @@ class WorkflowExecutionStatus(IntEnum):
temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT
)

@property
def to_pascal_case(self) -> str:
Copy link
Member

@cretz cretz Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrmm, even though it's the case that this enumerate returned from describe/list happens to have the same values in a different format as the search attribute, not sure we want to guarantee that will always be the case. We intentionally document the explicit set of enumerate strings at https://docs.temporal.io/search-attribute#default-search-attribute without reference to this enumerate, but it's technically possible that list could change unrelated to this enumerate (even if it happens to match today even in server code). Most (all?) parts of the list filter/query don't have non-string-literal representations in SDKs at this time.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understandable, would it make more sense to have something as part of the search attributes then?

That way, you would not introduce a breaking change if you change the format and update the property/function? Just thought it seemed strange that your own types would not work as search attributes out of the box.

Copy link
Member

@cretz cretz Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple of search attributes that have pre-defined enumerates (e.g. TemporalWorkflowVersioningBehavior in addition to this one), but the actual type is truly Keyword which is a string. I think, in the absence of a type safe query builder or even a predefined set of SearchAttributeKeys for all of the built-in search attributes, we currently recommend using the string format only for keys/values for list filters based on expectations in the doc instead of hardcoded SDK-side expectations.

We can keep an issue open for if/when we do decide to provide type safe assistance to building of list filters (not just enumerates, but the predefined typed key set, TemporalWorkerDeploymentVersion parsing, conditional/query building, string escaping, etc). And we would of course do it across every SDK for parity not just Python.

"""Convert WorkflowExecutionStatus enum name to Temporal search attribute format.

Returns:
Formatted string for ExecutionStatus search attribute
"""
# Convert "TIMED_OUT" -> "TimedOut", "CONTINUED_AS_NEW" -> "ContinuedAsNew", etc.
return "".join(word.capitalize() for word in self.name.split("_"))



@dataclass
class WorkflowExecutionCount:
Expand Down
20 changes: 20 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,26 @@ async def test_query_rejected(client: Client, worker: ExternalWorker):
assert err.value.status == WorkflowExecutionStatus.COMPLETED


def test_workflow_execution_status_search_attribute_value():
"""Test that WorkflowExecutionStatus.to_pascal_case converts to PascalCase correctly."""
test_cases = [
(WorkflowExecutionStatus.RUNNING, "Running"),
(WorkflowExecutionStatus.COMPLETED, "Completed"),
(WorkflowExecutionStatus.FAILED, "Failed"),
(WorkflowExecutionStatus.CANCELED, "Canceled"),
(WorkflowExecutionStatus.TERMINATED, "Terminated"),
(WorkflowExecutionStatus.TIMED_OUT, "TimedOut"),
(WorkflowExecutionStatus.CONTINUED_AS_NEW, "ContinuedAsNew"),
]

for status, expected in test_cases:
actual = status.to_pascal_case
assert actual == expected, (
f"WorkflowExecutionStatus.{status.name}.to_pascal_case "
f"returned '{actual}', expected '{expected}'"
)


async def test_signal(client: Client, worker: ExternalWorker):
handle = await client.start_workflow(
"kitchen_sink",
Expand Down