Skip to content

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

Free Functions

All user-facing workflow operations are exposed as 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.

Execute a step and fail the execution on any non-Ok outcome.

pub async fn require<S>(s: S) -> Result<S::Output, TaskError>
where
S: ZartStep + Send,
S::Error: std::error::Error + Send + Sync + 'static,

This is the default path — use it when a step’s failure means the whole workflow cannot proceed.

let inventory = reserve_inventory(order_id.clone()).await?;

Both BusinessErr and ZartErr result in TaskError::StepFailed, preserving the step name for on_failure inspection.

Execute a step and return the three-way StepOutcome.

pub async fn step<S: ZartStep + Send>(
s: S,
) -> Result<StepOutcome<S::Output, S::Error>, StepError>

Use this when the handler body genuinely needs to branch on a specific step’s outcome.

match zart::step(check_balance(account_id.clone())).await? {
StepOutcome::Ok(b) => { /* ... */ }
StepOutcome::BusinessErr(PaymentError::InsufficientFunds { balance, needed }) => { /* ... */ }
StepOutcome::BusinessErr(PaymentError::CardDeclined { reason }) => { /* ... */ }
StepOutcome::ZartErr(e) => return Err(e.into()),
}

The outer ? propagates control-flow signals (Scheduled, StepExecuted) — never matched by user code.

Execute a step, returning default on any failure (business or framework).

pub async fn step_or<S: ZartStep + Send>(
s: S,
default: S::Output,
) -> Result<S::Output, StepError>

A blind fallback — the error is discarded. Use only when the specific failure reason does not matter.

let cached = zart::step_or(fetch_remote_config(), Config::default()).await?;

Execute a step, computing a fallback on business error only.

pub async fn step_or_else<S, F>(
s: S,
f: F,
) -> Result<S::Output, StepError>
where
S: ZartStep + Send,
F: FnOnce(S::Error) -> S::Output,

The closure receives S::Error only — ZartStepError (retry exhausted, timeout) is not passed to f and still propagates as a framework error. This is intentional: framework-level failures are not business decisions and should not be silently swallowed.

let payment = zart::step_or_else(
charge_card(account_id.clone(), data.amount),
|e: PaymentError| {
PaymentResult { transaction_id: "fallback".into(), amount: 0.0 }
},
)
.await?;

Register a step for parallel execution without waiting for it to complete.

pub fn schedule<S: ZartStep + Send + 'static>(
s: S,
) -> StepHandle<S::Output>

The returned handle can be passed to zart::wait to collect results.

let h1 = zart::schedule(check_service("auth-api".into()));
let h2 = zart::schedule(check_service("payments".into()));
let h3 = zart::schedule(check_service("users-db".into()));

Wait for all handles returned by zart::schedule to complete.

pub async fn wait<T>(
handles: Vec<StepHandle<T>>,
) -> Result<Vec<Result<T, StepError>>, StepError>
where
T: Serialize + for<'de> Deserialize<'de>,

Returns Ok(results) where each element corresponds to one handle in order. An individual step failure appears as Err(StepError) inside the Vec.

let results = zart::wait(vec![h1, h2, h3]).await?;
for result in results {
let svc = result?; // fail-fast on any step failure
println!("{}: {}", svc.name, svc.status);
}

Suspend execution for duration, resuming at now + duration.

pub async fn sleep(
name: &str,
duration: Duration,
) -> Result<(), StepError>

The name must be a stable, unique string within this execution body. Treat it like a migration name — do not change it after the execution has started.

zart::sleep("warehouse-settle", Duration::from_secs(60)).await?;

Suspend execution until an absolute UTC timestamp.

pub async fn sleep_until(
name: &str,
wake_time: chrono::DateTime<chrono::Utc>,
) -> Result<(), StepError>
let next_monday = compute_next_monday();
zart::sleep_until("wait-for-monday", next_monday).await?;

Wait for an external event to be delivered to this execution.

pub async fn wait_for_event<T: DeserializeOwned>(
name: &str,
timeout: Option<Duration>,
) -> Result<T, StepError>
let decision: ApprovalDecision = zart::wait_for_event(
"manager-approval",
Some(Duration::from_secs(86400)),
).await?;

See Steps → wait_for_event for the full walkthrough.

Capture a synchronous, pure value durably. On first body run: evaluates f(), writes the result as a completed step row, returns the value. On replay: returns the cached DB value; f is never called.

pub async fn capture<T, F>(
name: &str,
f: F,
) -> Result<T, StepError>
where
T: Serialize + for<'de> Deserialize<'de>,
F: FnOnce() -> T,
let started_at = zart::capture!("started-at", chrono::Utc::now());
// On replay: returns the cached DateTime — Utc::now() is never called again.

The macro form zart_capture! (re-exported as zart::capture!) expands to ::zart::capture(name, || expr).await?.

Shorthand for capture(name, chrono::Utc::now).

pub async fn now(
name: &str,
) -> Result<chrono::DateTime<chrono::Utc>, StepError>
let created_at = zart::now("created-at").await?;

Returns read-only information about the current execution. Callable from anywhere — handler body or step body.

pub fn context() -> ExecutionInfo
let info = zart::context();
println!("Execution: {}", info.execution_id);
println!("Task: {}", info.task_name);
println!("Attempt: {}", info.current_attempt);
println!("Is retry: {}", info.is_retry());
FieldTypeDescription
execution_idStringUnique identifier of this execution
task_nameStringRegistered name of the handler
dataserde_json::ValueThe original input payload (read-only)
current_attemptusize0-indexed retry count
max_retriesOption<usize>Maximum configured retries
is_retry()boolcurrent_attempt > 0