Skip to content

💥 Standalone Activities for Ruby#443

Open
GregoryTravis wants to merge 38 commits into
mainfrom
gmt/ruby-standalone-activities
Open

💥 Standalone Activities for Ruby#443
GregoryTravis wants to merge 38 commits into
mainfrom
gmt/ruby-standalone-activities

Conversation

@GregoryTravis
Copy link
Copy Markdown
Contributor

Add standalone activity API (Client#start_activity + friends)

Introduces support for standalone activities — activities that execute independently of any workflow.

💥 Breaking Changes:

  • Activity::Info#workflow_id, #workflow_run_id, #workflow_type are now nullable (String?); they return nil when the activity is standalone.
  • Activity::Info#workflow_namespace is now marked @deprecated in favor of the new #namespace accessor. Both fields always carry the same value regardless of standalone-vs-workflow context; the rename matches the cross-SDK convention.

New public API surface:

  • Client#start_activity / Client#execute_activity — start a standalone activity execution by name, class, instance, or Activity::Definition::Info; execute_activity is start_activity + handle.result as a shortcut
  • Client#activity_handle(activity_id, activity_run_id:, result_hint:) — get a handle to an existing standalone activity (e.g. one started by a different process)
  • Client#list_activities(query) / Client#count_activities(query) — visibility queries over standalone activities; list_activities returns an Enumerator<ActivityExecution>
  • Client::ActivityHandle — handle returned by start_activity / activity_handle; provides #result(result_hint:, rpc_options:), #describe, #cancel(reason), #terminate(reason)
  • Client::ActivityExecution — lightweight metadata used in list_activities results
  • Client::ActivityExecution::Description — rich descriptor returned by ActivityHandle#describe; subclass of ActivityExecution. Exposes ~25 fields from the proto (status, attempt, retry_policy, last_failure, last_heartbeat_time, priority, canceled_reason, static_summary/static_details, etc.)
  • Client::ActivityExecutionCount + ::AggregationGroup — result of count_activities(), with group-by support
  • Client::ActivityExecutionStatus — enum module wrapping the proto's status values
  • ActivityIDReusePolicy + ActivityIDConflictPolicy — enum modules in lib/temporalio/common_enums.rb mirroring the workflow-side policies
  • Error::ActivityAlreadyStartedError (in error/failure.rb) — raised by start_activity when the server rejects a duplicate ID
  • Error::ActivityFailedError (in error.rb) — raised by handle.result when the activity terminates in failure; cause carries the underlying failure (ApplicationError, CanceledError, TimeoutError, TerminatedError)

New interceptor API:

  • Client::Interceptor::StartActivityInput, DescribeActivityInput, CancelActivityInput, TerminateActivityInput, ListActivitiesInput, CountActivitiesInput, FetchActivityOutcomeInput — seven new Data.define input types
  • Client::Interceptor::Outbound extended with seven matching pass-through methods (start_activity, describe_activity, cancel_activity, terminate_activity, list_activities, count_activities, fetch_activity_outcome)
  • Chain ordering convention is first-added-is-outermost.

Activity::Info additions:

  • #activity_run_id — run-scoped id assigned by the server to each standalone activity execution (nil for workflow-dispatched)
  • #namespace — canonical namespace accessor (preferred over the deprecated #workflow_namespace)
  • #in_workflow? — predicate distinguishing workflow-dispatched activities from standalone ones

Client#async_activity_handle extension (no new method):

  • Now accepts an ActivityIDReference constructed via ActivityIDReference.for_standalone(activity_id:, activity_run_id:).

ActivityIDReference additions:

  • ActivityIDReference.for_standalone(activity_id:, activity_run_id: nil) — class factory for the new standalone shape.
  • #standalone? — predicate for branching in async-completion routing

CI / dev server:

  • Enable standalone-activity server feature flags in the Temporal CLI dev server used by unit tests: frontend.activityAPIsEnabled, activity.enableStandalone, history.enableChasm, history.enableTransitionHistory (added to test/test.rb).
  • Bumped sdk-core submodule to a22517e4 (Rust crate 0.4.0) to pick up the worker-side coresdk.activity_task.Start.run_id proto field used to plumb activity_run_id into Activity::Info.

Tests:

  • test/client_activity_test.rb (28 tests) — integration tests covering the full lifecycle: start, execute, by-name, already-started, timeouts, failure-throws-ActivityFailedError, describe (running / terminated / canceled / retried / raw-info parity), state-transition-count, terminate-result-throws, polling-correctness (with timing assertions across one server long-poll deadline boundary), list, count, get-handle-with-nil-run-id, cancel-transitions-to-CANCELED, retry-policy / priority / id-reuse / id-conflict-policy round trips
  • test/client_activity_async_completion_test.rb (6 tests) — async completion via AsyncActivityHandle against a standalone ActivityIDReference: complete by activity_id, complete with run_id, heartbeat, fail, heartbeat+fail, report_cancellation
  • test/client_activity_hints_test.rb (6 tests) — definition hints used for client-side arg encode and worker-side decode, definition result_hint used for worker-side encode and client-side decode, call-site overrides on start_activity, by-name activities produce nil hints, handle.result(result_hint:) overrides, activity_handle(id, result_hint:) constructor-time hint, AsyncActivityHandle#complete(result, result_hint:) propagates the hint
  • test/client_activity_interceptor_chain_test.rb (5 tests) — multi-interceptor ordering for activity client calls; verifies first-added-is-outermost
  • test/client_workflow_interceptor_chain_test.rb (2 tests) — mirror of the above for the workflow client chain
  • test/activity_info_standalone_test.rb (2 tests) — Activity::Info standalone-vs-workflow fields asserted from inside the activity body
  • test/client/activity_id_reference_test.rb (5 tests) — factory shapes, for_standalone correctness, both forms coexist independently

Documentation:

  • README.md — added "Standalone Activities" subsection under "Activities" with five runnable code examples (start, execute, get-handle + describe/result/cancel/terminate, list/count, in-activity-body context introspection)

…ds, and new nullability for old fields, and a factory for creating the standalone version.

Enums for activity id reuse and conflict policies.
SAA-specific errors.
…e polling.

Add test_async_completion_heartbeat_and_fail_standalone in addition to previous heartbeat test.
Don’t use queue mechanism for gathering id info when it can be gotten directly.
Consistency of parameter names and ordering.
Add static_details.
Use constant for “activity:” prefix.
@GregoryTravis GregoryTravis requested a review from a team as a code owner May 19, 2026 21:15
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class support for standalone activities (activities started directly from a Temporalio::Client, not from within a workflow), including lifecycle operations (start/execute/result/describe/cancel/terminate), visibility querying, interceptor hooks, and updated activity context metadata to distinguish workflow-scheduled vs standalone execution.

Changes:

  • Introduces Client#start_activity / #execute_activity, Client::ActivityHandle, visibility APIs (list_activities / count_activities), and supporting data types/enums/errors.
  • Extends async-activity completion to accept standalone-form ActivityIDReference, and updates Activity::Info to surface standalone-vs-workflow metadata (with workflow fields becoming nullable).
  • Bumps embedded core (Rust crates) and refreshes generated API/proto bindings; adds extensive integration tests and README documentation.

Reviewed changes

Copilot reviewed 57 out of 62 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
temporalio/test/worker_workflow_test.rb Adjusts test to account for nullable workflow_id in Activity::Info.
temporalio/test/worker_activity_test.rb Adjusts tests to account for nullable workflow_id in Activity::Info.
temporalio/test/test.rb Enables dev-server dynamic config flags needed for standalone activities in tests.
temporalio/test/sig/client_workflow_interceptor_chain_test.rbs RBS for new workflow interceptor chain ordering tests.
temporalio/test/sig/client_activity_test.rbs RBS for standalone activity client tests.
temporalio/test/sig/client_activity_interceptor_chain_test.rbs RBS for standalone activity interceptor chain ordering tests.
temporalio/test/sig/client_activity_hints_test.rbs RBS for standalone activity hint propagation tests.
temporalio/test/sig/client_activity_async_completion_test.rbs RBS for standalone activity async-completion tests.
temporalio/test/sig/activity_info_standalone_test.rbs RBS for Activity::Info standalone-vs-workflow tests.
temporalio/test/client/activity_id_reference_test.rb Tests new standalone form of ActivityIDReference.
temporalio/test/client_workflow_interceptor_chain_test.rb Verifies workflow interceptor ordering convention.
temporalio/test/client_activity_test.rb End-to-end tests for standalone activity lifecycle + visibility + policies.
temporalio/test/client_activity_interceptor_chain_test.rb Verifies standalone activity interceptor coverage and ordering.
temporalio/test/client_activity_hints_test.rb Verifies arg/result hint propagation across standalone + async-completion paths.
temporalio/test/client_activity_async_completion_test.rb Tests async completion (complete/heartbeat/fail/cancel) for standalone activities.
temporalio/test/activity_info_standalone_test.rb Validates Activity::Info fields inside activity body for standalone vs workflow execution.
temporalio/sig/temporalio/internal/client/implementation.rbs Adds typing for new internal constant used by standalone async completion routing.
temporalio/sig/temporalio/error/failure.rbs Adds RBS for ActivityAlreadyStartedError.
temporalio/sig/temporalio/error.rbs Adds RBS for ActivityFailedError.
temporalio/sig/temporalio/common_enums.rbs Adds RBS for ActivityIDReusePolicy and ActivityIDConflictPolicy.
temporalio/sig/temporalio/client/interceptor.rbs Adds RBS for new activity-related interceptor inputs and outbound hooks.
temporalio/sig/temporalio/client/connection/cloud_service.rbs Adds RBS for new CloudService custom role RPCs.
temporalio/sig/temporalio/client/activity_id_reference.rbs Updates RBS for standalone-form fields and constructors.
temporalio/sig/temporalio/client/activity_handle.rbs Adds RBS for new Client::ActivityHandle.
temporalio/sig/temporalio/client/activity_execution.rbs Adds RBS for ActivityExecution and rich Description.
temporalio/sig/temporalio/client/activity_execution_status.rbs Adds RBS for standalone activity execution status enum wrapper.
temporalio/sig/temporalio/client/activity_execution_count.rbs Adds RBS for activity count + group-by result type.
temporalio/sig/temporalio/client.rbs Adds RBS for new public standalone activity APIs.
temporalio/sig/temporalio/api/payload_visitor.rbs Updates visitor signatures for new CloudService responses.
temporalio/sig/temporalio/api/cloud/namespace/v1/message.rbs Updates generated Cloud namespace message typings.
temporalio/sig/temporalio/api/cloud/identity/v1/message.rbs Updates generated Cloud identity message typings (custom roles).
temporalio/sig/temporalio/api/cloud/cloudservice/v1/request_response.rbs Updates generated CloudService request/response typings.
temporalio/sig/temporalio/activity/info.rbs Updates Activity::Info typing (new fields + workflow fields now nullable).
temporalio/sig/temporalio/activity/definition.rbs Adds RBS for _type_and_hints_from_parameter helper used by client APIs.
temporalio/lib/temporalio/testing/activity_environment.rb Updates test activity environment default Activity::Info construction for new required fields.
temporalio/lib/temporalio/internal/worker/activity_worker.rb Populates new Activity::Info fields (standalone-aware) when executing activities.
temporalio/lib/temporalio/internal/client/implementation.rb Implements standalone activity RPCs + visibility + long-poll outcome fetch + async completion routing changes.
temporalio/lib/temporalio/error/failure.rb Adds Error::ActivityAlreadyStartedError.
temporalio/lib/temporalio/error.rb Adds Error::ActivityFailedError.
temporalio/lib/temporalio/common_enums.rb Adds standalone activity ID reuse/conflict policy enums.
temporalio/lib/temporalio/client/interceptor.rb Adds activity interceptor inputs and outbound methods.
temporalio/lib/temporalio/client/connection/cloud_service.rb Adds CloudService custom role RPC wrappers.
temporalio/lib/temporalio/client/activity_id_reference.rb Extends ActivityIDReference with standalone form + predicate.
temporalio/lib/temporalio/client/activity_handle.rb Adds Client::ActivityHandle (result/describe/cancel/terminate).
temporalio/lib/temporalio/client/activity_execution.rb Adds ActivityExecution + Description wrappers for list/describe results.
temporalio/lib/temporalio/client/activity_execution_status.rb Adds standalone activity execution status constants.
temporalio/lib/temporalio/client/activity_execution_count.rb Adds count result type with aggregation groups.
temporalio/lib/temporalio/client.rb Adds activity_handle, start_activity, execute_activity, list_activities, count_activities public APIs.
temporalio/lib/temporalio/api/payload_visitor.rb Updates payload visitor for new CloudService custom role responses.
temporalio/lib/temporalio/api/cloud/namespace/v1/message.rb Updates generated Cloud namespace protobuf Ruby bindings.
temporalio/lib/temporalio/api/cloud/identity/v1/message.rb Updates generated Cloud identity protobuf Ruby bindings.
temporalio/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb Updates generated CloudService protobuf Ruby bindings.
temporalio/ext/src/client_rpc_generated.rs Adds dispatch entries for CloudService custom role RPCs.
temporalio/ext/Cargo.toml Bumps embedded Rust crates to 0.4.0.
temporalio/Cargo.lock Lockfile updates for Rust dependency bump.
README.md Documents standalone activity usage with runnable examples.
CHANGELOG.md Adds changelog entry for standalone activities.
.gitattributes Adds union merge driver config for CHANGELOG.md.
Files not reviewed (4)
  • temporalio/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb: Language not supported
  • temporalio/lib/temporalio/api/cloud/cloudservice/v1/service.rb: Language not supported
  • temporalio/lib/temporalio/api/cloud/identity/v1/message.rb: Language not supported
  • temporalio/lib/temporalio/api/cloud/namespace/v1/message.rb: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread README.md
handle = client.activity_handle('my-activity-id')
description = handle.describe # ActivityExecution::Description
result = handle.result # blocks until the activity reaches a terminal state
handle.cancel('reason for cancel'). # or
)

