Guide for building reliable, fault-tolerant Rust applications with DBOS durable workflows.
When to Apply
Reference these guidelines when:
- Adding DBOS to existing Rust code
- Creating workflows and steps
- Running DBOS calls concurrently inside a workflow
- Using queues for concurrency control
- Implementing workflow communication (events, messages, streams)
- Configuring and launching DBOS applications
- Using the DBOS Client from external applications
- Testing DBOS applications
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Lifecycle | CRITICAL | lifecycle- |
| 2 | Workflow | CRITICAL | workflow- |
| 3 | Step | HIGH | step- |
| 4 | Queue | HIGH | queue- |
| 5 | Communication | MEDIUM | comm- |
| 6 | Pattern | MEDIUM | pattern- |
| 7 | Testing | LOW-MEDIUM | test- |
| 8 | Client | MEDIUM | client- |
| 9 | Advanced | LOW | advanced- |
Critical Rules
Installation
Add the dbos crate (0.6) and Tokio:
cargo add dbos
cargo add tokio --features full
cargo add serde --features derive
cargo add thiserror tracing-subscriber futures
cargo add --dev uuid --features v4
serde is for workflow inputs and outputs, thiserror for workflow error types, tracing-subscriber to see DBOS's logs, futures for joining DBOS calls (try_join_all), and uuid for fresh workflow IDs in tests.
DBOS runs on Tokio by default; smol and async-global-executor are Cargo features (see references/lifecycle-runtimes.md). DBOS for Rust does not support DBOS Conductor or DBOS Cloud.
DBOS Configuration and Launch
A DBOS application MUST create a DBOS instance, register every workflow, and launch before running any workflows:
use dbos::{Config, DBOS};
async fn my_workflow(_: ()) -> dbos::Result<String> {
Ok("done".to_owned())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
// Reads the system database URL from DBOS_SYSTEM_DATABASE_URL.
let mut config = Config::from_env("my-app");
config.app_version = Some("0.1.0".to_owned());
let dbos = DBOS::new(config);
// Register every workflow before launch.
let workflow = dbos.register_workflow("my_workflow", my_workflow)?;
dbos.launch().await?;
let result = workflow.run(()).await?;
println!("{result}");
dbos.shutdown().await;
Ok(())
}
An application version is REQUIRED: DBOS for Rust doesn't compute one, so set config.app_version (or DBOS__APPVERSION) or launch fails. When creating a new application, set it to "0.1.0" or env!("CARGO_PKG_VERSION"). When editing an existing application, leave its configured version alone: changing it is a deployment decision (see references/advanced-application-versions.md).
Config::from_env(app_name) reads the system database URL from DBOS_SYSTEM_DATABASE_URL; DBOS creates the database if it doesn't exist. See references/lifecycle-config.md for all configuration options.
DBOS is cheap to clone. Outside a workflow, call its methods (dbos.send, dbos.cancel, dbos.list_workflows, queue and schedule management, ...); queue, schedule and app-version management have no free-function forms; from another application, use a Client (see references/client-setup.md).
Workflow and Step Structure
A workflow is an async function of one input that returns a dbos::Result. Any function performing complex operations or accessing external services must run as a step with dbos::step:
async fn generate_random_number(n: u32) -> dbos::Result<u32> {
Ok(rand::random_range(0..n))
}
async fn workflow_function(n: u32) -> dbos::Result<u32> {
// The closure passes arguments into the step function.
let random_number = step("generate_random_number", || generate_random_number(n)).await?;
Ok(random_number)
}
Inputs, outputs and error types must implement serde::Serialize and serde::Deserialize; an application error type must also implement std::error::Error (see references/workflow-errors.md).
Key Constraints
- Workflows MUST be deterministic: non-deterministic operations go in steps
- Do NOT race, time out, spawn, or consume in completion order any DBOS call in a workflow body: no
tokio::select!,tokio::time::timeout,FuturesUnordered,buffer_unorderedortokio::spawnaround DBOS calls. Usedbos::select_step!to race,tokio::join!/futures::future::try_join_allto run concurrently, anddbos::sleepto wait (seereferences/workflow-concurrency.md) - Every DBOS call takes its step ID where it is built, not where it is awaited;
dbos::patch,dbos::deprecate_patchand a debouncer'sdebounceare the exceptions, so await those on their own, never inside a join - A workflow's future must be
Send + 'static: don't hold anRc, astd::sync::MutexGuardor a borrow across an.awaitin a workflow body - To start a child workflow, capture its
WorkflowRefin the parent's registered closure; aWorkflowRefcan't be a workflow's input - Do NOT start or enqueue workflows, or call
dbos::recv,dbos::set_eventordbos::patch, from within steps (Error::InsideStep) - Do NOT capture the
DBOSinstance in a workflow closure: use the free functions (dbos::send,dbos::cancel_workflow, ...), which need none (seereferences/workflow-capture.md) - All workflows MUST be registered before
launch; queues are registered and schedules created after it - Do NOT modify global variables from workflows or steps
- Starting a workflow with an ID already in use returns the existing workflow by default; set
workflow_id_reuse_policy: WorkflowIdReusePolicy::Rejectto getdbos::sysdb::Error::WorkflowIdInUseinstead (seereferences/pattern-idempotency.md) - A panic in a workflow does not crash the process: the workflow stays
PENDING, like a crash, and is recovered on the next launch - Steps are at-least-once and there is no transactional step: make database writes in steps idempotent
How to Use
Read individual rule files for detailed explanations and examples:
references/lifecycle-config.md
references/workflow-determinism.md
references/workflow-concurrency.md
references/queue-concurrency.md
references/workflow-control.md