Skip to main content
This article is part of the “Building with AI” series documenting my journey using multi-agent AI workflows to build production systems.All examples are from personal projects and do not represent employer technologies.

The Challenge

January 2026. My SaaS platform needed to implement product bundle/unbundle workflows for subscription management. The requirement: customers should be able to remove items from a bundle, get appropriate price adjustments, and trigger approvals if changes are significant. The Problem: What happens if Step 3 fails after Steps 1-2 succeed? You’re left with a half-completed operation and corrupted state. AI Focus: Can AI design a saga pattern that handles compensation logic automatically? System Example: A state machine workflow with automatic rollback using the SagaStep macro for multi-step business processes.

The Multi-Step Problem

Here’s what a bundle/unbundle workflow looks like: Happy Path:
  1. Validate unbundle request (check flags, required components)
  2. Calculate new pricing (apply discount adjustments)
  3. Check if approval needed (>50% components removed)
  4. Execute unbundle operation
  5. Emit domain events (audit trail)
Nightmare Scenario:
Step 1: ✅ Validation passed
Step 2: ✅ Pricing calculated
Step 3: ✅ Approval created
Step 4: ❌ DynamoDB write failed
Result: Approval exists but operation didn't execute
Now your database thinks the customer approved something that never happened.
The Evaluator analyzed the workflow requirements and proposed a saga pattern with compensation:
Forward steps: Validate → Price → Approve → Execute → Emit
Compensation: Cancel Approval ← Revert Price ← Fail Validation
Builder agent generated the SagaStep macro to eliminate boilerplate:
  • Auto-implement compensation methods
  • Track step execution order
  • Handle partial rollbacks

The Saga Pattern

AI discovered we could model workflows as a series of compensatable steps:
pub async fn unbundle_product(
    opportunity_id: Uuid,
    items_to_remove: Vec<Uuid>,
) -> Result<()> {
    // Step 1: Validate
    validate_unbundle(&opportunity_id, &items_to_remove)?;

    // Step 2: Calculate pricing
    let new_price = calculate_pricing(&opportunity_id, &items_to_remove)?;

    // Step 3: Create approval if needed
    let approval_id = if requires_approval(&items_to_remove) {
        Some(create_approval(&opportunity_id).await?)  // ❌ What if this succeeds...
    } else {
        None
    };

    // Step 4: Execute unbundle
    execute_unbundle(&opportunity_id, &items_to_remove).await?;  // ❌ ...but this fails?

    // Step 5: Emit event
    emit_unbundled_event(&opportunity_id).await?;

    Ok(())
}
// Result: Approval exists but unbundle never happened. Corruption!

How Compensation Works

If execute_unbundle() fails, the saga automatically runs compensation steps in reverse:
Forward:       Validate → Price → Approve → Execute → Emit

                                             FAIL!

Compensation:  Validate ← Revert Price ← Cancel Approval
Result: Database is consistent. Approval is canceled. Price is restored.

The SagaStep Macro

The SagaStep macro eliminates saga boilerplate. Here’s what it generates: You write:
#[derive(SagaStep)]
pub struct UnbundleSaga {
    opportunity_id: Uuid,
    items_to_remove: Vec<Uuid>,
}
Macro generates:
impl TransactionalStep for UnbundleSaga {
    fn step_name(&self) -> &str {
        "UnbundleSaga"
    }

    fn step_count(&self) -> usize {
        5  // validate, price, approve, execute, emit
    }

    async fn execute_step(&mut self, step: usize) -> Result<()> {
        match step {
            0 => self.validate().await,
            1 => self.calculate_pricing().await,
            2 => self.create_approval().await,
            3 => self.execute_unbundle().await,
            4 => self.emit_event().await,
            _ => Err(StepOutOfBounds),
        }
    }

    async fn compensate_step(&mut self, step: usize) -> Result<()> {
        match step {
            1 => self.compensate_calculate_pricing().await,
            2 => self.compensate_create_approval().await,
            _ => Ok(())  // Steps without compensation
        }
    }
}
Savings: 150 lines of boilerplate per saga.

The Bundle/Unbundle Workflow

Here’s the actual workflow AI generated for the bundle/unbundle feature: Business Rules:
pub mod bundle_operations {
    /// Check if unbundle is allowed
    pub fn validate_unbundle_allowed(bundle: &Product) -> Result<()> {
        if !bundle.unbundle_allowed {
            return Err(UnbundleNotAllowed);
        }
        if bundle.min_components > remaining_components(&bundle) {
            return Err(RequiredComponentsMissing);
        }
        Ok(())
    }

    /// Calculate price adjustment with reduced discount
    pub fn calculate_unbundle_pricing(
        bundle: &Product,
        items_to_remove: &[Uuid],
    ) -> Decimal {
        let original_discount = bundle.bundle_discount_value;
        let penalty_factor = items_to_remove.len() as f64 / bundle.components.len() as f64;

        // Reduce discount proportionally
        let new_discount = original_discount * (1.0 - penalty_factor * bundle.unbundle_price_adjustment);

        bundle.base_price * (1.0 - new_discount)
    }

