Skip to main content
Version: 2.0 prerelease

Rust SDK

Use this guide for the general SDK surface, then continue to the generated API reference for individual types and methods. Evaluators enrolled in Durable Workflow Cloud controlled early access can also use the secondary Rust Cloud quickstart to run one workflow and activity from a fresh Sample App Codespace.

The first-party Rust SDK is a workflow-authoring surface, not only a protocol compatibility client. Rust authors deterministic workflows, activities, and long-running worker services against the same durable execution model used by PHP and Python. The async control-plane client starts, signals, queries, updates, cancels, terminates, and awaits executions; the worker runtime replays workflow history, runs workflow/activity/update handlers, reports worker and activity heartbeats, and exchanges language-neutral payloads with a self-hosted Server or Durable Workflow Cloud namespace runtime.

The Rust documentation landing page provides the SDK index and general entry points. For crate modules, structs, traits, and methods, continue to the generated Rust SDK API reference.

Cloud customers configure the provisioned namespace's runtime URL and use separate credentials for client and worker roles, even when both roles run in one process. See Cloud Managed Runtime for the managed connection boundary and the Cloud-specific connection example below.

The supported Rust prerelease supports durable timers, child workflows, activity retries and timeouts, signals, replayed query handlers, cancellation and termination, server-enforced workflow deadlines, typed side effects, version markers, updates, and typed terminal/replay failures. It does not yet claim schedule management. Use the 2.0 Capability Index instead of assuming every SDK has identical feature breadth.

This page belongs to the explicit 2.0 prerelease docs line. The Rust crate is published independently from the stable 1.x PHP documentation line and does not change which docs version is the public default.

Package and source

Install the Rust SDK from the last passing qualified tuple. The exact requirement is generated from the same machine-readable authority as the Server quickstart:

cargo add durable-workflow@=2.0.0-rc.7

Or declare the same qualified requirement directly in Cargo.toml:

[dependencies]
durable-workflow = "=2.0.0-rc.7"

The crate requires Rust 1.86 or newer. Its package metadata declares the exact qualified Durable Workflow Server range, worker protocol 1.2, and control plane 2. During deployment, the protocol manifests advertised by GET /api/cluster/info remain authoritative.

Server negotiates worker-protocol headers within major 1: a server advertising 1.N accepts a worker header 1.M only when M <= N. Rust SDK workers send X-Durable-Workflow-Protocol-Version: 1.2, so they require the same synchronized server train, which must also advertise worker protocol 1.2 or newer. The current server advertises 1.13, accepts the Rust header, and returns 1.13 in its response header and body.

Negotiation fails closed. A missing or malformed header, a different major, or a worker minor newer than the server's advertised minor is rejected. The server version range selects the release family; it does not override the runtime protocol manifest.

Prepare the released repository example

The repository's hello_world example registers a Rust worker, starts a workflow, sends a signal, runs an activity, reports an activity heartbeat, and waits for the completed result. Because that example runs the application client and worker in one process, a Cloud connection must give its Client both role-specific credentials instead of setting the generic token fallback.

Before running mode-specific commands, choose either Cloud or self-hosted Server. Both paths start from the same exact released SDK source, but their connection settings are alternatives: use the Cloud path with a provisioned namespace, or use the self-hosted path after the local Server quickstart.

For a reproducible source exercise, obtain the exact crate source recorded by the qualified tuple. The value below is generated from that machine-readable authority. The example directory is absolute so either connection path can enter it directly:

export DURABLE_WORKFLOW_RUST_VERSION=2.0.0-rc.7
export DURABLE_WORKFLOW_RUST_EXAMPLE_DIR="$PWD/durable-workflow-rust-${DURABLE_WORKFLOW_RUST_VERSION}"
git clone --depth 1 --single-branch --branch "$DURABLE_WORKFLOW_RUST_VERSION" \
https://github.com/durable-workflow/sdk-rust.git "$DURABLE_WORKFLOW_RUST_EXAMPLE_DIR"

Connect to Durable Workflow Cloud

Export the values returned when Cloud provisions the namespace and creates its two runtime credentials:

export DURABLE_WORKFLOW_RUNTIME_URL='https://your-runtime-url'
export DURABLE_WORKFLOW_RUNTIME_NAMESPACE='orders'
export DURABLE_WORKFLOW_CLIENT_TOKEN='dwr_client_credential'
export DURABLE_WORKFLOW_WORKER_TOKEN='dwr_worker_credential'

In $DURABLE_WORKFLOW_RUST_EXAMPLE_DIR/examples/hello_world.rs, replace the existing server_url, token, and Client::builder(...) setup with this split-token builder configuration:

