Retry Simulation
This example simulates a transient failure to show how Zart automatically retries steps. The first attempt always fails; the framework retries and the second attempt succeeds. All retry metadata is accessible inside the step via zart::context().
Features demonstrated: #[zart_step] with retry = "fixed(n, delay)", zart::context().current_attempt, zart::context().is_retry(), zart::context().max_retries, DurableExecution trait.
Data types
Section titled “Data types”#[derive(Debug, Clone, Serialize, Deserialize)]struct RetrySimulationInput { name: String,}
#[derive(Debug, Clone, Serialize, Deserialize)]struct RetrySimulationOutput { name: String, total_attempts: usize, message: String, attempts_log: Vec<String>,}
#[derive(Debug, Clone, Serialize, Deserialize)]struct RetryStepResult { message: String, attempt_number: usize,}Step definitions
Section titled “Step definitions”use zart::prelude::*;use zart::zart_step;
/// Fails on attempt 0; succeeds on any retry./// `retry = "fixed(3, 1s)"` means up to 3 retries, 1 second apart.#[zart_step("intentional-failure", retry = "fixed(3, 1s)")]async fn intentional_failure_step(name: String) -> Result<RetryStepResult, StepError> { let ctx = zart::context(); println!( "[intentional-failure] Attempt #{} | is_retry={} | max_retries={:?}", ctx.current_attempt, ctx.is_retry(), ctx.max_retries, );
if ctx.current_attempt == 0 { return Err(StepError::Failed { step: "intentional-failure".into(), reason: format!("Simulated transient error on attempt {}", ctx.current_attempt + 1), }); }
Ok(RetryStepResult { message: format!("Succeeded for '{}' on retry attempt #{}", name, ctx.current_attempt), attempt_number: ctx.current_attempt, })}
/// A step that always succeeds — no retry configuration needed.#[zart_step("normal-step")]async fn normal_step(name: String) -> Result<String, StepError> { Ok(format!("Normal step completed for '{}'", name))}The workflow
Section titled “The workflow”struct RetrySimulationTask;
#[async_trait]impl DurableExecution for RetrySimulationTask { type Data = RetrySimulationInput; type Output = RetrySimulationOutput;
async fn run(&self, data: Self::Data) -> Result<Self::Output, TaskError> { // Step 1: fails first, then retries automatically. let result = intentional_failure_step(data.name.clone()).await?;
let mut attempts_log = vec![format!( "intentional-failure: succeeded on attempt #{} ({} retries)", result.attempt_number, result.attempt_number )];
// Step 2: always succeeds on the first try. let normal = normal_step(data.name.clone()).await?; attempts_log.push(normal);
Ok(RetrySimulationOutput { name: data.name, total_attempts: result.attempt_number + 1, message: format!( "Completed after {} attempt(s), succeeded on retry #{}", result.attempt_number + 1, result.attempt_number ), attempts_log, }) }}Running the workflow
Section titled “Running the workflow”let mut registry = TaskRegistry::new();registry.register("retry-simulation", RetrySimulationTask);let registry = Arc::new(registry);
let durable = DurableScheduler::new(sched.clone());durable .start_for::<RetrySimulationTask>(&execution_id, "retry-simulation", &RetrySimulationInput { name: "retry-demo".into(), }) .await?;
let output = durable.wait_for::<RetrySimulationTask>(&execution_id, Duration::from_secs(60)).await?;println!("{}", output.message);What you’ll see
Section titled “What you’ll see”=== Zart Retry Simulation Example ===
Starting execution 'retry-sim-...'...
[intentional-failure] Attempt #0 | is_retry=false | max_retries=Some(3)Simulated transient failure for 'retry-demo' on attempt #0
[intentional-failure] Attempt #1 | is_retry=true | max_retries=Some(3)Succeeded for 'retry-demo' on retry attempt #1
[normal-step] Running (no retries needed)
=== Execution Completed === Name: retry-demo Total attempts: 2 Message: Completed after 2 attempt(s), succeeded on retry #1Key concepts
Section titled “Key concepts”retry = "fixed(n, delay)" — configures up to n retries with a constant delay between attempts. Use "exponential(n, initial)" for doubling delays.
zart::context().current_attempt — zero-indexed attempt counter available inside every step. 0 on the first run, 1 on the first retry, and so on.
zart::context().is_retry() — returns true if current_attempt > 0. Useful for conditional logic such as skipping expensive setup on retries.
zart::context().max_retries — returns the configured retry limit so the step can adapt its behavior as retries are exhausted.
See ZartStep — Retry Configuration for the full retry API reference.