Skip to content

Standalone activity - #1138

Closed
dandavison wants to merge 8 commits into
mainfrom
standalone-activity
Closed

Standalone activity#1138
dandavison wants to merge 8 commits into
mainfrom
standalone-activity

Conversation

@dandavison

@dandavisondandavison commented Oct 1, 2025

Copy link
Copy Markdown
Contributor

To test:

In the server repo (using branch main)

make start
temporal operator namespace create -n default

Then in sdk-python, checkout this branch and

git submodule update --init --recursive
uv run poe build-develop
uv run pytest -E localhost:7233 tests/test_activity.py

See


Note

Introduces standalone activity support across SDK types and API surfaces.

  • Updates activity.Info: adds namespace, makes workflow_* fields optional with in_workflow helper, adds activity_run_id, and switches metrics/logging to use namespace
  • Adds ActivityDefinition.get_name_and_result_type utility for resolving activity name/result type from callables
  • Extends API: new ActivityExecutionOutcome, ActivityExecutionInfo, and ActivityExecutionListInfo messages; exports new activity enums (ActivityExecutionStatus, ID reuse/conflict policies)
  • Adds error detail ActivityExecutionAlreadyStartedFailure
  • Exposes WorkflowService requests/responses for standalone activities: start, describe, list/count, poll, request cancel, terminate, delete, and related operations

Written by Cursor Bugbot for commit 904c04f. This will update automatically on new commits. Configure here.

Comment threadtemporalio/client.py Outdated
self._id_or_token = ActivityIDReference(activity_id=id, run_id=run_id)
self.run_id = run_id

# TODO: do we support something like `follow_runs: bool`?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need there's no concept of an execution chain for activities.

Comment threadtemporalio/client.py Outdated
handle = await self.start_activity(*args, **kwargs)
return await handle.result()

