Macros
#[zart_durable]
Section titled “#[zart_durable]”Transforms an async fn into a durable execution handler. The generated code implements the necessary trait infrastructure so the worker can dispatch your function.
Syntax
Section titled “Syntax”#[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)]Parameters
Section titled “Parameters”| Parameter | Required | Description |
|---|---|---|
"task-name" | Yes | Registered name of the handler. Used by DurableScheduler::start_for to look up the handler. |
timeout | No | Execution-level deadline. Duration string: 30s, 5m, 2h, 3d. |
on_failure | No | Name of an async fn that handles execution-level failures. |
Function Signature
Section titled “Function Signature”#[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.
The on_failure Handler
Section titled “The on_failure Handler”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 { /* ... */ }) } }}Examples
Section titled “Examples”// 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))}#[zart_step]
Section titled “#[zart_step]”Transforms an async fn into a type that implements ZartStep. The generated code implements IntoFuture so you call the function directly.
Syntax
Section titled “Syntax”#[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 = "...")]Parameters
Section titled “Parameters”| Parameter | Required | Description |
|---|---|---|
"step-name" | Yes | Stable, unique name within the execution. |
retry | No | Retry policy: fixed(n, delay) or exponential(n, delay). |
timeout | No | Step-level deadline. Duration string: 30s, 5m, 2h, 3d. See Timeout Scope below. |
timeout_scope | No | How the timeout applies across retries: global (default) or per_attempt. |
Timeout Scope
Section titled “Timeout Scope”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):
| Value | Behavior |
|---|---|
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_attempt | Each retry attempt gets a fresh countdown. No deadline is persisted. |
See Timeouts and Deadlines for a full conceptual walkthrough.
Retry Syntax
Section titled “Retry Syntax”| Form | Meaning |
|---|---|
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).
Function Signature
Section titled “Function Signature”#[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.
Dynamic Step Names
Section titled “Dynamic Step Names”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", …}Examples
Section titled “Examples”// 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}What the Macros Generate
Section titled “What the Macros Generate”#[zart_durable] generates:
Section titled “#[zart_durable] generates:”- A unit struct named after your function (e.g.,
Onboardingforonboarding). - An
impl DurableExecutionwithrun()delegating to your function. - Registration with the
TaskRegistryunder the specified task name.
#[zart_step] generates:
Section titled “#[zart_step] generates:”- A struct named from your function (e.g.,
SendEmailforsend_email) with fields for each argument. impl ZartStepwithstep_name()returning the specified name.impl IntoFutureso you call the function directly:send_email("a@b.com").await?.retry_config(),timeout(), andtimeout_scope()if the respective attributes are present.
Next Steps
Section titled “Next Steps”- Free Functions — all
zart::free functions - Steps — conceptual walkthrough for steps and retries
- Error Handling — the three-way outcome model