    /// Check if approval workflow needed
    pub fn requires_approval(
        bundle: &Product,
        items_to_remove: &[Uuid],
    ) -> bool {
        let removal_percentage = items_to_remove.len() as f64 / bundle.components.len() as f64;
        removal_percentage > 0.5  // >50% removal requires approval
    }
}
Test Scenarios:
#[tokio::test]
async fn test_unbundle_happy_path() {
    // Remove 1 of 5 components, no approval needed
    let result = unbundle_product(opportunity_id, vec![component_1]).await;
    assert!(result.is_ok());
    assert_eq!(approval_created, false);
}

#[tokio::test]
async fn test_unbundle_requires_approval() {
    // Remove 3 of 5 components (60%), approval required
    let result = unbundle_product(opportunity_id, vec![c1, c2, c3]).await;
    assert!(result.is_ok());
    assert_eq!(approval_created, true);
}

#[tokio::test]
async fn test_unbundle_compensation() {
    // Simulate failure after approval creation
    let saga = UnbundleSaga::new(opportunity_id, items_to_remove);
    saga.execute_until_step(2).await?;  // Approval created
    saga.fail_at_step(3).await?;  // Execute unbundle fails

    // Verify compensation ran
    assert!(approval_canceled());
    assert_eq!(price, original_price);
}
Coverage: 11 test scenarios, including happy path, approval triggers, compensation, and edge cases.

What Went Wrong

Mistake: The initial saga implementation didn’t handle idempotency. If a compensation step failed and we retried, it would try to “cancel approval” twice.Why it failed: AI designed pure compensation logic but didn’t consider retry scenarios. DynamoDB would throw ConditionalCheckFailedException on the second cancel attempt.How we fixed it: Added idempotency checks to compensation methods:
async fn compensate_create_approval(&mut self) -> Result<()> {
    if let Some(approval_id) = self.approval_id {
        // Check if already canceled
        match get_approval_status(approval_id).await? {
            ApprovalStatus::Canceled => return Ok(()),  // Already done
            ApprovalStatus::Pending => cancel_approval(approval_id).await?,
            _ => {}  // Other states don't need cancellation
        }
    }
    Ok(())
}
Lesson: AI designs correct forward and backward flows but misses production edge cases like retry idempotency. Humans need to add defensive checks.

Key Learnings

AI Strength

AI excels at state machine generation. Give it business rules + compensation requirements, and it generates perfect forward/backward flows with all the boilerplate.

AI Weakness

AI doesn’t think about retry scenarios or idempotency. It assumes happy path execution and single-pass compensation.

Human Role

Humans define business rules (approval thresholds, pricing formulas) and add production hardening (idempotency, error recovery).

Process Insight

The best workflow design separates business logic (pure functions) from orchestration (saga). AI is perfect for generating orchestration boilerplate.

Actionable Takeaways

If you’re building multi-step workflows with AI:
  1. Separate logic from orchestration - Pure business functions (validate, calculate) + saga for orchestration. AI generates the saga, you write the business logic.
  2. Design compensation first - Don’t start coding until you know how each step rolls back. AI can help design this if you give it the requirements.
  3. Add idempotency everywhere - Compensation steps will retry. Make sure they can run multiple times safely.
Pro tip: Use the SagaStep macro pattern even for non-saga workflows. The compensation boilerplate alone saves 150 lines per workflow, and it forces you to think about rollback logic upfront.

Metrics

  • Workflow implementation: 2,073 lines
  • Integration tests: 457 lines
  • Test scenarios: 11 (happy path + edge cases + compensation)
  • Boilerplate saved per saga: ~150 lines (macro)

The Saga State Machine

Here’s the actual state machine AI generated for bundle/unbundle:
                    ┌─────────────┐
                    │  Initiated  │
                    └──────┬──────┘

                    ┌──────▼──────┐
                    │  Validated  │
                    └──────┬──────┘

                    ┌──────▼──────┐
                    │ Priced      │ ◄──── Compensation: Revert Price
                    └──────┬──────┘

                    ┌──────▼──────┐
                    │ Approved    │ ◄──── Compensation: Cancel Approval
                    └──────┬──────┘

                    ┌──────▼──────┐
                    │  Executed   │
                    └──────┬──────┘

                    ┌──────▼──────┐
                    │  Completed  │
                    └─────────────┘

If any step fails:
    Run compensation for previous steps (in reverse order)
    Transition to Failed state
    Database remains consistent

Resources & Further Reading


Next in This Series

Week 6: How we built 5 derive macros that eliminated 600-800 lines of boilerplate per domain entity.

Week 6: The Macro Revolution

Code generation that reduced repository boilerplate by 80%

Discussion

Share Your Experience

How do you handle multi-step workflows? Manual rollback or automated compensation?Connect on LinkedIn or comment on the YouTube Short

Disclaimer: This content represents my personal learning journey using AI for a personal project. It does not represent my employer’s views, technologies, or approaches.All code examples are generic patterns or pseudocode for educational purposes.