let runtime_url = std::env::var("DURABLE_WORKFLOW_RUNTIME_URL")
.expect("DURABLE_WORKFLOW_RUNTIME_URL must be set");
let runtime_namespace = std::env::var("DURABLE_WORKFLOW_RUNTIME_NAMESPACE")
.expect("DURABLE_WORKFLOW_RUNTIME_NAMESPACE must be set");
let client_token = std::env::var("DURABLE_WORKFLOW_CLIENT_TOKEN")
.expect("DURABLE_WORKFLOW_CLIENT_TOKEN must be set");
let worker_token = std::env::var("DURABLE_WORKFLOW_WORKER_TOKEN")
.expect("DURABLE_WORKFLOW_WORKER_TOKEN must be set");

let client = Client::builder(runtime_url)
.namespace(runtime_namespace)
.control_token(Some(client_token))
.worker_token(Some(worker_token))
.build()?;

Then run the example normally:

cd "$DURABLE_WORKFLOW_RUST_EXAMPLE_DIR"
cargo run --example hello_world

The example's workflow start, signal, describe, and result calls use control_token; its Worker registration, polling, heartbeat, and completion calls use worker_token. Both roles use the same Cloud-provided runtime URL, runtime namespace, and task queue, but they do not reuse a credential. Do not replace either Cloud credential with .token(...): that method is the generic single-token fallback used by the self-hosted configuration below.

Run the combined example with self-hosted Server

The unmodified hello_world example accepts one token for a self-hosted Durable Workflow Server whose authentication policy allows the same credential to make workflow commands and poll for work:

cd "$DURABLE_WORKFLOW_RUST_EXAMPLE_DIR"
DURABLE_WORKFLOW_SERVER_URL=http://localhost:8080 \
DURABLE_WORKFLOW_TOKEN=dev-token \
cargo run --example hello_world

This DURABLE_WORKFLOW_TOKEN command is for self-hosted Server usage, not a Cloud namespace. The example's client uses the default namespace provisioned by the local Server quickstart. Use TASK_QUEUE to override its default rust-workers task queue.

Start with server-enforced workflow timeouts

The Rust SDK provides WorkflowStartOptions and Client::start_workflow_with_options for workflow deadlines that the server enforces even after the starting process exits. Execution timeout covers the whole workflow instance, including continue-as-new runs; run timeout covers one run and is recomputed when a new run begins.

use durable_workflow::{json, Client, Result, WorkflowStartOptions};

async fn start(client: &Client) -> Result<()> {
let handle = client.start_workflow_with_options(
"orders.await-payment",
"orders",
"order-42",
WorkflowStartOptions::new()
.execution_timeout_seconds(300)
.run_timeout_seconds(30),
json!([{"order_id": "order-42"}]),
).await?;

println!("workflow={} run={:?}", handle.workflow_id, handle.run_id);
Ok(())
}

Both values are seconds, must be positive, and the run timeout cannot exceed the execution timeout. The existing Client::start_workflow convenience method uses WorkflowStartOptions::default(): a 3600-second execution timeout and a 600-second run timeout. See Timeouts for the server's deadline and continue-as-new semantics.

These are workflow policy, not HTTP or result-polling timeouts. In particular, WorkflowResultOptions::timeout only stops the local result() call from waiting. It does not close, cancel, or otherwise change the workflow run. The caller can inspect the returned identity and wait again. An execution or run deadline configured with WorkflowStartOptions is durable server state; when it expires, the server closes the run with a terminal timed_out outcome.

Deterministic side effects and version markers

The Rust SDK records small non-deterministic values with WorkflowContext::side_effect and derives deterministic UUIDv4 values with WorkflowContext::uuid_v4. A cold replay decodes the recorded value instead of invoking the callback again; reordered, missing, duplicate, or codec-incompatible markers return a typed Error::NonDeterministicReplay.

Use WorkflowContext::get_version(change_id, min_supported, max_supported) to keep old and new workflow branches replay-compatible during a rollout. New runs record max_supported; existing runs reuse the durable marker. patched(change_id) provides the boolean rollout form, and deprecate_patch(change_id) preserves the marker after the legacy branch has drained. See Side Effects and Versioning for the shared durable semantics.

Cancel, terminate, and handle terminal outcomes

The 2.0 baseline separates cooperative cancellation from forced termination. Cancellation is the normal lifecycle operation when workflow and activity code should observe the stop request and clean up. Termination closes the run without waiting for that cleanup and should be reserved for an operator-enforced stop.