# Input for {Outbound.fetch_activity_outcome}. Used by `ActivityHandle#result` for long-polling
# the activity outcome via `DescribeActivityExecution` with `include_outcome: true`.
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.

Minimal, but this does seem like correct analysis

Comment on lines +171 to +177
# Build info. Standalone activities have empty workflow_execution / workflow_type / workflow_namespace
# on the wire; translate empty strings to nil for the user-facing Info fields.
workflow_id = Internal::ProtoUtils.string_or(start.workflow_execution&.workflow_id, nil)
workflow_run_id = Internal::ProtoUtils.string_or(start.workflow_execution&.run_id, nil)
workflow_type = Internal::ProtoUtils.string_or(start.workflow_type, nil)
activity_run_id = Internal::ProtoUtils.string_or(start.run_id, nil)
namespace = start.workflow_namespace
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.

Comment seems valid, would Internal::ProtoUtils.string_or(start.workflow_namespace, nil) be applicable here?

Copy link
Copy Markdown
Member

@chris-olszewski chris-olszewski left a comment

Choose a reason for hiding this comment

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

Looks very good! Mostly minor things and a few areas that I think could use more tests.

# a `UTF-8` encoded string nor a valid UTF-8 string.
# @!attribute workflow_id
# @return [String] Workflow ID that started this activity.
# @return [String, nil] Workflow ID that started this activity. Nil for standalone 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.

