Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
b1c199a
Bump sdk-core to a22517e4 (0.4.0) (step 0)
GregoryTravis May 12, 2026
30ae677
Merge branch 'main' into saa-yolo
GregoryTravis May 13, 2026
411220b
Update ActivityIDReference to support standalone activities: new fiel…
GregoryTravis May 13, 2026
0561236
wip (step 2)
GregoryTravis May 13, 2026
8b3e08b
Merge branch 'main' into gmt/ruby-standalone-activities
GregoryTravis May 14, 2026
79ec0e8
Merge branch 'main' into gmt/ruby-standalone-activities
GregoryTravis May 14, 2026
aea6eb6
Touch up docs.
GregoryTravis May 14, 2026
029e387
Remove doc references to later steps
GregoryTravis May 14, 2026
96bd69c
Interceptor path, Client, Info, async compltion (step 3)
GregoryTravis May 15, 2026
6c463ff
More tests
GregoryTravis May 15, 2026
785f99a
Make result polling test activity run long enough to actually exercis…
GregoryTravis May 15, 2026
ff172a9
Document that attempt ordering starts at 1.
GregoryTravis May 15, 2026
3a76877
Workflow interceptor ordering tests
GregoryTravis May 18, 2026
4978bde
Amend comment
GregoryTravis May 18, 2026
a134674
Simplify activity specification dispatch
GregoryTravis May 18, 2026
7e679eb
Complete docs for execute_activity
GregoryTravis May 18, 2026
2432134
Default ActivityIDConflictPolicy to UNSPECIFIED.
GregoryTravis May 19, 2026
85ae01f
Default back to ActivityIDConflictPolicy::FAIL
GregoryTravis May 19, 2026
54ea93e
Add result to example
GregoryTravis May 19, 2026
4430429
Fix count assertions in tests
GregoryTravis May 19, 2026
b1540a0
Check result in test_get_handle_with_nil_run_id
GregoryTravis May 19, 2026
3aefd55
Update name of test_activity_handle_describe_terminate_smoke
GregoryTravis May 19, 2026
346da6d
In test_describe_canceled_reason_after_cancel, check the status and t…
GregoryTravis May 19, 2026
553ad50
Additional hint tests
GregoryTravis May 19, 2026
404674a
Test type hint override in activity_handle constructor.
GregoryTravis May 19, 2026
1823197
Update docstring on ActivitHandle.result
GregoryTravis May 19, 2026
35931d7
Test for multiple long-poll timeouts
GregoryTravis May 19, 2026
b7bc32b
Remove presence check for proto .run_id.
GregoryTravis May 19, 2026
b8b960d
Use nil for missing reason
GregoryTravis May 19, 2026
c677537
Clarify doc on _type_and_hints_from_parameter
GregoryTravis May 19, 2026
9de8b3d
Merge branch 'main' into gmt/ruby-standalone-activities
GregoryTravis May 19, 2026
b496fb8
"or" clarification in cancel/terminate example
GregoryTravis May 19, 2026
fc15980
CHANGELOG
GregoryTravis May 19, 2026
3887a4e
.gitattributes with union=merge for CHANGELOG
GregoryTravis May 19, 2026
cdd4ee9
Lint
GregoryTravis May 20, 2026
cf6ae35
Regenerate proto classes
GregoryTravis May 20, 2026
ccbd9ef
yard fix
GregoryTravis May 20, 2026
35f774b
Merge branch 'main' into gmt/ruby-standalone-activities
GregoryTravis May 21, 2026
e710c58
Potential fix for pull request finding
GregoryTravis May 29, 2026
72f4233
Fix missing-fields comment
GregoryTravis May 29, 2026
5046a13
nil instead of Nil
GregoryTravis May 29, 2026
4a110b2
_activity_id_reference_request_fields
GregoryTravis May 29, 2026
50834b1
workflow namespace is optional
GregoryTravis May 29, 2026
f93c2f4
Missing namespace falls back to client namespace
GregoryTravis May 29, 2026
b09b9c1
review
GregoryTravis Jun 1, 2026
43b31a8
review
GregoryTravis Jun 1, 2026
6fcdfc9
Add test_cancel_activity_interceptor_called.
GregoryTravis Jun 1, 2026
e64fb21
Test for list activities pagination
GregoryTravis Jun 1, 2026
c6e0f43
workflow_namespace is nil for SAA
GregoryTravis Jun 1, 2026
5ba0ea7
review
GregoryTravis Jun 1, 2026
2f948d4
Update temporalio/lib/temporalio/client/activity_execution.rb
GregoryTravis Jun 1, 2026
21868fe
Merge branch 'gmt/ruby-standalone-activities' of https://github.com/t…
GregoryTravis Jun 1, 2026
8a49b7d
Update temporalio/lib/temporalio/client/activity_execution.rb
GregoryTravis Jun 1, 2026
06db942
Merge branch 'gmt/ruby-standalone-activities' of https://github.com/t…
GregoryTravis Jun 1, 2026
2a28187
review
GregoryTravis Jun 1, 2026
56be55f
review
GregoryTravis Jun 1, 2026
143f159
PendingActivityState constant module
GregoryTravis Jun 1, 2026
c6357a8
review
GregoryTravis Jun 1, 2026
caefb23
Merge branch 'main' into gmt/ruby-standalone-activities
GregoryTravis Jun 1, 2026
2085f2e
Revert sdk-core/rust version bump, no longer needed.
GregoryTravis Jun 2, 2026
37c5183
SAA server flags no longer needed for tests.
GregoryTravis Jun 2, 2026
5c6c296
Don't expose state transition count
GregoryTravis Jun 2, 2026
392bef4
Markdown quoting, not rdoc.
GregoryTravis Jun 2, 2026
7bd3fbd
Update temporalio/lib/temporalio/client/activity_handle.rb
GregoryTravis Jun 2, 2026
0a316a3
Merge branch 'gmt/ruby-standalone-activities' of https://github.com/t…
GregoryTravis Jun 2, 2026
6c094d7
review
GregoryTravis Jun 2, 2026
ba57829
Mention ID reuse and conflict policy.
GregoryTravis Jun 2, 2026
6dd9732
Restore activity pause flag
GregoryTravis Jun 2, 2026
6ac8a9e
Update temporalio/lib/temporalio/common_enums.rb
GregoryTravis Jun 2, 2026
9d9d35f
Fix reuse policy docs.
GregoryTravis Jun 2, 2026
57a92c4
Reformat CHANGELOG to standard format, remove code snippet.
GregoryTravis Jun 2, 2026
ac8d528
Input validation for start_activity
GregoryTravis Jun 2, 2026
e325de4
RBS types for protobufs
GregoryTravis Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CHANGELOG.md merge=union
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Changelog

