Durable Workflow Rust SDK
durable-workflow is the first-party Rust SDK for Durable Workflow workers
and clients. It can register workflow and activity handlers, long-poll the
worker protocol, start, signal, and query workflow executions, start and await
durable child workflows, expose named read-only query handlers, heartbeat
workers and activities, and exchange JSON-native payloads through the
platform's generic Avro wrapper. Workflow code can also wait on server-backed
durable time without blocking a Rust executor thread.
Install
Add the exact crates.io release with Cargo:
Or add the same exact requirement directly to Cargo.toml:
[]
= "=0.1.6"
Version 0.1.6 requires Rust 1.86 or newer. Snapshot inspection queries were
introduced in 0.1.1; replayed workflow-instance state queries are available
from 0.1.2, deterministic durable timers are available from 0.1.4, and
durable child workflows are available from 0.1.5.
Compatibility
| SDK releases | Durable Workflow server | Worker protocol | Control plane |
|---|---|---|---|
0.1.0 |
>=0.2,<0.3 |
1.2 |
2 |
0.1.1 |
>=0.2,<0.3 |
1.2 (snapshot queries require 1.8) |
2 |
0.1.2–0.1.3 |
>=0.2,<0.3 |
1.2 (replayed queries require 1.8) |
2 |
0.1.4 |
>=0.2,<0.3 |
1.2 (timers; replayed queries require 1.8) |
2 |
0.1.5+ |
>=0.2,<0.3 |
1.2 (timers and child workflows; replayed queries require 1.8) |
2 |
The machine-readable values live in [package.metadata.durable-workflow] in
Cargo.toml as supported-server-versions, worker-protocol-version, and
control-plane-version. Query-capable releases also publish query-tasks,
query-task-minimum-worker-protocol-version, replayed-instance-state-queries,
query-state-model, snapshot-inspection-queries, and payload-codecs.
Timer-capable releases additionally publish durable-timers, timer-command,
and timer-replay-validation. Child-capable releases additionally publish
child-workflows, child-workflow-command, and
child-workflow-failure-reasons. Existing worker operations retain the 1.2
baseline; only query-task poll, complete, and fail requests use the additive
1.8 feature floor. The server's advertised protocol manifests remain
authoritative when checking compatibility during deployment.
Durable timers
WorkflowContext::sleep waits on server-backed durable wall time. It emits a
start_timer workflow command and yields the workflow future; it never calls
tokio::time::sleep or keeps a process-local deadline. Durations are rounded
up to whole seconds, so a timer cannot be scheduled earlier than requested.
WorkflowContext::start_timer is an alias for sleep. On every workflow task,
the SDK reconstructs one shared sequence-ordered command stream for activities,
timers, and signal waits. A replayed timer must have a matching
TimerScheduled event, and it resolves only when the same sequence and timer
identity has one TimerFired event. The recorded delay must equal the current
workflow call. Worker and server restarts therefore preserve the original
deadline, while replay and repeated polling do not append or consume the timer
twice. Changed command order, changed delay, unpaired events, duplicate durable
sequences, and duplicate fires return Error::NonDeterministicReplay; its
ReplayFailure exposes stable reason, sequence, expected, and actual
fields. Durations too large to round up without shortening the requested wait
return Error::TimerDurationOverflow without emitting a command.
# use Duration;
# use ;
#
Use WorkflowContext::sleep only inside workflow code. Ordinary
tokio::time::sleep remains appropriate for worker-process concerns such as
local polling or shutdown coordination, but it is not durable workflow state.
Running and completed executions remain available through WorkflowHandle's
describe, query, and result methods. Server protocol incompatibilities
remain Error::Protocol(ProtocolFailure), including stable reason, status,
and requested/supported version fields; other rejected worker requests remain
typed Error::Http values with the response status and body.
Child workflows
WorkflowContext::start_child_workflow records a named child on a mandatory,
explicit task queue and waits for a terminal ChildRun* history event. The
same history is replayed after a Rust worker restart: a committed result is
decoded using its recorded payload codec and no second child command is
emitted. A recorded child that has not settled remains pending without another
start command on redelivery. This also permits a Rust parent to call a PHP or
Python child (or the reverse) without changing payload shape.
# use ;
# async
Success returns ChildWorkflowResult, including parent and child workflow/run
identities. Failure, cancellation, and termination return
Error::ChildWorkflowFailed(ChildWorkflowFailure) inside workflow code. Match
its kind or stable reason (child_workflow, cancelled, or terminated),
not its message. An uncaught error becomes a durable fail_workflow command
whose structured exception retains those fields.
ParentClosePolicy::Abandon leaves an open child running when the parent
closes. RequestCancel requests child cancellation, and Terminate closes it
immediately. Retry and timeout options are recorded server-side with the child
call; they are not SDK HTTP retry limits.
Worker
use ;
async
Client
# use ;
# async
Queries
Worker::register_replayed_workflow gives ordinary workflow execution a typed
WorkflowInstance<S>. Put transitions after activity and signal resolution in
that workflow closure. Worker::register_replayed_query re-runs the same
closure over committed durable history, then invokes the named query with an
immutable, detached Arc<S>. This is the recommended workflow-instance query
API: query code does not parse history or duplicate transition logic.
Replay-generated commands are discarded. A query handler has no command API, and its detached state is never retained, so successful and failed queries do not append history, advance execution, or change a later query. The same query serves running, restarted, and successfully completed workflows:
# use ;
#
#
#
Worker::register_query remains the lower-level snapshot-inspection API. Its
QueryContext exposes normalized workflow input, raw committed history, and
decoded signals. Use it for transport-level inspection when replayed typed
state is not appropriate; snapshot handlers must reduce history themselves and
are not workflow-instance query parity.
Client-side rejections are Error::QueryFailed(QueryFailure). Match the
public reason and status fields for automation; the original response is
retained in body. Stable reasons include rejected_unknown_query,
invalid_query_arguments, query_handler_unavailable,
query_payload_decode_failed, query_workflow_state_unavailable, and
query_worker_unavailable. Protocol negotiation failures use
Error::Protocol(ProtocolFailure) and retain supported/requested versions.
Heartbeats
Worker::run and Worker::run_until register the worker and then send worker
heartbeats automatically. The registration response supplies the preferred
cadence; Worker::heartbeat_interval is the fallback when the server does not
advertise one. Use Worker::on_worker_heartbeat to observe successful server
acknowledgements for metrics or structured logging.
Activity handlers report progress with ActivityContext::heartbeat. The
returned ActivityHeartbeatResponse exposes heartbeat_recorded and
cancel_requested so long-running work can respond to server state:
# use ;
#
Lower-level integrations can call Client::heartbeat_worker and
Client::heartbeat_activity_task directly.
Worker liveness and errors
Workflow, activity, and query polls advertise the configured poll timeout to
the server. An empty response at that boundary is normal: Worker::run and
Worker::run_until keep every poller and worker heartbeats running, so the same
worker can accept work after an idle period.
Replaying a workflow that is still blocked on a recorded activity, timer, child workflow, or signal wait can also produce no new commands. The worker acknowledges that task as waiting for scheduled history instead of submitting an invalid empty completion. Workflow and query pollers therefore remain live, and worker heartbeats continue while unrelated signals are recorded.
Poll acquisition and worker-heartbeat transport failures, HTTP 408/429
responses, and server errors use capped exponential backoff. Configure the
bound with Worker::retry_policy; the default retries five times from 100 ms
up to 5 seconds. Retries wrap only acquisition and heartbeat requests, never a
leased task's handler or settlement request, so an ambiguous completion is not
re-executed by the retry loop. Once the retry bound is exhausted, the transport
or HTTP error is returned.
Authentication failures remain Error::Http with their status and response
body, and protocol incompatibilities remain
Error::Protocol(ProtocolFailure) with stable reason and version fields.
Codec, handler, and other non-retryable failures are returned immediately and
are never retried indefinitely.
Example
examples/hello_world.rs contains a complete round trip: it registers a Rust
worker, starts a workflow, sends a signal, runs an activity, heartbeats that
activity, exposes a named query, and waits for the completed result.
With a Durable Workflow server running locally:
DURABLE_WORKFLOW_SERVER_URL=http://127.0.0.1:8080 \
DURABLE_WORKFLOW_TOKEN=your-token \
TASK_QUEUE optionally overrides the default rust-workers task queue.
API documentation
The complete API reference is published at
rust.durable-workflow.com. Documentation
for main is rebuilt and deployed automatically.
Ownership and versioning
The Durable Workflow project owns and maintains the crate. This repository is
the authoritative source for the durable-workflow crate and its Rust API
documentation.
Crate releases follow semantic versioning and are tagged with the exact crate
version, such as 0.1.1. Rust SDK versions are independent from Durable
Workflow server image versions. A compatible server range is declared in
package metadata instead of coupling crate publication to a server release.