nil seems to be our comment style for referencing it in prose

Suggested change
# @return [String, nil] Workflow ID that started this activity. Nil for standalone activities.
# @return [String, nil] Workflow ID that started this activity. `nil` for standalone activities.

# standalone or scheduled from a workflow.
# @!attribute workflow_run_id
# @return [String] Workflow run ID that started this activity.
# @return [String, nil] Workflow run ID that started this activity. Nil for standalone 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.

Suggested change
# @return [String, nil] Workflow run ID that started this activity. Nil for standalone activities.
# @return [String, nil] Workflow run ID that started this activity. `nil` for standalone activities.

# @return [String, nil] Workflow run ID that started this activity. Nil for standalone activities.
# @!attribute workflow_type
# @return [String] Workflow type name that started this activity.
# @return [String, nil] Workflow type name that started this activity. Nil for standalone 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.

Suggested change
# @return [String, nil] Workflow type name that started this activity. Nil for standalone activities.
# @return [String, nil] Workflow type name that started this activity. `nil` for standalone activities.

@raw_info.activity_id
end

# @return [String, nil] Run ID for this activity execution attempt. Nil if absent on the server.
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.

None of the other SDKs mention server here. Would either remove of clarify when it would be absent on server.

Suggested change
# @return [String, nil] Run ID for this activity execution attempt. Nil if absent on the server.
# @return [String, nil] Run ID for this activity execution attempt.