use durable_workflow::{Client, WorkflowCommandOptions};

# async fn cancel(client: &Client) -> durable_workflow::Result<()> {
client.cancel_workflow(
"order-42",
WorkflowCommandOptions::new()
.reason("customer withdrew the order")
.request_id("cancel-order-42"),
).await?;
# Ok(())
# }

Instance-targeted cancel_workflow and terminate_workflow resolve the current run on the server. For selected-run safety, call cancel_workflow_run or terminate_workflow_run, or use a handle's cancel_selected_run and terminate_selected_run methods. If a selected run is stale, Error::WorkflowCommandRejected exposes the stable historical_run_command_rejected reason together with workflow ID, run ID, target scope, HTTP status, and the response body.

Successful WorkflowHandle::result calls continue to return the decoded JSON value. Match the typed terminal variants for every other outcome. Branch on the stable reason and category fields instead of display text:

use durable_workflow::{Error, WorkflowHandle, WorkflowResultOptions};

# async fn wait(handle: WorkflowHandle) -> durable_workflow::Result<()> {
match handle.result(WorkflowResultOptions::default()).await {
Ok(value) => println!("completed: {value}"),
Err(Error::WorkflowCancelled(outcome)) => {
println!("cancelled {:?}: {}", outcome.run_id, outcome.reason);
}
Err(Error::WorkflowTerminated(outcome)) => {
println!("terminated: {}", outcome.reason);
}
Err(Error::WorkflowFailed(outcome)) => {
println!("failure {:?}: {:?}", outcome.failure_id, outcome.exception_class);
}
Err(Error::WorkflowTimedOut(outcome)) => match (
outcome.reason.as_str(),
outcome.failure_category.as_deref(),
) {
("result_wait_timeout", Some("client_timeout")) => {
println!(
"caller deadline for {} / {:?}; the run may still be open",
outcome.workflow_id, outcome.run_id,
);
}
("execution_timeout" | "run_timeout", category) => {
println!(
"server timeout for {} / {:?}: reason={} category={:?}",
outcome.workflow_id, outcome.run_id, outcome.reason, category,
);
}
(reason, category) => {
println!("other typed timeout: reason={reason} category={category:?}");
}
}
Err(error) => return Err(error),
}
# Ok(())
# }

Each terminal outcome carries workflow and run identity. It also retains the public reason, failure category and identity, exception type and class, non-retryable state, message, and exception payload when the server supplies them. A local wait deadline has reason result_wait_timeout and category client_timeout; a server timeout is a terminal timed_out run whose stable reason is execution_timeout or run_timeout.

Handles returned by either start method retain the selected run_id. WorkflowHandle::result describes that run-specific route, so reusing the same workflow ID for a newer run cannot make a wait silently report the newer run's outcome. Preserve both outcome.workflow_id and outcome.run_id in logs, metrics, and retry records; use instance-level lookups only when following the current run is intentional.

Workflow updates

Rust supports durable updates across application-client, selected-handle, and worker-authoring roles. Use Client::update_workflow or WorkflowHandle::update for JSON-compatible values, and the matching update_workflow_avro_value or update_avro_value methods for explicitly typed Avro values.

Workers register named handlers with Worker::register_update; use register_update_avro_value when the handler consumes and returns Avro values directly. An update is a durable, result-bearing workflow mutation. It is separate from fire-and-forget signals and read-only replayed queries.

Payload envelope

Workflow input, signals, activities, queries, and results use the published PayloadEnvelope contract: a codec plus encoded blob. The SDK's default avro path uses its declared apache-avro dependency and the platform's fixed versioned Value schema; do not hand-roll the blob or replace the envelope with an implementation-specific record.

use durable_workflow::{decode_payload, json, PayloadEnvelope, Result, Value};

fn round_trip() -> Result<()> {
let envelope = PayloadEnvelope::avro(&json!({"order_id": "order-42"}))?;
assert_eq!(envelope.codec, "avro");

let decoded: Value = decode_payload(&envelope)?;
assert_eq!(decoded["order_id"], "order-42");
Ok(())
}

start_workflow and start_workflow_with_options apply this envelope automatically to their serializable input. Use the public helpers only when a program needs to exchange an envelope directly.

Long-running activities should heartbeat and inspect should_stop(). On cancellation, release temporary files, connections, or other process-local resources and return promptly. A late completion is rejected by durable state and cannot convert a cancelled or terminated run into success; managed workers continue polling after that definitive rejection and after restart.

For server images, authentication, and production topology, continue with the server setup guide.