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.
// 1. Define your workflowstruct OnboardingTask;
#[async_trait]impl TaskHandler for OnboardingTask { type Data = OnboardingData; type Output = ();
async fn run(&self, ctx: &mut TaskContext<impl Scheduler>, data: OnboardingData) -> Result<(), TaskError> { // Each step runs exactly once — result is stored in the DB ctx.step("send-email", || async { send_welcome_email(&data.email).await }).await?;
// Retry up to 3× with exponential backoff if billing fails ctx.step_with_retry( "setup-billing", RetryConfig::exponential(3, Duration::from_secs(2)), || async { setup_stripe_customer(&data.email).await }, ).await?;
Ok(()) }}
// 2. Register and run a workerlet mut registry = TaskRegistry::new();registry.register("onboarding", OnboardingTask);
Worker::new(scheduler, registry, WorkerConfig::default()).run().await;
// 3. Schedule an execution (idempotent — safe to call twice with the same ID)scheduler.start_typed("signup-user-42", "onboarding", &OnboardingData { user_id: "u-42".into(), email: "alice@example.com".into(),}).await?;Prerequisites
Section titled “Prerequisites”- Rust 1.75 or later
- A supported database: PostgreSQL (recommended), SQLite (dev/embedded), or MySQL
tokioruntime
Installation
Section titled “Installation”[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"
# Pick one storage backend:zart-postgres = "0.1" # PostgreSQL# zart-sqlite = "0.1" # SQLite# zart-mysql = "0.1" # MySQLRun Migrations
Section titled “Run Migrations”export DATABASE_URL="postgres://user:pass@localhost/mydb"zart migrateThis creates the zart_executions, zart_tasks, and zart_events tables. Migrations are idempotent — safe to run on every deploy.
Start a Worker
Section titled “Start a Worker”use zart::{TaskRegistry, Worker, WorkerConfig};use zart_postgres::PostgresScheduler;
#[tokio::main]async fn main() -> anyhow::Result<()> { let pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL")?).await?; let scheduler = Arc::new(PostgresScheduler::new(pool));
let mut registry = TaskRegistry::new(); registry.register("onboarding", OnboardingTask);
Worker::new(scheduler, Arc::new(registry), WorkerConfig::default()) .run() .await;
Ok(())}Schedule an Execution
Section titled “Schedule an Execution”Call this from anywhere — a web handler, a CLI command, a test:
let durable = DurableScheduler::new(scheduler.clone(), registry.clone());
durable.start_typed( "signup-user-42", // idempotency key — safe to call twice "onboarding", &OnboardingData { user_id: "u-42".into(), email: "alice@example.com".into() },).await?;
// Optionally wait for it to finishlet status = durable.wait("signup-user-42", None).await?;Next Steps
Section titled “Next Steps”- TaskHandler Trait — full reference for
ctx.step(), retries, sleeps, and events - Macros — the
#[zart_durable]proc-macro layer for less boilerplate - Parallel Steps — run steps concurrently with
wait_all - Wait for Event — human-in-the-loop and webhook patterns
- Deployment Options — embedded, sidecar, and CLI modes