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.
The Three Layers
Section titled “The Three Layers”┌────────────────────────────────────────────────────────────────┐│ 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 │└────────────────────────────────────────────────────────────────┘Scheduler Layer
Section titled “Scheduler Layer”Responsible for persisting and claiming executions. Implements SKIP LOCKED polling so multiple workers can run concurrently without coordination.
| Type | Role |
|---|---|
Scheduler | Trait — poll for due tasks, mark complete/failed |
DurableScheduler | Trait — schedule new executions, deliver events |
PostgresScheduler | Concrete — PostgreSQL backend |
Worker | Drives the poll loop, dispatches to TaskRegistry |
Free Functions Layer
Section titled “Free Functions Layer”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.
Macro Layer
Section titled “Macro Layer”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.
| Macro | Purpose |
|---|---|
#[zart_durable] | Marks an async fn as a durable workflow |
#[zart_step] | Turns an async fn into a step struct with impl ZartStep |
Quick Example
Section titled “Quick Example”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}Free Functions Reference
Section titled “Free Functions Reference”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.
| Function | Signature | Description | See also |
|---|---|---|---|
zart::step | async 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::schedule | fn schedule<S: ZartStep>(s: S) -> StepHandle<S::Output> | Register a step for parallel execution without waiting. | Parallel Steps |
zart::wait | async fn wait<T>(handles: Vec<StepHandle<T>>) -> Result<Vec<Result<T, _>>, _> | Durably await all handles returned by schedule. | Parallel Steps |
zart::sleep | async fn sleep(name: &str, duration: Duration) -> Result<(), StepError> | Suspend execution for a fixed duration. name must be unique and stable. | Durable Loops |
zart::sleep_until | async fn sleep_until(name: &str, wake_time: DateTime<Utc>) -> Result<(), StepError> | Suspend until a specific UTC timestamp. | — |
zart::wait_for_event | async fn wait_for_event<T>(name: &str, timeout: Option<Duration>) -> Result<T, StepError> | Suspend until an external event is delivered. | Wait for Event |
zart::capture | async fn capture<T, F>(name: &str, f: F) -> Result<T, StepError> | Persist a pure synchronous value durably. | Capture Variables |
zart::now | async fn now(name: &str) -> Result<DateTime<Utc>, StepError> | Persist the current UTC time durably. Shorthand for capture(name, Utc::now). | Capture Variables |
zart::context | fn context() -> ExecutionInfo | Read-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.
Execution Lifecycle
Section titled “Execution Lifecycle”- Schedule —
DurableScheduler::start_for()inserts a row intozart_executionswith statuspending. - Claim — Worker polls with
SELECT … FOR UPDATE SKIP LOCKED. One worker owns one execution at a time. - Run —
DurableExecution::run()is called. Eachzart::step()call (or direct.awaiton a step) checkszart_tasksfor 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 tozart_tasks, then returned.
- Complete — execution status set to
completed, output stored. - Fail — if
runreturnsErr, execution is retried up tomax_retries(). Final failure sets statusfailed.
Next Steps
Section titled “Next Steps”- DurableExecution Trait — full reference for the workflow trait
- ZartStep Trait — defining durable steps
- Capture Variables — persist synchronous values durably
- Durable Loops — iterate over collections durably
- Parallel Steps — concurrent fan-out with
zart::scheduleandzart::wait - Wait for Event — external signals and human approvals