Getting Started
Here is a complete durable workflow in Zart. Every step persists its result — if your process dies and restarts, completed steps are skipped and the workflow continues from where it stopped.
use zart::{zart_durable, zart_step};use zart::prelude::*;
// 1. Define steps as plain async functions#[zart_step("send-email", retry = "exponential(3, 2s)")]async fn send_email(email: &str) -> Result<(), StepError> { mailer.send(email, "Welcome!").await}
#[zart_step("setup-billing")]async fn setup_billing(email: &str) -> Result<CustomerId, StepError> { stripe.create_customer(email).await}
// 2. Compose steps inside a durable handler — just call and await#[zart_durable("onboarding")]async fn onboarding(data: OnboardingData) -> Result<(), TaskError> { send_email(&data.email).await?; // ✓ persisted, retried on failure setup_billing(&data.email).await?; // ✓ persisted Ok(())}No context objects to thread through your code. Steps look and feel like plain async functions — they just happen to be durable.
Prerequisites
Section titled “Prerequisites”- Rust 1.75 or later
- A PostgreSQL 14+ database
- A
tokioruntime
Step 1: Add Dependencies
Section titled “Step 1: Add Dependencies”[dependencies]zart = "0.1"zart-macros = "0.1" # optional — ergonomic proc-macro layerasync-trait = "0.1"tokio = { version = "1", features = ["full"] }serde = { version = "1", features = ["derive"] }serde_json = "1"sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }Step 2: Run Migrations
Section titled “Step 2: Run Migrations”use sqlx::PgPool;
let pool = PgPool::connect(&std::env::var("DATABASE_URL")?).await?;let sched = Arc::new(PostgresScheduler::new(pool));sched.run_migrations().await?;This creates the zart_tasks, zart_execution_runs, and zart_events tables. Migrations are idempotent — safe to run on every deploy.
You can also run them from the CLI:
just migrateStep 3: Register Handlers and Start a Worker
Section titled “Step 3: Register Handlers and Start a Worker”use zart::prelude::*;use zart::registry::TaskRegistry;use zart::worker::Worker;
#[tokio::main]async fn main() -> anyhow::Result<()> { let pool = PgPool::connect(&std::env::var("DATABASE_URL")?).await?; let sched = Arc::new(PostgresScheduler::new(pool)); sched.run_migrations().await?;
let mut registry = TaskRegistry::new(); registry.register("onboarding", Onboarding);
let config = zart::WorkerConfig { poll_interval: Duration::from_millis(200), max_tasks_per_poll: 10, max_concurrent_tasks: 4, shutdown_timeout: Duration::from_secs(5), orphan_timeout: Duration::from_secs(30), ..Default::default() };
let worker = Arc::new(Worker::new(sched.clone(), Arc::new(registry), config)); let w = worker.clone(); tokio::spawn(async move { w.run().await });
Ok(())}The worker polls PostgreSQL for due tasks and dispatches them to registered handlers. Multiple worker instances can run against the same database — SKIP LOCKED ensures each task is processed by exactly one worker.
Step 4: Schedule an Execution
Section titled “Step 4: Schedule an Execution”Call this from anywhere — a web handler, a background task, a test:
let durable = DurableScheduler::new(sched.clone());
durable .start_for::<OnboardingTask>( "signup-user-42", // idempotency key — safe to call twice "onboarding", &OnboardingData { email: "alice@example.com".into() }, ) .await?;Step 5: Wait for Completion (Optional)
Section titled “Step 5: Wait for Completion (Optional)”The simplest approach is wait_completion, which blocks until the execution completes and automatically deserializes the result to your output type:
let output: MyOutput = durable .wait_completion("signup-user-42", Duration::from_secs(60), None) .await?;
println!("Workflow completed with output: {:?}", output);No manual serde_json::from_value needed — the type parameter tells Zart exactly what to expect.
You can also combine start and wait into a single call:
let output = durable .start_and_wait_for::<OnboardingTask>("signup-user-42", "onboarding", &input, Duration::from_secs(60)) .await?;Run the Examples
Section titled “Run the Examples”The repository ships with runnable examples:
# Start PostgreSQLdocker compose up -d
# Run migrationsjust migrate
# Try the sleep examplejust example-sleep
# Try the brewery finderjust example-brewerySee the full Examples section for detailed walkthroughs.
Next Steps
Section titled “Next Steps”- Durable Execution — the core concept, lifecycle, and idempotency
- Steps — defining steps, retries, sleeps, events, and durable values
- Flow Control — parallel steps and durable loops
- Error Handling — the three-way outcome model