Skip to content

💥 feat(sdk): Support Standalone Activities - #1402

Open
maciejdudko wants to merge 1 commit into
temporalio:mainfrom
maciejdudko:standalone-activities
Open

💥 feat(sdk): Support Standalone Activities#1402
maciejdudko wants to merge 1 commit into
temporalio:mainfrom
maciejdudko:standalone-activities

Conversation

@maciejdudko

@maciejdudko maciejdudko commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

💥 Breaking changes

  • ActivityInfo fields related to the containing workflow are made optional.
  • async_activity_handle::ActivityIdentifier::ById is split into ByIdWorkflow and ByIdStandalone.

What was changed

  • Added support for executing Standalone Activities in SDK Worker.
  • Added high-level client interfaces for starting and interacting with Standalone Activities.

Why?

Feature request: temporalio/features#706

Checklist

  1. Closes [Feature Request] Support standalone activities #1070

  2. How was this tested:

Added tests in crates/sdk-core/tests/integ_tests/standalone_activity_tests.rs

  1. Any docs updates needed?

SAA guide for Rust needs to be added here: https://docs.temporal.io/standalone-activity

@maciejdudko
maciejdudko force-pushed the standalone-activities branch 9 times, most recently from 49d89b7 to dd726c2 Compare July 22, 2026 17:55
@maciejdudko
maciejdudko force-pushed the standalone-activities branch from dd726c2 to bbcf171 Compare July 23, 2026 16:02
@maciejdudko maciejdudko changed the title feat(sdk): support Standalone Activities 💥 feat(sdk): Support Standalone Activities Jul 23, 2026
@maciejdudko
maciejdudko marked this pull request as ready for review July 23, 2026 16:10
@maciejdudko
maciejdudko requested a review from a team as a code owner July 23, 2026 16:10

@Sushisource Sushisource left a comment

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.

Awesome to see this ready!

Comment on lines +124 to +126
/// To support deserialization of transmitted payloads, the object internally stores a reference
/// to the client used to make the request. For this reason, this type is parametrized with client
/// type. The client reference can be dropped by calling [`simple`](Self::untyped).

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 comment looks like it's not accurate any more since just a dataconverter, not a whole client, is stored (which is good).

Comment on lines +227 to +232
/// True if activity input is present.
/// See [`ActivityDescribeOptions::include_input`](crate::ActivityDescribeOptions::include_input).
/// Use [`input`](Self::input) or [`raw_input`](Self::raw_input) to retrieve it.
pub fn has_input(&self) -> bool {
self.raw_input.is_some()
}

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.

Nit: IMO we don't need this - easy enough to just call raw_input().is_some()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd keep these methods for symmetry with heartbeat_details/last_failure, also I don't like making users call raw_* methods when they're not interested in raw payloads.

/// True if activity outcome is present.
/// See [`ActivityDescribeOptions::include_outcome`](crate::ActivityDescribeOptions::include_outcome).
/// Use [`outcome`](Self::outcome) or [`raw_outcome`](Self::outcome) to retrieve it.
pub fn has_outcome(&self) -> 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.

Same here

pub enum ActivityResultError {
/// Activity execution did not complete successfully.
#[error("Activity failed: {0}")]
ActivityFailed(#[source] IncomingError),

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.

With WorkflowGetResultError we distinguish Failed / timedout / cancelled etc at the top level, probably makes sense to do that here too.

.await?
.into_inner();

// If resp.outcome.value is None, poll again

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.

Will this hot-loop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The server uses long-poll for this request.

Comment thread crates/client/src/lib.rs
/// Start a standalone activity and wait for its result.
///
/// Equivalent to `start_activity(...).await?.result()`.
pub async fn execute_activity<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.

We don't have execute_workflow, personally not a big fan of these simple sugars.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Going to remove it for now since it can always be added later.

Comment thread crates/client/src/lib.rs
///
/// To get a handle that can be used to complete an activity asynchronously,
/// see [`get_async_activity_handle`](Self::get_async_activity_handle).
pub fn get_untyped_activity_handle(

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.

Same here, I think just calling get_activity_handle<UntypedActivity>(...) is fine and we can say how to do that in the docstring.

@maciejdudko maciejdudko Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

get_activity_handle requires the activity as argument. get_untyped_activity_handle does not. get_activity_handle::<UntypedActivity> would not work.

@Sushisource Sushisource Jul 28, 2026

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.

Ah, yeah, I just wrote it wrong though - you can still just pass UntypedActivity as the arg.

...though I guess that does require you to pointlessly specify a name, so, that being the case I'm fine with this then

/// Activity options. Specifying at least one of them is required, but specifying both is also
/// allowed. Note that this type does not cover all available timeout options for an Activity.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActivityCloseTimeoutOptions {

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.

IMO no reason to add the "options" postfix here

pub id: String,
/// Combined schedule-to-close and start-to-close timeout options. At least one of them is
/// required.
#[builder(setters(name = combined_close_timeouts, vis = ""))]

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 is part of the start_fn when starting from a workflow, and it should be here too IMO, that simplifies a lot of other stuff.

The "combined" wording in the docstring / setter (which can go away if we make it part of constructor) diverges from the workflow version too, I think we can keep it the same.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This setter is private. The idea is that the user does not use combined_close_timeouts or ActivityCloseTimeoutOptions directly - instead, they use either schedule_to_close_timeout or start_to_close_timeout to set one of them, or close_timeouts to set both.

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.

Ah. I didn't see it was private. Regardless... I still think it makes sense to just do exactly what the workflow version does. Passing the right variant to close_timeouts is easy

@@ -0,0 +1,15 @@
use std::error::Error;

pub(crate) fn try_into_or_box_err<A, B, E, MapErr>(

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.

Since it's only used from client lib, can probably just stick it in there as private.

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.

[Feature Request] Support standalone activities

2 participants