Macros
The proc-macros transform ordinary Rust async functions into full DurableExecution implementations and ZartStep step definitions — no trait boilerplate required. They are bundled with zart and re-exported directly, so no separate dependency is needed.
[dependencies]zart = "0.1" # macros are included — use zart::zart_durable, zart::zart_step, etc.#[zart_durable]
Section titled “#[zart_durable]”Annotate an async function to make it a durable workflow. The macro generates the DurableExecution impl and a unit struct named after the function in PascalCase.
use zart_macros::zart_durable;
#[zart_durable("checkout", timeout = "10m")]async fn checkout(order: Order) -> Result<Receipt, zart::error::TaskError> { // workflow body — use zart::* free functions for all operations Ok(Receipt { /* ... */ })}
// Macro generates `struct Checkout;` and `impl DurableExecution for Checkout`// Register with:// .register_durable_task("checkout", Checkout)// in the WorkerBuilder chainParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
"name" | string | yes | Task name used when scheduling |
timeout | duration string | no | Wall-clock timeout, e.g. "30m", "2h", "90s" |
The function must accept the workflow payload as its only parameter and return Result<T, TaskError> where T implements Serialize.
#[zart_step]
Section titled “#[zart_step]”Turn an async function into a durable step. The macro generates a step struct, implements ZartStep, and wires up IntoFuture so the step can be directly awaited.
use zart_macros::zart_step;
#[zart_step("charge-card", retry = "exponential(3, 2s)")]async fn charge_card(card: &str, amount: i64) -> Result<ChargeResult, StepError> { if zart::context().is_retry() { println!("Retry attempt #{}", zart::context().current_attempt); } stripe.charge(card, amount).await}Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
"name" | string | yes | Step name for database tracking |
retry | string | no | Retry config: "fixed(n, dur)" or "exponential(n, dur)" |
timeout | duration string | no | Per-step timeout, e.g. "5m", "30s" |
What the Macro generates
Section titled “What the Macro generates”For the charge_card example above, the macro produces:
ChargeCardStep<'_>struct — holds the parameters (card,amount)impl ZartStep— withstep_name(),retry_config(), andrun()(no ctx parameter)- Builder function —
fn charge_card(card, amount) -> ChargeCardStep<'_>(replaces the original fn) impl IntoFuture— delegates tozart::step(self), enabling direct.await
Calling a Step
Section titled “Calling a Step”Direct await (recommended):
let charge = charge_card(&card, amount).await?;The step builder returns a value that implements IntoFuture. Awaiting it calls zart::step() internally, which persists the result.
Via zart::step() explicitly — useful when passing the step to another function:
let charge = zart::step(charge_card(&card, amount)).await?;Via zart::schedule() for parallel execution:
let handle = zart::schedule(charge_card(&card, amount));// ... schedule other steps ...let results = zart::wait(vec![handle]).await?;Dynamic Step Names
Section titled “Dynamic Step Names”Use a {field} placeholder in the step name for loops or repeated steps:
#[zart_step("process-item-{index}")]async fn process_item(index: usize, item: Item) -> Result<Output, StepError> { // step_name() returns "process-item-0", "process-item-1", etc. transform(item).await}For call-site naming, use .named():
notify_user(user_id).named(format!("notify-{i}")).await?;See Durable Loops for detailed patterns.
Side-by-Side: Macros vs. Manual
Section titled “Side-by-Side: Macros vs. Manual”The two styles are fully equivalent. Choose the one that fits your team’s preferences.
With macros (recommended for most workflows):
use zart::prelude::*;use zart_macros::{zart_durable, zart_step};
#[zart_step("send-email")]async fn send_email(email: &str) -> Result<(), StepError> { send_welcome_email(email).await}
#[zart_step("create-stripe-customer", retry = "exponential(3, 2s)")]async fn create_customer(email: &str) -> Result<String, StepError> { create_stripe_customer(email).await}
#[zart_durable("onboarding", timeout = "50h")]async fn onboarding(data: OnboardingData) -> Result<OnboardingResult, TaskError> { send_email(&data.email).await?; let customer_id = create_customer(&data.email).await?; let _: VerifyPayload = zart::wait_for_event( "email-verified", Some(Duration::from_secs(172_800)), ).await?; Ok(OnboardingResult { customer_id })}Manually implementing the traits:
use zart::prelude::*;use async_trait::async_trait;use std::time::Duration;
struct SendEmailStep { email: String }
#[async_trait]impl ZartStep for SendEmailStep { type Output = (); fn step_name(&self) -> Cow<'static, str> { Cow::Borrowed("send-email") } async fn run(&self) -> Result<Self::Output, StepError> { send_welcome_email(&self.email).await }}
struct CreateCustomerStep { email: String }
#[async_trait]impl ZartStep for CreateCustomerStep { type Output = String; fn step_name(&self) -> Cow<'static, str> { Cow::Borrowed("create-stripe-customer") } fn retry_config(&self) -> Option<RetryConfig> { Some(RetryConfig::exponential(3, Duration::from_secs(2))) } async fn run(&self) -> Result<Self::Output, StepError> { create_stripe_customer(&self.email).await }}
pub struct OnboardingTask;
#[async_trait]impl DurableExecution for OnboardingTask { type Data = OnboardingData; type Output = OnboardingResult;
async fn run(&self, data: OnboardingData) -> Result<Self::Output, TaskError> { zart::step(SendEmailStep { email: data.email.clone() }).await?; let customer_id: String = zart::step(CreateCustomerStep { email: data.email.clone() }).await?; let _: VerifyPayload = zart::wait_for_event( "email-verified", Some(Duration::from_secs(172_800)), ).await?; Ok(OnboardingResult { customer_id }) }
fn max_retries(&self) -> usize { 3 } fn timeout(&self) -> Option<Duration> { Some(Duration::from_secs(180_000)) }}Duration Strings
Section titled “Duration Strings”Both #[zart_durable] and #[zart_step] accept human-readable duration strings:
| String | Duration |
|---|---|
"30s" | 30 seconds |
"5m" | 5 minutes |
"2h" | 2 hours |
"1d" | 1 day |