Skip to content

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

Parallel Steps

Zart supports running multiple steps in parallel via zart::schedule() and zart::wait(). Each scheduled step becomes an independent task in the scheduler — it can run on a different worker node and is fully durable.

use zart::prelude::*;
use zart::{zart_durable, zart_step};
#[zart_step("send-email")]
async fn send_email(email: String) -> Result<(), StepError> {
send_welcome_email(&email).await
}
#[zart_step("setup-billing")]
async fn setup_billing(email: String) -> Result<String, StepError> {
setup_stripe_customer(&email).await
}
#[zart_step("provision-resources")]
async fn provision_resources(user_id: String) -> Result<String, StepError> {
provision_aws(&user_id).await
}
#[zart_step("notify-ready")]
async fn notify_ready(email: String) -> Result<(), StepError> {
send_ready_notification(&email).await
}
#[zart_durable("onboarding")]
async fn onboarding(data: OnboardingData) -> Result<(), TaskError> {
// Schedule three parallel steps
let h1 = zart::schedule(send_email(data.email.clone()));
let h2 = zart::schedule(setup_billing(data.email.clone()));
let h3 = zart::schedule(provision_resources(data.user_id.clone()));
// Durably wait for all three to complete
let _results = zart::wait(vec![h1, h2, h3]).await?;
// This line only runs after all three succeed
notify_ready(data.email).await?;
Ok(())
}

Each zart::schedule() call:

  1. Inserts a new child task into zart_executions with a reference back to the parent.
  2. Returns a StepHandle — a token identifying that scheduled task.
  3. The child task can be picked up by any available worker, potentially running on a different machine.

zart::wait(handles):

  1. Marks the parent execution as waiting in the database.
  2. Returns the parent to the queue — no worker thread is blocked.
  3. When all child tasks complete, the scheduler re-queues the parent.
  4. On resumption, wait returns the collected results.

Each scheduled step returns its output through the zart::wait result vector, in the same order as the handles:

let h1 = zart::schedule(send_email(data.email.clone()));
let h2 = zart::schedule(setup_billing(data.email.clone()));
let results = zart::wait(vec![h1, h2]).await?;
// results[0] = output of send_email
// results[1] = output of setup_billing (the new customer_id String)

If any child task fails (returns Err), zart::wait returns that error. Other in-flight tasks are not cancelled — they run to completion (or failure), but their results are not returned to the parent.

let results = zart::wait(vec![h1, h2, h3]).await;
match results {
Ok(outputs) => { /* all succeeded */ }
Err(e) => {
// At least one child failed
// The parent can retry — already-completed children won't re-run
return Err(e);
}
}

Schedule a variable number of parallel tasks based on runtime data:

#[zart_step("process-{region}")]
async fn process_region(region: String) -> Result<RegionResult, StepError> {
run_regional_job(&region).await
}
#[zart_durable("multi-region")]
async fn multi_region(data: MultiRegionJob) -> Result<Vec<RegionResult>, TaskError> {
// Schedule one task per region dynamically
let handles: Vec<_> = data.regions
.iter()
.map(|region| zart::schedule(process_region(region.clone())))
.collect();
let raw_results = zart::wait(handles).await?;
let results: Vec<RegionResult> = raw_results
.into_iter()
.map(|r| r.map_err(|e| TaskError::StepFailed { step: "dynamic".into(), source: e }))
.collect::<Result<_, _>>()?;
Ok(results)
}

Parallel steps can themselves schedule further parallel steps, creating a tree of concurrent work. Each level is fully durable and can span multiple worker nodes.

Sequential (.await)Parallel (schedule + wait)
ExecutionIn-process, one after anotherPotentially cross-worker, concurrent
DurabilityYesYes
Result storageYesYes
Best forSteps that depend on each otherIndependent, parallelizable work
// Sequential — each step waits for the previous one to finish
let a = step_a().await?;
let b = step_b().await?;
let c = step_c().await?;
// Parallel — all three are registered first, then collected together
let h1 = zart::schedule(step_a());
let h2 = zart::schedule(step_b());
let h3 = zart::schedule(step_c());
let results = zart::wait(vec![h1, h2, h3]).await?;

The Parallel Steps example runs three concurrent service health checks and aggregates the results — a complete, runnable demonstration of zart::schedule and zart::wait.