Skip to content

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

Rust API Overview

Zart’s Rust API is organized into three layers that build on each other. You can use all three together, or drop down to a lower layer when you need more control.

┌────────────────────────────────────────────────────────────────┐
│ Macro Layer #[zart_durable] #[zart_step] │
│ (zart-macros) Ergonomic async fn → DurableExecution │
│ Standalone step functions │
├────────────────────────────────────────────────────────────────┤
│ Free Functions zart::step zart::schedule zart::wait │
│ (zart) zart::sleep zart::wait_for_event … │
│ All workflow operations — no ctx threading │
├────────────────────────────────────────────────────────────────┤
│ Scheduler Layer Scheduler DurableScheduler Worker │
│ (zart + backend) PostgreSQL polling via SKIP LOCKED │
└────────────────────────────────────────────────────────────────┘

Responsible for persisting and claiming executions. Implements SKIP LOCKED polling so multiple workers can run concurrently without coordination.

TypeRole
SchedulerTrait — poll for due tasks, mark complete/failed
DurableSchedulerTrait — schedule new executions, deliver events
PostgresSchedulerConcrete — PostgreSQL backend
WorkerDrives the poll loop, dispatches to TaskRegistry

All user-facing workflow operations are free functions under the zart:: namespace. There is no ctx to thread through your code — the framework uses task-local storage to make the current execution context available wherever you are in the call stack.

See the full reference table below.

Optional. The zart-macros crate provides proc-macros that transform an ordinary async fn into a full DurableExecution implementation, removing the boilerplate of the trait impl.

MacroPurpose
#[zart_durable]Marks an async fn as a durable workflow
#[zart_step]Turns an async fn into a step struct with impl ZartStep
use zart::prelude::*;
use zart::{zart_durable, zart_step};
// One attribute turns a plain async fn into a durable step.
#[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<String, StepError> {
billing.create_customer(email).await
}
// The workflow body looks like ordinary async Rust.
// Every .await? is a durable checkpoint — results are persisted,
// and a crashed process resumes exactly where it left off.
#[zart_durable("onboarding", timeout = "10m")]
async fn onboarding(data: OnboardingData) -> Result<(), TaskError> {
send_email(&data.email).await?; // durable, with retries
setup_billing(&data.email).await?; // durable
Ok(())
}

No new invocation syntax, no context objects to thread through your code. send_email and setup_billing look and feel like plain async functions — they just happen to be durable. Add #[zart_step], call them normally, and Zart handles persistence, replay, and retries.

// 3. Register and start a worker
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let scheduler = PostgresScheduler::connect(&std::env::var("DATABASE_URL")?).await?;
let mut registry = TaskRegistry::new();
registry.register("onboarding", Onboarding);
let worker = Worker::new(scheduler.clone(), registry, WorkerConfig::default());
// Schedule an execution
DurableScheduler::start_for::<Onboarding>(&scheduler, "run-1", "onboarding", &OnboardingData { /* ... */ }).await?;
// Run the worker (blocks until shutdown signal)
worker.run().await
}

All functions are available in the zart crate and work from within any durable handler body. zart::context() is additionally callable from inside a step body.

FunctionSignatureDescriptionSee also
zart::stepasync fn step<S: ZartStep>(s: S) -> Result<S::Output, StepError>Execute a step and persist its result. Steps awaited directly via IntoFuture call this internally.
zart::schedulefn schedule<S: ZartStep>(s: S) -> StepHandle<S::Output>Register a step for parallel execution without waiting.Parallel Steps
zart::waitasync fn wait<T>(handles: Vec<StepHandle<T>>) -> Result<Vec<Result<T, _>>, _>Durably await all handles returned by schedule.Parallel Steps
zart::sleepasync fn sleep(name: &str, duration: Duration) -> Result<(), StepError>Suspend execution for a fixed duration. name must be unique and stable.Durable Loops
zart::sleep_untilasync fn sleep_until(name: &str, wake_time: DateTime<Utc>) -> Result<(), StepError>Suspend until a specific UTC timestamp.
zart::wait_for_eventasync fn wait_for_event<T>(name: &str, timeout: Option<Duration>) -> Result<T, StepError>Suspend until an external event is delivered.Wait for Event
zart::captureasync fn capture<T, F>(name: &str, f: F) -> Result<T, StepError>Persist a pure synchronous value durably.Capture Variables
zart::nowasync fn now(name: &str) -> Result<DateTime<Utc>, StepError>Persist the current UTC time durably. Shorthand for capture(name, Utc::now).Capture Variables
zart::contextfn context() -> ExecutionInfoRead-only execution metadata. Callable from handler body and step body.

ExecutionInfo fields: execution_id: String, task_name: String, data: serde_json::Value, current_attempt: usize, max_retries: Option<usize>, and fn is_retry() -> bool.

  1. ScheduleDurableScheduler::start_for() inserts a row into zart_executions with status pending.
  2. Claim — Worker polls with SELECT … FOR UPDATE SKIP LOCKED. One worker owns one execution at a time.
  3. RunDurableExecution::run() is called. Each zart::step() call (or direct .await on a step) checks zart_tasks for an existing result.
    • Step hit — stored result deserialized and returned without calling the step logic.
    • Step miss — step’s run() is called, result serialized and written to zart_tasks, then returned.
  4. Complete — execution status set to completed, output stored.
  5. Fail — if run returns Err, execution is retried up to max_retries(). Final failure sets status failed.