Skip to content

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

Macros

Transforms an async fn into a durable execution handler. The generated code implements the necessary trait infrastructure so the worker can dispatch your function.

#[zart_durable("task-name")]
#[zart_durable("task-name", timeout = "30m")]
#[zart_durable("task-name", on_failure = my_failure_handler)]
#[zart_durable("task-name", timeout = "30m", on_failure = my_failure_handler)]
ParameterRequiredDescription
"task-name"YesRegistered name of the handler. Used by DurableScheduler::start_for to look up the handler.
timeoutNoExecution-level deadline. Duration string: 30s, 5m, 2h, 3d.
on_failureNoName of an async fn that handles execution-level failures.
#[zart_durable("my-task")]
async fn my_handler(data: MyInput) -> Result<MyOutput, TaskError>

The input type must implement DeserializeOwned and the output type must implement Serialize.

async fn handle_failure(
data: MyInput,
failure: ExecutionFailure,
) -> Result<MyOutput, TaskError>

The handler receives the original input and the failure details. It can return a compensated output (e.g., “payment failed, here’s your balance”) or re-propagate the error.

#[zart_durable("order-processing", on_failure = handle_order_failure)]
async fn process_order(data: OrderInput) -> Result<OrderOutput, TaskError> {
// ...
}
async fn handle_order_failure(
data: OrderInput,
failure: ExecutionFailure,
) -> Result<OrderOutput, TaskError> {
match failure {
ExecutionFailure::StepFailed { step, raw } if step == "charge-card" => {
// Deserialize raw into the step's typed error
match serde_json::from_value::<PaymentError>(raw) {
Ok(PaymentError::InsufficientFunds { balance, needed }) => {
Ok(OrderOutput { /* ... */ })
}
Err(_) => Ok(OrderOutput { /* generic failure */ }),
}
}
ExecutionFailure::ExecutionDeadlineExceeded => {
Ok(OrderOutput { status: "timed_out".into(), /* ... */ })
}
ExecutionFailure::RetriesExhausted { attempts } => {
Ok(OrderOutput { /* ... */ })
}
}
}
// Minimal
#[zart_durable("echo")]
async fn echo(data: String) -> Result<String, TaskError> {
Ok(format!("echo: {}", data))
}
// With timeout
#[zart_durable("timed-task", timeout = "5m")]
async fn timed_task(_data: ()) -> Result<(), TaskError> {
Ok(())
}
// With struct input
#[derive(Debug, Clone, Serialize, Deserialize)]
struct OrderData { id: u64, amount: f64 }
#[zart_durable("order-task")]
async fn order_handler(data: OrderData) -> Result<String, TaskError> {
Ok(format!("order-{}-{:.2}", data.id, data.amount))
}

Transforms an async fn into a type that implements ZartStep. The generated code implements IntoFuture so you call the function directly.

#[zart_step("step-name")]
#[zart_step("step-name", retry = "fixed(3, 1s)")]
#[zart_step("step-name", retry = "exponential(3, 1s)")]
#[zart_step("step-name", timeout = "5m")]
#[zart_step("step-name", timeout = "5m", timeout_scope = "global")]
#[zart_step("step-name", timeout = "30s", timeout_scope = "per_attempt")]
#[zart_step("step-name", retry = "...", timeout = "...")]
ParameterRequiredDescription
"step-name"YesStable, unique name within the execution.
retryNoRetry policy: fixed(n, delay) or exponential(n, delay).
timeoutNoStep-level deadline. Duration string: 30s, 5m, 2h, 3d. See Timeout Scope below.
timeout_scopeNoHow the timeout applies across retries: global (default) or per_attempt.

The timeout_scope attribute controls whether a step’s timeout is a global deadline (shared across all retry attempts) or a per-attempt countdown (fresh countdown on each attempt):

ValueBehavior
global (default)Deadline = first_attempt + duration. All retries share the same window. If the deadline has passed when a retry is picked up, the step immediately completes with TimedOut.
per_attemptEach retry attempt gets a fresh countdown. No deadline is persisted.

See Timeouts and Deadlines for a full conceptual walkthrough.

FormMeaning
fixed(3, 1s)3 retries, 1-second fixed delay between each
exponential(5, 2s)5 retries, exponential backoff starting at 2 seconds

Duration units: s (seconds), m (minutes), h (hours), d (days).

#[zart_step("my-step")]
async fn my_step(arg1: Type1, arg2: &str) -> Result<OutputType, ErrorType>

The return type must be Result<T, E> where T: Serialize and E: Serialize + std::error::Error.

Use {identifier} placeholders in the step name. Each placeholder is replaced with the value of the corresponding function parameter at runtime.

#[zart_step("process-report-{index}")]
async fn process_report(index: usize, report: Report) -> Result<ProcessedReport, StepError> {
// Produces: "process-report-0", "process-report-1", …
}
// Basic
#[zart_step("send-email")]
async fn send_email(to: &str) -> Result<(), StepError> {
mailer.send(to, "Hello").await
}
// With retries
#[zart_step("fetch-api", retry = "exponential(3, 2s)")]
async fn fetch_api() -> Result<Response, StepError> {
client.get("https://api.example.com").send().await
}
// With timeout
#[zart_step("call-service", timeout = "30s")]
async fn call_service() -> Result<String, StepError> {
external_client.call().await
}
// With both — global scope (default): 5 minutes total across all retries
#[zart_step("flaky-api", retry = "fixed(3, 1s)", timeout = "5m")]
async fn flaky_call() -> Result<Response, StepError> {
client.get("https://unreliable.example.com").send().await
}
// Per-attempt scope: each attempt gets a fresh 30 seconds
#[zart_step("call-service", timeout = "30s", timeout_scope = "per_attempt", retry = "fixed(3, 1s)")]
async fn call_service_per_attempt() -> Result<String, StepError> {
external_client.call().await
}
  • A unit struct named after your function (e.g., Onboarding for onboarding).
  • An impl DurableExecution with run() delegating to your function.
  • Registration with the TaskRegistry under the specified task name.
  • A struct named from your function (e.g., SendEmail for send_email) with fields for each argument.
  • impl ZartStep with step_name() returning the specified name.
  • impl IntoFuture so you call the function directly: send_email("a@b.com").await?.
  • retry_config(), timeout(), and timeout_scope() if the respective attributes are present.