Standalone Activities Feature Guide
Standalone Activities are Activities that run independently, without being
orchestrated by a Workflow. Instead of executing an Activity from within a Workflow Definition using
ctx.execute_activity(), you execute a Standalone Activity directly from a
Client.
The way you write the Activity and register it with a Worker is identical to Workflow Activities. The only difference is that you execute a Standalone Activity directly from your Temporal Client.
New to Standalone Activities? Start with the Standalone Activities Quickstart.
This page covers the following:
- Prerequisites
- Start a Standalone Activity without waiting for the result
- Get a handle to an existing Standalone Activity
- Wait for the result of a Standalone Activity
- List Standalone Activities
- Count Standalone Activities
- Run Standalone Activities with Temporal Cloud
This documentation uses source code from the standalone_activities sample.
Prerequisites
Standalone Activities require:
- Rust 1.92.0+
- Temporal Rust SDK v1.0.0 or higher
- Temporal CLI v1.9.1 or higher
The Standalone Activities Quickstart walks through installing these.
Start a Standalone Activity without waiting for the result
Starting a Standalone Activity means sending a request to the Temporal Server to durably enqueue your Activity job, without waiting for it to be executed by your Worker.
Use Client::start_activity
to start a Standalone Activity and get a handle without waiting for the result:
let options = ActivityStartOptions::with_start_to_close_timeout(
"standalone-activities",
"standalone-activity-id",
Duration::from_secs(10),
)
.build();
// Returns as soon as the server has durably enqueued the activity.
let handle = client
.start_activity(
GreetingActivities::compose_greeting,
("Hello".to_string(), "Temporal".to_string()),
options,
)
.await?;
println!(
"Started activity, id: {} run_id: {:?}",
handle.activity_id(),
handle.run_id()
);
The first argument identifies the Activity to run. Passing the Activity method itself, such as
GreetingActivities::compose_greeting, gives you a typed
ActivityHandle,
so the input and output types are checked at compile time. The second argument is the Activity's
input; if your Activity takes several values, pass them as a tuple.
ActivityStartOptions
requires a Task Queue, an Activity ID, and a close timeout. The
with_start_to_close_timeout and with_schedule_to_close_timeout constructors return a builder
with that timeout already set; call .build() to finish, or set additional fields such as
retry_policy, heartbeat_timeout, id_reuse_policy, or priority first.
With the Temporal Server and Worker running, open a new terminal in the crates/sdk directory and
run:
cargo run --features examples --example standalone-activities-start
Or use the Temporal CLI:
temporal activity start \
--type 'GreetingActivities::compose_greeting' \
--activity-id standalone-activity-id \
--task-queue standalone-activities \
--start-to-close-timeout 10s \
--input '["Hello","Temporal"]'
By default, the #[activities] macro names each Activity <ImplType>::<method_name>, so the
compose_greeting method on GreetingActivities registers as GreetingActivities::compose_greeting. Use that
name when referring to the Activity from the CLI or from a List Filter query.
Get a handle to an existing Standalone Activity
Use Client::get_activity_handle
to create an
ActivityHandle
for a previously started Standalone Activity:
// Passing `None` for the run ID targets the latest run with this activity ID.
let handle = client.get_activity_handle(
GreetingActivities::compose_greeting,
"standalone-activity-id",
None,
);
Pass None for the run ID to target the latest run of the given Activity ID, or pass
Some(run_id) to target a specific run.
If you don't have the Activity definition on hand, for example in a tool that operates on
Activities it didn't start, use
Client::get_untyped_activity_handle
instead. You can still describe, cancel, and terminate through an untyped handle.
You can then use the handle to wait for the result, describe, cancel, or terminate the Activity:
handle.result().await?; // block until the activity completes
handle.describe(Default::default()).await?; // status, timestamps, attempt, ...
handle.cancel(Default::default()).await?; // request cancellation
handle.terminate(Default::default()).await?; // force-close the activity
describe
takes an
ActivityDescribeOptions.
Its include_input, include_outcome, include_heartbeat_details, and include_last_failure
fields are off by default, because those fields carry Payloads that can be arbitrarily large. Turn
them on only when you need them:
let description = handle
.describe(
ActivityDescribeOptions::builder()
.include_outcome(true)
.build(),
)
.await?;
println!("Status: {:?}", description.status());
println!("Type: {}", description.activity_type());
println!("Attempt: {}", description.attempt());
The accessors on the description (status(), activity_type(), schedule_time(), and so on)
come from the
ActivityExecutionInfoLike
trait, so bring it into scope to use them.
Run it, after executing an Activity with one of the samples above:
cargo run --features examples --example standalone-activities-get-handle
Or use the Temporal CLI to describe an Activity by ID:
temporal activity describe --activity-id standalone-activity-id
Wait for the result of a Standalone Activity
The Rust SDK has no single call that both starts an Activity and waits for its result. Call
start_activity to durably enqueue the Activity, then
ActivityHandle::result
to block until it completes and return the result:
// There is no single "execute" call: start the activity, then await its result.
let handle = client
.start_activity(
GreetingActivities::compose_greeting,
("Hello".to_string(), "Temporal".to_string()),
options,
)
.await?;
let result = handle.result().await?;
println!("Activity result: {result}");
Because the handle is typed, result returns the Activity's own output type, String in this
case, with no downcasting. It fails with an
ActivityResultError
if the Activity failed, was cancelled, or was terminated.
Splitting start from result also means you don't have to wait in the same process, or even the same
program, that started the Activity: get a handle later and call result on
it.
Or use the Temporal CLI to wait for a result by Activity ID:
temporal activity result --activity-id standalone-activity-id
List Standalone Activities
Use Client::list_activities
to list Standalone Activity Executions that match a List Filter query. The result is
a ListActivitiesStream,
a Stream of
ActivityExecutionInfo
values that fetches pages from the server on demand as the stream is consumed.
These APIs return only Standalone Activity Executions. Activities running inside Workflows are not included.
use futures::StreamExt;
use temporalio_client::ActivityExecutionInfoLike;
let mut executions =
client.list_activities("TaskQueue = 'standalone-activities'", Default::default());
while let Some(execution) = executions.next().await {
let execution = execution?;
println!(
"{} {} {:?}",
execution.activity_id(),
execution.activity_type(),
execution.status()
);
}
list_activities is not async. It returns the stream immediately, and the requests happen as you
poll it. Each item is a Result, because a page fetch can fail partway through the
stream.
Run it:
cargo run --features examples --example standalone-activities-list
Or use the Temporal CLI:
temporal activity list
The query parameter accepts the same List Filter syntax used for Workflow
Visibility. For example,
ActivityType = 'GreetingActivities::compose_greeting' AND ExecutionStatus = 'Running'.
Count Standalone Activities
Use Client::count_activities
to count Standalone Activity Executions that match a List Filter query. This returns
the total count of executions (running, completed, failed, etc.) — not the number of queued tasks.
It works the same way as counting Workflow Executions.
let count = client
.count_activities("TaskQueue = 'standalone-activities'", Default::default())
.await?;
println!("Total: {}", count.count());
// Non-empty only when the query has a GROUP BY clause.
for group in count.groups() {
println!(" {:?} => {}", group.get::<String>(0), group.count());
}
If the query has a GROUP BY clause,
groups()
holds the per-group counts and count() is their sum; otherwise groups() is empty. Group values
are typed: read them with get::<T>(index), or with try_get if you want to handle a
deserialization failure rather than get None.
Run it:
cargo run --features examples --example standalone-activities-count
Or use the Temporal CLI:
temporal activity count
Run Standalone Activities with Temporal Cloud
The Worker and Client code in the Standalone Activities Quickstart
use ClientOptions::load_from_config,
so the same code works against Temporal Cloud — configure the connection via environment variables
or a TOML profile. No code changes are needed.
For a step-by-step guide on connecting to Temporal Cloud, including Namespace creation, certificate generation, and authentication setup in the Cloud UI, see Connect to Temporal Cloud.
Connect with mTLS
Set these environment variables with values from your Temporal Cloud Namespace settings:
export TEMPORAL_ADDRESS=<your-namespace>.<your-account-id>.tmprl.cloud:7233
export TEMPORAL_NAMESPACE=<your-namespace>.<your-account-id>
export TEMPORAL_TLS_CLIENT_CERT_PATH='path/to/your/client.pem'
export TEMPORAL_TLS_CLIENT_KEY_PATH='path/to/your/client.key'
Connect with an API key
Set these environment variables with values from your Temporal Cloud API key settings:
export TEMPORAL_ADDRESS=<your-namespace>.<your-account-id>.tmprl.cloud:7233
export TEMPORAL_NAMESPACE=<your-namespace>.<your-account-id>
export TEMPORAL_API_KEY=<your-api-key>
Then run the Worker and starter code as shown in the Standalone Activities Quickstart.