async def list_activities(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would be interesting to see how we can model this to return both workflow and standalone "client" activities.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I've made the iterator yield Union[ActivityExecution, WorkflowActivityExecution]. Those two dataclasses share a few fields.

Comment threadtemporalio/client.py

# - TODO: Overloads for no-param, single-param, multi-param
# - TODO: Support sync and async activity functions
async def start_activity(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also need execute_activity but we can leave that for later.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Actually execute_activity is already present below.

Comment threadtemporalio/client.py Outdated


@dataclass(frozen=True)
class AsyncActivityIDReference:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider deprecating and renaming to WorkflowActivityIDReference.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Or merging the two reference types where workflow_id becomes an optional field.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I've merged them into ActivityIDReference and retained the AsyncActivityIDReference name as an alias.

Comment threadtemporalio/client.py Outdated
"""Handle representing an activity started by a workflow."""

def __init__(
self, client: Client, id_or_token: Union[AsyncActivityIDReference, bytes]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should work with any activity IMHO.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Agreed. The PR:

  • Makes get_async_activity_handle work for Standalone Activities and Workflow Activities. This is essentially a client appropriate for the "owner" of the activity, permitting manual completion/fail/cancellation/heartbeating
  • Introduces Client.get_activity_handle for SA only. This is a cliet appropriate for the caller of the activity: describe, poll, request cancellation, etc

Comment threadtemporalio/client.py Outdated
self._id_or_token = id_or_token


WorkflowActivityHandle = AsyncActivityHandle

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will also need pause, reset, etc...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we still want AsyncActivityHandle because you can obtain one with a token and can't do anything else with the token but issue completion requests.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I've added implementations of pause / unpause / reset to the new ActivityHandle for SAs.

The existing AsyncActivityHandle is unperturbed by this PR: it just gains a constructor for SAs.

Comment threadtemporalio/client.py Outdated
"""
raise NotImplementedError

# TODO:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also TODO: all of the async completion methods.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

You mean heartbeat, complete, fail, and report_cancellation, right? Those are all inherited from _BaseActivityHandle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm on the fence whether we want to expose these methods on the activity handle as opposed to having the async completion handle as a separate concept. The use cases are different for the two.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes, I also changed direction here: they are now on AsyncActivityHandle, for SA as they are for WA.

Comment threadtemporalio/client.py Outdated
)


# TODO: This name is suboptimal now. We could deprecate it and introduce WorkflowActivityHandle as a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, let's do that. Which means that you would have to accept a WorkflowActivityHandle where you accept AsyncActivityHandle.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I've renamed, and am retaining the old name as an alias to the same class object.

Comment threadtemporalio/client.py Outdated

class AsyncActivityHandle:
"""Handle representing an external activity for completion and heartbeat."""
class _BaseActivityHandle:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would consider just duplicating instead of inheriting but don't have a strong opinion.

@dandavison
dandavisonforce-pushed the standalone-activity branch 3 times, most recently from e53a5c2 to 0a21bf7CompareOctober 8, 2025 21:47
@dandavisondandavison changed the title Standalone activity API sketchesStandalone activity prototypeOct 8, 2025
@dandavison
dandavisonforce-pushed the standalone-activity branch 6 times, most recently from ace675c to 741603fCompareOctober 10, 2025 09:47
)

@classmethod
def get_name_and_result_type(

@cretzcretzOct 13, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we're extracting this common logic out of workflow_instance.py, can we update workflow_instance.py use this too? Also, then can we get rid of must_from_callable and inline it into this method since it won't be called anywhere anymore?

Comment threadtemporalio/client.py Outdated
# - TODO: Support sync and async activity functions
async def start_activity(
self,
activity: Union[str, Callable[..., Awaitable[ReturnType]]],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think when we get to the overloads, this final form may not make sense to use the generic

Comment threadtemporalio/common.py Outdated
)


class IdReusePolicy(IntEnum):

@cretzcretzOct 13, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think each top-level thing should have its own enumerate here. It doesn't make sense to have workflow ID reuse policy and not activity ID reuse policy. Same for ID conflict policy. It makes more sense from a user POV not to pretend like this is a common ID reuse policy when it is not (nor do we need it to be).

We should not eschew consistency just because we may have a NexusOperationIdReusePolicy one day (and we'll be happy we kept them separate if they diverge). This is no different than cancellation type or any of these others.

Comment threadtemporalio/client.py Outdated
retry_policy: Optional[temporalio.common.RetryPolicy] = None,
search_attributes: Optional[
Union[
temporalio.common.SearchAttributes,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IMO we don't need to accept this deprecated form of search attributes for newer API, but it's not harmful

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks, I meant to remove that. Removed.

Comment threadtemporalio/client.py Outdated

def list_activities(
self,
query: Optional[str] = None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you confirm whether we expect a query specifically saying activity kind is "standalone" at this time so that when we add non-standalone one day it doesn't surprise users?

Comment threadtemporalio/common.py
TIMED_OUT = 6 # ACTIVITY_EXECUTION_STATUS_TIMED_OUT


class PendingActivityState(IntEnum):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this also may make sense in the client module

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should go ahead and put here the expected changes to activity runtime. Specifically I assume all workflow_-prefixed fields of Info will become optional. I would also recommend either a "kind" enumerate for activities, or add an is_standalone akin to is_local so users can know it's not the traditional activity.

Comment threadtemporalio/converter.py Outdated
namespace: str
"""Namespace."""

workflow_id: Optional[str]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What situations would workflow ID be optional here?

Comment threadtemporalio/exceptions.py Outdated
Comment on lines +250 to +253
# TODO: This error class has required history event fields. I propose we retain it as
# workflow-specific and introduce client.ActivityFailureError for an error in a standalone activity.
# We could deprecate this name and introduce WorkflowActivityError as a preferred-going-forwards
# alias.

@cretzcretzOct 13, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree with behavior, but disagree with the aliasing. This specifically maps to ActivityFailureInfo in our API, I think we should keep that naming correlation (same for all failure errors). I expect similar for standalone Nexus operation failures (they don't use the named-in-proto failure messages, so they don't affect failure error things).

Comment threadtemporalio/converter.py Outdated
Comment on lines +137 to +138
activity_id: Optional[str]
"""Activity ID. Optional if this is an activity started from a workflow."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In what situations is this optional? To confirm, is this not the activity ID for regular activities? We have to make sure that every value that is present on the deserialization side is present on the serialization side. So a workflow should always set this (right now it's defaulted in the constructor of the activity handle, but we can move it out if needed).

@maciejdudko

maciejdudko commented Oct 17, 2025

Copy link
Copy Markdown
Contributor

I see that activity.Info was left unchanged.

  • How should an activity check if it's running standalone (e.g. before conditionally reading workflow ID)?
  • How does a standalone activity checks its own standalone-specific options? For now that would be only id_reuse_policy and id_conflict_policy as far as I can tell.

@dandavison
dandavisonforce-pushed the standalone-activity branch 2 times, most recently from 3eccdd3 to bdb2557CompareDecember 19, 2025 19:54
dandavisonand others added 3 commits January 6, 2026 18:36
Implement list/count activities
alphabetical order
ActivityHandle.result
Implement start_activity
Update sdk-core
Server isn't returning RunID yet
Add activity IDs to test since they are now set in context
Eliminate base serialization context class
Extend tests
Don't hardcode workflow IDs
Fix AsyncActivityHandle
TEMP: Alter manual activity tests to not require worker
Skip tests globally
Revert "TEMP: Alter manual activity tests to not require worker"
This reverts commit 7127c0f.
Assert describe returns new state after transition
Fix cancellation and failure tests
Cleanup
Add experimental notices
Workaround mypy bug
Skip Standalone Activity tests in CI
Only support TypedSearchAttributes
This commit updates the Python SDK standalone activity implementation to match
Maciej's cross-language design specification.
- ActivityIdReusePolicy → ActivityIDReusePolicy
- ActivityIdConflictPolicy → ActivityIDConflictPolicy
- Renamed property 'id' → 'activity_id'
- Renamed property 'run_id' → 'activity_run_id'
- Updated internal _id and _run_id variables accordingly
- Renamed 'run_id' → 'activity_run_id'
- Renamed 'schedule_time' → 'scheduled_time'
- Added 'namespace' field (copied from calling client)
- Removed 'state_transition_count' field
- Removed the separate class; all list results now return ActivityExecution
- Renamed 'run_id' → 'activity_run_id'
- Renamed 'schedule_time' → 'scheduled_time'
- Added 'namespace', 'close_time', 'execution_duration' fields
- Removed 'state_transition_count' field
- Added ActivityExecutionCountAggregationGroup class
- Added 'groups' field for aggregation support
- Renamed 'ret_type' → 'result_type'
- Renamed 'static_summary' → 'summary'
- Renamed 'static_details' → 'details'
- CancelActivityInput.run_id → activity_run_id
- TerminateActivityInput.run_id → activity_run_id
- DescribeActivityInput.run_id → activity_run_id
- Added 'wait_for_cancel_completed: bool' field
- Renamed parameter 'run_id' → 'activity_run_id'
- Added 'namespace: str' field
- Added 'activity_run_id: str | None' field (None for workflow activities)
- Added 'in_workflow' property
- Made workflow_id, workflow_namespace, workflow_run_id, workflow_type optional
(None for standalone activities)
- Deprecated 'workflow_namespace' in favor of 'namespace'
- Updated tests/test_activity.py and tests/worker/test_activity.py
- Fixed temporalio/contrib/opentelemetry.py
- Fixed temporalio/contrib/openai_agents/_mcp.py
- GetActivityResultInput and interceptor method - would require significant
refactoring of the result caching mechanism in ActivityHandle
@dandavison
dandavisonforce-pushed the standalone-activity branch 2 times, most recently from 28315b7 to 5b0294aCompareJanuary 12, 2026 19:30
@dandavison
dandavison marked this pull request as ready for review January 12, 2026 19:31
@dandavison
dandavison requested a review from a team as a code ownerJanuary 12, 2026 19:31
@dandavisondandavison changed the title Standalone activity prototypeStandalone activityJan 12, 2026
@dandavison
dandavison marked this pull request as draft January 12, 2026 19:46
…tivity
# Conflicts:
#	temporalio/api/activity/v1/message_pb2.py
#	temporalio/api/activity/v1/message_pb2.pyi
#	temporalio/api/workflowservice/v1/__init__.py
#	temporalio/api/workflowservice/v1/request_response_pb2.py
#	temporalio/api/workflowservice/v1/request_response_pb2.pyi
#	temporalio/api/workflowservice/v1/service_pb2.py
#	temporalio/api/workflowservice/v1/service_pb2_grpc.py
#	temporalio/api/workflowservice/v1/service_pb2_grpc.pyi
#	temporalio/bridge/sdk-core
#	temporalio/bridge/services_generated.py
#	temporalio/bridge/src/client_rpc_generated.rs
@cretz

Copy link
Copy Markdown
Contributor

Closing this PR in favor of a newer PR w/out the draft comments (may take a bit to get the new PR ready, so no link right away, but closing this one so mutations to the branch don't keep updating it)

@cretzcretz closed this Jan 14, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@dandavison@maciejdudko@cretz@bergundy