# @return [String] ID for the activity.
attr_reader :id

# @return [String, nil] Run ID for this activity execution. Nil targets the latest run.
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.

Suggested change
# @return [String, nil] Run ID for this activity execution. Nil targets the latest run.
# @return [String, nil] Run ID for this activity execution. `nil` targets the latest run.

Comment thread README.md
result = handle.result # blocks until the activity completes
```

Or use the start-and-wait shortcut:
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.

"shortcut" is my primary objection with this, helper feels more appropriate

Suggested change
Or use the start-and-wait shortcut:
Or use the execute helper to start and wait:

Comment thread README.md
```

Standalone activities require the dev server to enable the SAA feature flags. See the test bootstrap in
`temporalio/test/test.rb` for the required `--dynamic-config-value` flags.
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.

Let us just be explicit here instead of referencing a file that might become stale.

Suggested change
`temporalio/test/test.rb` for the required `--dynamic-config-value` flags.
Standalone activities require the dev server to enable the following feature flags: `frontend.activityAPIsEnabled`, `activity.enableStandalone`, `history.enableChasm`, `history.enableTransitionHistory`.

def test_execute_activity_by_name
with_activity_worker([SimpleActivity]) do |task_queue|
result = env.client.execute_activity(
'SimpleActivity',
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.

AI pointed out that we don't have test coverage for starting activities by symbol, activity instance, or activity definition. Would be nice to have some verification there, at bare minimum we should verify the unhappy path here e.g. dynamic activity or a true junk input.

super
end

def cancel_activity(input)
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 interceptor isn't covered by any test

end
end

def test_list_activities_simple_list_is_accurate
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 should probably have some test coverage of the pagination logic just as a smoke test.

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