💥 Standalone Activities for Ruby#443
Conversation
…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.
…he result exception
Test 2+ activit retries.
Use constant for “activity:” prefix.
There was a problem hiding this comment.
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 updatesActivity::Infoto 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.
| 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`. |
There was a problem hiding this comment.
Minimal, but this does seem like correct analysis
| # 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 |
There was a problem hiding this comment.
Comment seems valid, would Internal::ProtoUtils.string_or(start.workflow_namespace, nil) be applicable here?
chris-olszewski
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
nil seems to be our comment style for referencing it in prose
| # @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. |
There was a problem hiding this comment.
| # @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. |
There was a problem hiding this comment.
| # @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. |
There was a problem hiding this comment.
None of the other SDKs mention server here. Would either remove of clarify when it would be absent on server.
| # @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. |
There was a problem hiding this comment.
| # @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. |
| result = handle.result # blocks until the activity completes | ||
| ``` | ||
|
|
||
| Or use the start-and-wait shortcut: |
There was a problem hiding this comment.
"shortcut" is my primary objection with this, helper feels more appropriate
| Or use the start-and-wait shortcut: | |
| Or use the execute helper to start and wait: |
| ``` | ||
|
|
||
| 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. |
There was a problem hiding this comment.
Let us just be explicit here instead of referencing a file that might become stale.
| `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', |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
This interceptor isn't covered by any test
| end | ||
| end | ||
|
|
||
| def test_list_activities_simple_list_is_accurate |
There was a problem hiding this comment.
We should probably have some test coverage of the pagination logic just as a smoke test.
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_typeare now nullable (String?); they returnnilwhen the activity is standalone.Activity::Info#workflow_namespaceis now marked@deprecatedin favor of the new#namespaceaccessor. 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, orActivity::Definition::Info;execute_activityisstart_activity+handle.resultas a shortcutClient#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_activitiesreturns anEnumerator<ActivityExecution>Client::ActivityHandle— handle returned bystart_activity/activity_handle; provides#result(result_hint:, rpc_options:),#describe,#cancel(reason),#terminate(reason)Client::ActivityExecution— lightweight metadata used inlist_activitiesresultsClient::ActivityExecution::Description— rich descriptor returned byActivityHandle#describe; subclass ofActivityExecution. 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 ofcount_activities(), with group-by supportClient::ActivityExecutionStatus— enum module wrapping the proto's status valuesActivityIDReusePolicy+ActivityIDConflictPolicy— enum modules inlib/temporalio/common_enums.rbmirroring the workflow-side policiesError::ActivityAlreadyStartedError(inerror/failure.rb) — raised bystart_activitywhen the server rejects a duplicate IDError::ActivityFailedError(inerror.rb) — raised byhandle.resultwhen the activity terminates in failure;causecarries the underlying failure (ApplicationError,CanceledError,TimeoutError,TerminatedError)New interceptor API:
Client::Interceptor::StartActivityInput,DescribeActivityInput,CancelActivityInput,TerminateActivityInput,ListActivitiesInput,CountActivitiesInput,FetchActivityOutcomeInput— seven new Data.define input typesClient::Interceptor::Outboundextended with seven matching pass-through methods (start_activity,describe_activity,cancel_activity,terminate_activity,list_activities,count_activities,fetch_activity_outcome)Activity::Infoadditions:#activity_run_id— run-scoped id assigned by the server to each standalone activity execution (nilfor workflow-dispatched)#namespace— canonical namespace accessor (preferred over the deprecated#workflow_namespace)#in_workflow?— predicate distinguishing workflow-dispatched activities from standalone onesClient#async_activity_handleextension (no new method):ActivityIDReferenceconstructed viaActivityIDReference.for_standalone(activity_id:, activity_run_id:).ActivityIDReferenceadditions:ActivityIDReference.for_standalone(activity_id:, activity_run_id: nil)— class factory for the new standalone shape.#standalone?— predicate for branching in async-completion routingCI / dev server:
frontend.activityAPIsEnabled,activity.enableStandalone,history.enableChasm,history.enableTransitionHistory(added totest/test.rb).sdk-coresubmodule toa22517e4(Rust crate 0.4.0) to pick up the worker-sidecoresdk.activity_task.Start.run_idproto field used to plumbactivity_run_idintoActivity::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 tripstest/client_activity_async_completion_test.rb(6 tests) — async completion viaAsyncActivityHandleagainst a standaloneActivityIDReference: complete by activity_id, complete with run_id, heartbeat, fail, heartbeat+fail, report_cancellationtest/client_activity_hints_test.rb(6 tests) — definition hints used for client-side arg encode and worker-side decode, definitionresult_hintused for worker-side encode and client-side decode, call-site overrides onstart_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 hinttest/client_activity_interceptor_chain_test.rb(5 tests) — multi-interceptor ordering for activity client calls; verifies first-added-is-outermosttest/client_workflow_interceptor_chain_test.rb(2 tests) — mirror of the above for the workflow client chaintest/activity_info_standalone_test.rb(2 tests) —Activity::Infostandalone-vs-workflow fields asserted from inside the activity bodytest/client/activity_id_reference_test.rb(5 tests) — factory shapes,for_standalonecorrectness, both forms coexist independentlyDocumentation:
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)