Skip to content

Conversation

dandavison
Copy link
Contributor

@dandavison dandavison commented Oct 1, 2025

To test:

In temporal checkout the WIP server branch and

make start
temporal operator namespace create -n default

Then in sdk-python, checkout this branch and

# Remove the `pytest.skip` from `tests/test_activity.py`.
git submodule update --init --recursive
uv run poe build-develop
uv run pytest -E localhost:7233 'tests/test_activity.py::test_start_activity_and_describe_activity'
uv run pytest -E localhost:7233 'tests/test_activity.py::test_manual_completion'

See

@dandavison dandavison force-pushed the standalone-activity branch from a1d3efe to 23df3f8 Compare October 1, 2025 18:13
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
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.

handle = await self.start_activity(*args, **kwargs)
return await handle.result()

async def list_activities(
Copy link
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
Contributor Author

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.


# - TODO: Overloads for no-param, single-param, multi-param
# - TODO: Support sync and async activity functions
async def start_activity(
Copy link
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
Contributor Author

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.



@dataclass(frozen=True)
class AsyncActivityIDReference:
Copy link
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
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
Contributor Author

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.

"""Handle representing an activity started by a workflow."""

def __init__(
self, client: Client, id_or_token: Union[AsyncActivityIDReference, bytes]
Copy link
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
Contributor Author

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

self._id_or_token = id_or_token


WorkflowActivityHandle = AsyncActivityHandle
Copy link
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
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
Contributor Author

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.

"""
raise NotImplementedError

# TODO:
Copy link
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
Contributor Author

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
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
Contributor Author

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.

)


# TODO: This name is suboptimal now. We could deprecate it and introduce WorkflowActivityHandle as a
Copy link
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
Contributor Author

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.


class AsyncActivityHandle:
"""Handle representing an external activity for completion and heartbeat."""
class _BaseActivityHandle:
Copy link
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 dandavison force-pushed the standalone-activity branch 3 times, most recently from e53a5c2 to 0a21bf7 Compare October 8, 2025 21:47
@dandavison dandavison changed the title Standalone activity API sketches Standalone activity prototype Oct 8, 2025
@dandavison dandavison force-pushed the standalone-activity branch from 07c6917 to 49ae6c3 Compare October 9, 2025 15:39
@dandavison dandavison force-pushed the standalone-activity branch from 49ae6c3 to 818c417 Compare October 9, 2025 15:51
@dandavison dandavison force-pushed the standalone-activity branch 2 times, most recently from 2ca269e to c681c13 Compare October 10, 2025 08:43
)

@classmethod
def get_name_and_result_type(
Copy link
Member

@cretz cretz Oct 13, 2025

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?

# - TODO: Support sync and async activity functions
async def start_activity(
self,
activity: Union[str, Callable[..., Awaitable[ReturnType]]],
Copy link
Member

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

)


class IdReusePolicy(IntEnum):
Copy link
Member

@cretz cretz Oct 13, 2025

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.

retry_policy: Optional[temporalio.common.RetryPolicy] = None,
search_attributes: Optional[
Union[
temporalio.common.SearchAttributes,
Copy link
Member

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
Contributor Author

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.


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

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?

TIMED_OUT = 6 # ACTIVITY_EXECUTION_STATUS_TIMED_OUT


class PendingActivityState(IntEnum):
Copy link
Member

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
Member

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.

namespace: str
"""Namespace."""

workflow_id: Optional[str]
Copy link
Member

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 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.
Copy link
Member

@cretz cretz Oct 13, 2025

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 on lines +137 to +138
activity_id: Optional[str]
"""Activity ID. Optional if this is an activity started from a workflow."""
Copy link
Member

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).

Sign up for free to 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.

3 participants