Skip to content

Zart is in active development — breaking API changes may occur despite our best efforts to keep contracts stable.

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.

  • Rust 1.75 or later
  • A PostgreSQL 14+ database
  • A tokio runtime
Cargo.toml
[dependencies]
zart = "0.1"
zart-macros = "0.1" # optional — ergonomic proc-macro layer
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
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:

Terminal window
just migrate

Step 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.

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?;

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?;

The repository ships with runnable examples:

Terminal window
# Start PostgreSQL
docker compose up -d
# Run migrations
just migrate
# Try the sleep example
just example-sleep
# Try the brewery finder
just example-brewery

See the full Examples section for detailed walkthroughs.