## [Unreleased]

### Breaking Changes

#### `Activity::Info` workflow fields are now nullable

With the introduction of Standalone Activities (see below), an activity is no longer guaranteed to
have been scheduled by a workflow. `Activity::Info#workflow_id`, `#workflow_run_id`,
`#workflow_type`, and `#workflow_namespace` are now nullable β€” they return `nil` when the activity
was started via `Client#start_activity` rather than from a workflow. A new `Activity::Info#namespace`
accessor is always set (falling back to the client's namespace for standalone activities) and is
the recommended replacement for the deprecated `#workflow_namespace`.

Existing workflow-only code paths are unaffected at runtime. The recommended migration is to call
`Activity::Info#in_workflow?` and branch on the result.

### Added

#### Standalone Activities

Activities can now be started directly from a client, independently of any workflow. `Client#start_activity`
and `Client#execute_activity` schedule a standalone activity execution by ID and task queue, accepting the
same `Activity::Definition` classes (or by-name strings/symbols) used in workflow-scheduled activities.
`Client::ActivityHandle` provides `#result`, `#describe`, `#cancel`, and `#terminate`; `Client#list_activities`
and `Client#count_activities` provide visibility-backed queries; and `Client#async_activity_handle` now
accepts a standalone-form `ActivityIDReference` (constructed via `ActivityIDReference.for_standalone`) for
async completion.

See https://docs.temporal.io/standalone-activity for the cross-SDK feature overview.
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,63 @@ it will raise the error raised in the activity.
The constructor of the environment has multiple keyword arguments that can be set to affect the activity context for the
activity.

#### Standalone Activities

Activities can be started directly from a client, outside the context of any workflow. Standalone activities reuse the
existing `Activity::Definition` class β€” the same activity code runs whether invoked from a workflow or as a standalone
activity. They are addressed by `activity_id` (with an optional `activity_run_id` for disambiguating re-runs).

Start a standalone activity:

```ruby
handle = client.start_activity(
MyActivity,
'some-arg',
id: 'my-activity-id',
task_queue: 'my-task-queue',
start_to_close_timeout: 60
)
result = handle.result # blocks until the activity completes
```

Or use the execute helper to start and wait:

```ruby
result = client.execute_activity(
MyActivity, 'some-arg',
id: 'my-activity-id', task_queue: 'my-task-queue', start_to_close_timeout: 60
)
```

Get a handle to an existing standalone activity to describe, cancel, terminate, or fetch its result:

```ruby
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
handle.terminate('reason for terminate')
```

List and count standalone activities (visibility queries):

```ruby
client.list_activities('ActivityType="MyActivity"').each { |exec| puts exec.activity_id }
count = client.count_activities('ActivityType="MyActivity"').count
```

Inside an activity body, `Temporalio::Activity::Context.current.info` exposes whether the activity is standalone
or workflow-scheduled:

```ruby
info = Temporalio::Activity::Context.current.info
if info.in_workflow?
# info.workflow_id, info.workflow_run_id, info.workflow_type are set
else
# info.activity_run_id is set; workflow_* fields are nil
end
```

### Telemetry

#### Metrics
Expand Down
22 changes: 22 additions & 0 deletions temporalio/lib/temporalio/activity/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,28 @@ class Info
# @return [Object, nil] Result hint
attr_reader :result_hint

# Resolve an activity argument into a `[name, arg_hints, result_hint]` triple. Used by
# `Client#start_activity` to accept any of: a `Definition` subclass, an instance of one,
# an `Info`, a `Symbol` activity name, or a `String` activity name. Class/instance/Info
# inputs carry their definition's hints; Symbol/String inputs return `nil` hints.
#
# @param activity [Class<Definition>, Definition, Info, Symbol, String] Activity argument.
# @return [Array(String, Array[Object]?, Object?)] name, arg_hints, result_hint.
def self._type_and_hints_from_parameter(activity)
case activity
when String, Symbol
[activity.to_s, nil, nil]
when Class, Definition, Info
# Return or construct an Info -- needed because we want the checks in Info.initialize.
info = from_activity(activity)
raise ArgumentError, 'Cannot pass dynamic activity to start_activity' unless info.name

[info.name.to_s, info.arg_hints, info.result_hint]
else
raise ArgumentError, "#{activity} is not an activity class, instance, info, symbol, or string"
end
end

# Obtain definition info representing the given activity, which can be a class, instance, or definition info.
#
# @param activity [Definition, Class<Definition>, Info] Activity to get info for.
Expand Down
23 changes: 18 additions & 5 deletions temporalio/lib/temporalio/activity/info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ module Temporalio
module Activity
Info = Data.define(
:activity_id,
:activity_run_id,
:activity_type,
:attempt,
:current_attempt_scheduled_time,
:heartbeat_timeout,
:local?,
:namespace,
:priority,
:retry_policy,
:raw_heartbeat_details,
Expand All @@ -31,16 +33,20 @@ module Activity
#
# @!attribute activity_id
# @return [String] ID for the activity.
# @!attribute activity_run_id
# @return [String, nil] Run ID for a standalone activity execution. nil for activities scheduled from a workflow.
# @!attribute activity_type
# @return [String] Type name for the activity.
# @!attribute attempt
# @return [Integer] Attempt the activity is on.
# @return [Integer] Attempt the activity is on. Attempts start at 1 and increment on each retry.
# @!attribute current_attempt_scheduled_time
# @return [Time] When the current attempt was scheduled.
# @!attribute heartbeat_timeout
# @return [Float, nil] Heartbeat timeout set by the caller.
# @!attribute local?
# @return [Boolean] Whether the activity is a local activity or not.
# @!attribute namespace
# @return [String] Namespace this activity is on.
# @!attribute priority
# @return [Priority] The priority of this activity.
# @!attribute retry_policy
Expand All @@ -64,17 +70,24 @@ module Activity
# @return [String] Task token uniquely identifying this activity. Note, this is a `ASCII-8BIT` encoded string, not
# 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.
# @!attribute workflow_namespace
# @return [String] Namespace this activity is on.
# @return [String, nil] Namespace of the workflow that scheduled this activity. Nil for standalone
# activities. Prefer {#namespace}, which is always set.
# @deprecated Use {#namespace} instead.
# @!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.
# @!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.
#
# @note WARNING: This class may have required parameters added to its constructor. Users should not instantiate this
# class or it may break in incompatible ways.
class Info
# @return [Boolean] True if this activity was scheduled by a workflow execution; false for standalone activities.
def in_workflow?
!workflow_id.nil?
end

# Convert raw heartbeat details into Ruby types.
#
# Note, this live-converts every invocation.
Expand Down
184 changes: 184 additions & 0 deletions temporalio/lib/temporalio/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'google/protobuf/well_known_types'
require 'logger'
require 'temporalio/api'
require 'temporalio/client/activity_handle'
require 'temporalio/client/async_activity_handle'
require 'temporalio/client/connection'
require 'temporalio/client/interceptor'
Expand Down Expand Up @@ -478,6 +479,189 @@ def workflow_handle(
)
end

# Get a handle for an existing standalone activity. Useful when the activity was started elsewhere
# (a different process, or by another client) and you have only its ID.
#
# @param activity_id [String] ID for the activity.
# @param activity_run_id [String, nil] Run ID for the activity execution. If nil, operations target the
# latest run of the given activity ID.
# @param result_hint [Object, nil] Converter hint for the activity's result. Set this when you know what
# type the activity returns so {ActivityHandle#result}'s deserialization uses the right hint.
#
# @return [ActivityHandle] The activity handle.
def activity_handle(activity_id, activity_run_id: nil, result_hint: nil)
ActivityHandle.new(client: self, id: activity_id, run_id: activity_run_id, result_hint:)
end

# Start a standalone activity execution and return its handle.
#
# @param activity [Class<Activity::Definition>, Activity::Definition, Activity::Definition::Info, Symbol, String]
# Activity definition, definition class or activity name.
# @param args [Array<Object>] Arguments to the activity.
# @param id [String] Unique identifier for the activity execution.
# @param task_queue [String] Task queue to run the activity on.
# @param schedule_to_close_timeout [Float, nil] Schedule-to-close timeout in seconds. Either this or
# `start_to_close_timeout` must be specified.
# @param schedule_to_start_timeout [Float, nil] Schedule-to-start timeout in seconds.
# @param start_to_close_timeout [Float, nil] Start-to-close timeout in seconds. Either this or
# `schedule_to_close_timeout` must be specified.
# @param heartbeat_timeout [Float, nil] Heartbeat timeout in seconds.
# @param id_reuse_policy [ActivityIDReusePolicy] Controls behavior when an activity with the same ID
# was previously run and has reached a terminal state. Defaults to `ALLOW_DUPLICATE`.
# @param id_conflict_policy [ActivityIDConflictPolicy] Controls behavior when an activity with the same ID
# is currently running. Defaults to `FAIL` (reject the start attempt).
# @param retry_policy [RetryPolicy, nil] Retry policy for the activity.
# @param search_attributes [SearchAttributes, nil] Search attributes for the activity.
# @param static_summary [String, nil] Fixed single-line summary for this activity execution.
# @param static_details [String, nil] Fixed details for this activity execution. May be in markdown format.
# @param priority [Priority] Priority for the activity.
# @param arg_hints [Array<Object>, nil] Argument hints.
# @param result_hint [Object, nil] Result hint.
# @param rpc_options [RPCOptions, nil] Advanced RPC options.
#
# @return [ActivityHandle] Handle to the started activity.
# @raise [Error::ActivityAlreadyStartedError] Activity already exists with this ID.
# @raise [Error::RPCError] RPC error from call.
def start_activity(
activity,
*args,
id:,
task_queue:,
schedule_to_close_timeout: nil,
schedule_to_start_timeout: nil,
start_to_close_timeout: nil,
heartbeat_timeout: nil,
id_reuse_policy: ActivityIDReusePolicy::ALLOW_DUPLICATE,
id_conflict_policy: ActivityIDConflictPolicy::FAIL,
retry_policy: nil,
search_attributes: nil,
static_summary: nil,
static_details: nil,
priority: Priority.default,
arg_hints: nil,
result_hint: nil,
rpc_options: nil
)
activity_name, defn_arg_hints, defn_result_hint =
Activity::Definition::Info._type_and_hints_from_parameter(activity)
@impl.start_activity(Interceptor::StartActivityInput.new(
activity: activity_name,
args:,
activity_id: id,
task_queue:,
schedule_to_close_timeout:,
schedule_to_start_timeout:,
start_to_close_timeout:,
heartbeat_timeout:,
id_reuse_policy:,
id_conflict_policy:,
retry_policy:,
search_attributes:,
static_summary:,
static_details:,
headers: {},
priority:,
arg_hints: arg_hints || defn_arg_hints,
result_hint: result_hint || defn_result_hint,
rpc_options:
))
end

# Start a standalone activity execution and wait for its result. Shortcut for
# {start_activity} + {ActivityHandle#result}.
#
# @param activity [Class<Activity::Definition>, Activity::Definition, Activity::Definition::Info, Symbol, String]
# Activity definition, definition class or activity name.
# @param args [Array<Object>] Arguments to the activity.
# @param id [String] Unique identifier for the activity execution.
# @param task_queue [String] Task queue to run the activity on.
# @param schedule_to_close_timeout [Float, nil] Schedule-to-close timeout in seconds. Either this or
# `start_to_close_timeout` must be specified.
# @param schedule_to_start_timeout [Float, nil] Schedule-to-start timeout in seconds.
# @param start_to_close_timeout [Float, nil] Start-to-close timeout in seconds. Either this or
# `schedule_to_close_timeout` must be specified.
# @param heartbeat_timeout [Float, nil] Heartbeat timeout in seconds.
# @param id_reuse_policy [ActivityIDReusePolicy] Controls behavior when an activity with the same ID
# was previously run and has reached a terminal state. Defaults to `ALLOW_DUPLICATE`.
# @param id_conflict_policy [ActivityIDConflictPolicy] Controls behavior when an activity with the same ID
# is currently running. Defaults to `FAIL` (reject the start attempt).
# @param retry_policy [RetryPolicy, nil] Retry policy for the activity.
# @param search_attributes [SearchAttributes, nil] Search attributes for the activity.
# @param static_summary [String, nil] Fixed single-line summary for this activity execution.
# @param static_details [String, nil] Fixed details for this activity execution. May be in markdown format.
# @param priority [Priority] Priority for the activity.
# @param arg_hints [Array<Object>, nil] Argument hints.
# @param result_hint [Object, nil] Result hint.
# @param rpc_options [RPCOptions, nil] Advanced RPC options.
#
# @return [Object, nil] Successful result of the activity.
# @raise [Error::ActivityAlreadyStartedError] Activity already exists with this ID.
Comment thread
GregoryTravis marked this conversation as resolved.
# @raise [Error::ActivityFailedError] With `cause` populated from the activity failure.
# @raise [Error::RPCError] RPC error from call.
def execute_activity(
activity,
*args,
id:,
task_queue:,
schedule_to_close_timeout: nil,
schedule_to_start_timeout: nil,
start_to_close_timeout: nil,
heartbeat_timeout: nil,
id_reuse_policy: ActivityIDReusePolicy::ALLOW_DUPLICATE,
id_conflict_policy: ActivityIDConflictPolicy::FAIL,
retry_policy: nil,
search_attributes: nil,
static_summary: nil,
static_details: nil,
priority: Priority.default,
arg_hints: nil,
result_hint: nil,
rpc_options: nil
)
start_activity(
activity,
*args,
id:,
task_queue:,
schedule_to_close_timeout:,
schedule_to_start_timeout:,
start_to_close_timeout:,
heartbeat_timeout:,
id_reuse_policy:,
id_conflict_policy:,
retry_policy:,
search_attributes:,
static_summary:,
static_details:,
priority:,
arg_hints:,
result_hint:,
rpc_options:
).result
end

# List standalone activities matching a visibility query.
#
# @param query [String] Visibility list filter.
# @param rpc_options [RPCOptions, nil] Advanced RPC options.
#
# @return [Enumerator<ActivityExecution>] Lazy enumerable of matching activity executions.
# @raise [Error::RPCError] RPC error from call.
def list_activities(query, rpc_options: nil)
@impl.list_activities(Interceptor::ListActivitiesInput.new(query:, rpc_options:))
end

# Count standalone activities matching a visibility query.
#
# @param query [String] Visibility list filter.
# @param rpc_options [RPCOptions, nil] Advanced RPC options.
#
# @return [ActivityExecutionCount] Count of activities (with per-group counts if the query had a group-by clause).
# @raise [Error::RPCError] RPC error from call.
def count_activities(query, rpc_options: nil)
@impl.count_activities(Interceptor::CountActivitiesInput.new(query:, rpc_options:))
end

# Start an update, possibly starting the workflow at the same time if it doesn't exist (depending upon ID conflict
# policy). Note that in some cases this may fail but the workflow will still be started, and the handle can then be
# retrieved on the start workflow operation.
Expand Down
Loading
Loading