.do

Workflow Design

Design business processes and automations with semantic patterns

Workflows orchestrate business processes by combining events, actions, conditions, and integrations into cohesive automations.

What are Workflows?

Workflows are executable business processes defined as code. They:

  • React to events (triggers)
  • Execute actions (operations)
  • Make decisions (conditions)
  • Maintain state (durable execution)
  • Handle errors (retries, compensation)
  • Integrate systems (external services)

Workflow Patterns

1. Sequential Workflows

Execute steps in order:

// Customer onboarding workflow
const onboardingWorkflow = $.workflow('customer-onboarding', async (customer: Person) => {
  // Step 1: Send welcome email
  await $.step('welcome-email', async () => {
    send.Email({
      to: customer.email,
      template: 'welcome',
    })
  })

  // Step 2: Create account
  const account = await $.step('create-account', async () => {
    return await db.Accounts.create({
      customer: customer.$id,
      status: 'active',
      createdAt: new Date(),
    })
  })

  // Step 3: Schedule onboarding call
  await $.step('schedule-call', async () => {
    send.Meeting.schedule({
      attendees: [customer.email, '[email protected]'],
      duration: 30,
      subject: 'Welcome to Acme!',
    })
  })

  return { customer, account }
})

// Trigger workflow
on.Customer.registered(async (customer) => {
  await onboardingWorkflow.execute(customer)
})

2. Parallel Workflows

Execute steps concurrently:

// Lead enrichment workflow
const enrichLeadWorkflow = $.workflow('enrich-lead', async (lead) => {
  // Execute all enrichments in parallel
  const [email, company, social] = await Promise.all([
    $.step('verify-email', () => verifyEmail(lead.email)),
    $.step('lookup-company', () => lookupCompany(lead.domain)),
    $.step('find-social', () => findSocialProfiles(lead.name)),
  ])

  // Combine results
  return await $.step('merge-data', () => ({
    ...lead,
    emailValid: email.valid,
    company: company,
    socialProfiles: social,
  }))
})

3. Conditional Workflows

Branch based on conditions:

// Order fulfillment workflow
const fulfillOrderWorkflow = $.workflow('fulfill-order', async (order: Order) => {
  // Check inventory
  const inventory = await $.step('check-inventory', async () => {
    return await db.Inventory.findOne({
      where: { productId: { $in: order.items.map((i) => i.productId) } },
    })
  })

  if (inventory.available) {
    // In stock: process immediately
    await $.step('reserve-inventory', () => send.Inventory.reserve(order.items))

    await $.step('create-shipment', () => send.Shipment.create(order))
  } else {
    // Out of stock: backorder
    await $.step('create-backorder', () => send.Backorder.create(order))

    await $.step('notify-customer', () =>
      send.Email({
        to: order.customer.email,
        template: 'backorder-notification',
      })
    )
  }
})

4. Loop Workflows

Iterate over collections:

// Batch email campaign workflow
const emailCampaignWorkflow = $.workflow('email-campaign', async (campaign) => {
  // Get recipients
  const recipients = await $.step('get-recipients', async () => {
    return await db.Person.list({
      where: { segment: campaign.segment },
    })
  })

  // Send to each recipient
  for (const recipient of recipients) {
    await $.step(`send-${recipient.$id}`, async () => {
      send.Email({
        to: recipient.email,
        template: campaign.template,
        data: await personalize(recipient),
      })
    })

    // Rate limiting
    await $.sleep(100) // 100ms between emails
  }

  return { sent: recipients.length }
})

5. Compensating Workflows

Handle failures with rollback:

// Payment workflow with compensation
const processPaymentWorkflow = $.workflow('process-payment', async (order: Order) => {
  // Reserve inventory
  const reservation = await $.step('reserve-inventory', async () => {
    return await send.Inventory.reserve(order.items)
  })

  try {
    // Charge payment
    const payment = await $.step('charge-payment', async () => {
      return await send.Payment.charge({
        amount: order.total,
        customer: order.customer,
      })
    })

    // Confirm order
    await $.step('confirm-order', async () => {
      send.Order.confirmed({ order, payment })
    })

    return { success: true, payment }
  } catch (error) {
    // Compensate: release inventory
    await $.step('release-inventory', async () => {
      send.Inventory.release(reservation)
    })

    // Notify failure
    await $.step('notify-failure', async () => {
      send.Order.failed({ order, error })
    })

    throw error
  }
})

Workflow Triggers

Event Triggers

React to system events:

// Trigger on order creation
on.Order.created(async (order) => {
  await fulfillOrderWorkflow.execute(order)
})

// Trigger on multiple events
on([$.User.registered, $.User.verified], async (user) => {
  await onboardingWorkflow.execute(user)
})

Schedule Triggers

Run on a schedule:

// Daily report workflow
const dailyReportWorkflow = $.workflow('daily-report', async () => {
  const stats = await calculateDailyStats()
  send.Report.generated(stats)
})

// Run every day at 9am
every.Daily.at('09:00', async () => {
  await dailyReportWorkflow.execute()
})

Manual Triggers

Invoke manually via API:

// API endpoint to trigger workflow
app.post('/api/workflows/process-refund', async (c) => {
  const { orderId } = await c.req.json()
  const order = await db.Orders.get(orderId)

  const result = await refundWorkflow.execute(order)
  return c.json(result)
})

Webhook Triggers

React to external webhooks:

// Webhook handler
app.post('/webhooks/stripe', async (c) => {
  const event = await c.req.json()

  if (event.type === 'payment_intent.succeeded') {
    await paymentSucceededWorkflow.execute(event.data)
  }

  return c.json({ received: true })
})

Workflow State

Durable Execution

Workflows maintain state across failures:

const longRunningWorkflow = $.workflow('long-running', async (data) => {
  // Step 1: Process data (takes 5 minutes)
  const processed = await $.step('process', async () => {
    return await processLargeDataset(data)
  })

  // If workflow crashes here, it resumes from Step 2
  // Step 1 won't re-execute

  // Step 2: Upload results (takes 10 minutes)
  const uploaded = await $.step('upload', async () => {
    return await uploadToStorage(processed)
  })

  // Step 3: Notify completion
  await $.step('notify', async () => {
    send.Notification({ result: uploaded })
  })
})

Workflow Variables

Store state across steps:

const orderWorkflow = $.workflow('order-processing', async (order) => {
  // Workflow-scoped variables
  let inventory: any
  let payment: any
  let shipment: any

  inventory = await $.step('check-inventory', async () => {
    return await checkInventory(order.items)
  })

  if (inventory.available) {
    payment = await $.step('process-payment', async () => {
      return await processPayment(order)
    })

    shipment = await $.step('create-shipment', async () => {
      return await createShipment(order, payment)
    })
  }

  return { inventory, payment, shipment }
})

Workflow Error Handling

Retries

Automatically retry failed steps:

const apiWorkflow = $.workflow('api-call', async (data) => {
  const result = await $.step(
    'call-api',
    async () => {
      return await externalAPI.call(data)
    },
    {
      retry: {
        maxAttempts: 3,
        backoff: 'exponential',
        initialDelay: 1000, // Start with 1 second
      },
    }
  )

  return result
})

Error Handling

Handle errors gracefully:

const resilientWorkflow = $.workflow('resilient', async (data) => {
  try {
    await $.step('risky-operation', async () => {
      return await riskyOperation(data)
    })
  } catch (error) {
    // Log error
    await $.step('log-error', async () => {
      send.Error.log({ error, data })
    })

    // Fallback operation
    await $.step('fallback', async () => {
      return await fallbackOperation(data)
    })
  }
})

Timeouts

Set step timeouts:

const timedWorkflow = $.workflow('timed', async (data) => {
  const result = await $.step(
    'slow-operation',
    async () => {
      return await slowOperation(data)
    },
    {
      timeout: 30000, // 30 seconds
    }
  )

  return result
})

Workflow Monitoring

Progress Tracking

Track workflow progress:

const workflow = $.workflow('tracked', async (data) => {
  // Emit progress events
  send.Workflow.progress({ step: 1, total: 5 })

  await $.step('step-1', async () => {
    await operation1(data)
    send.Workflow.progress({ step: 2, total: 5 })
  })

  await $.step('step-2', async () => {
    await operation2(data)
    send.Workflow.progress({ step: 3, total: 5 })
  })

  // ...more steps
})

Workflow Metrics

Collect workflow metrics:

const workflow = $.workflow('measured', async (data) => {
  const startTime = Date.now()

  const result = await $.step('operation', async () => {
    return await operation(data)
  })

  // Record metrics
  send.Metric.record({
    workflow: 'measured',
    duration: Date.now() - startTime,
    status: 'success',
  })

  return result
})

Workflow Composition

Sub-workflows

Compose workflows from other workflows:

// Reusable sub-workflows
const verifyEmailWorkflow = $.workflow('verify-email', async (email) => {
  // Email verification logic
})

const enrichDataWorkflow = $.workflow('enrich-data', async (data) => {
  // Data enrichment logic
})

// Parent workflow
const mainWorkflow = $.workflow('main', async (lead) => {
  // Call sub-workflows
  await $.step('verify', () => verifyEmailWorkflow.execute(lead.email))
  await $.step('enrich', () => enrichDataWorkflow.execute(lead))
})

Workflow Templates

Create reusable workflow templates:

// Template for approval workflows
const createApprovalWorkflow = (entity: string) => {
  return $.workflow(`${entity}-approval`, async (item) => {
    // Request approval
    const approval = await $.step('request-approval', async () => {
      return await send.Approval.request({
        entity,
        item,
        approvers: await getApprovers(entity),
      })
    })

    // Wait for approval
    const decision = await $.waitFor($.Approval.decided, {
      timeout: 86400000, // 24 hours
    })

    if (decision.approved) {
      await $.step('approve', () => send[$[entity].approved](item))
    } else {
      await $.step('reject', () => send[$[entity].rejected](item))
    }
  })
}

// Create specific workflows from template
const invoiceApproval = createApprovalWorkflow('Invoice')
const expenseApproval = createApprovalWorkflow('Expense')

Workflow Integration

External APIs

Integrate with external services:

const integrationWorkflow = $.workflow('integration', async (data) => {
  // Call external API
  const result = await $.step('call-salesforce', async () => {
    return await fetch('https://api.salesforce.com/leads', {
      method: 'POST',
      headers: { Authorization: `Bearer ${env.SALESFORCE_TOKEN}` },
      body: JSON.stringify(data),
    })
  })

  return result
})

Database Operations

Perform database operations:

const dbWorkflow = $.workflow('database', async (user) => {
  // Create user
  const created = await $.step('create-user', async () => {
    return await db.Person.create(user)
  })

  // Create related records
  await $.step('create-profile', async () => {
    return await db.Profile.create({
      user: created.$id,
      bio: '',
      avatar: null,
    })
  })

  return created
})

AI Operations

Use AI in workflows:

const aiWorkflow = $.workflow('ai-content', async (topic) => {
  // Generate content
  const content = await $.step('generate', async () => {
    return await ai.generate({
      model: 'gpt-5',
      prompt: `Write a blog post about ${topic}`,
      schema: $.BlogPost,
    })
  })

  // Review content
  const reviewed = await $.step('review', async () => {
    return await ai.generate({
      model: 'claude-sonnet-4.5',
      prompt: 'Review and improve this content',
      context: content,
      schema: $.BlogPost,
    })
  })

  return reviewed
})

Best Practices

Do's

  • Use descriptive workflow names
  • Break complex workflows into steps
  • Handle errors gracefully
  • Implement retries for flaky operations
  • Use timeouts to prevent hanging
  • Track workflow progress
  • Compose workflows from sub-workflows
  • Test workflows thoroughly

Don'ts

  • Don't create monolithic workflows
  • Don't ignore error handling
  • Don't skip retries for external calls
  • Don't forget timeouts
  • Don't lose workflow state
  • Don't make workflows too granular
  • Don't skip monitoring

Human-in-the-Loop Workflows

Approval Workflows

Implement approval processes:

const approvalWorkflow = $.workflow('expense-approval', async (expense) => {
  // Submit for approval
  await $.step('submit', async () => {
    await send($.Expense.submitted, {
      expenseId: expense.$id,
      amount: expense.amount,
      submittedBy: expense.employee,
    })
  })

  // Wait for manager approval
  const managerApproval = await $.waitFor(
    $.Approval.decided,
    {
      filter: (event) => event.expenseId === expense.$id && event.approver === expense.manager,
      timeout: 172800000, // 48 hours
    }
  )

  if (!managerApproval || managerApproval.decision === 'reject') {
    await $.step('reject', () => send($.Expense.rejected, { expenseId: expense.$id }))
    return { status: 'rejected' }
  }

  // For amounts over $1000, require VP approval
  if (expense.amount > 1000) {
    await $.step('escalate', () =>
      send($.Approval.requested, {
        expenseId: expense.$id,
        approver: expense.vp,
      })
    )

    const vpApproval = await $.waitFor(
      $.Approval.decided,
      {
        filter: (event) => event.expenseId === expense.$id && event.approver === expense.vp,
        timeout: 172800000,
      }
    )

    if (!vpApproval || vpApproval.decision === 'reject') {
      await $.step('reject-final', () => send($.Expense.rejected, { expenseId: expense.$id }))
      return { status: 'rejected' }
    }
  }

  // Process payment
  await $.step('approve', async () => {
    await send($.Expense.approved, { expenseId: expense.$id })
    await send($.Payment.process, {
      recipient: expense.employee,
      amount: expense.amount,
    })
  })

  return { status: 'approved' }
})

Review Workflows

Implement document review processes:

const documentReviewWorkflow = $.workflow('document-review', async (document) => {
  // Assign reviewers
  const reviewers = await $.step('assign-reviewers', async () => {
    return await assignReviewers(document.type, document.department)
  })

  // Request reviews in parallel
  const reviewRequests = reviewers.map((reviewer) =>
    $.step(`request-review-${reviewer.id}`, () =>
      send($.Review.requested, {
        documentId: document.$id,
        reviewerId: reviewer.id,
      })
    )
  )

  await Promise.all(reviewRequests)

  // Wait for all reviews (with timeout)
  const reviews = []
  for (const reviewer of reviewers) {
    try {
      const review = await $.waitFor(
        $.Review.submitted,
        {
          filter: (event) => event.documentId === document.$id && event.reviewerId === reviewer.id,
          timeout: 604800000, // 7 days
        }
      )
      reviews.push(review)
    } catch (timeoutError) {
      // Send reminder
      await $.step(`remind-${reviewer.id}`, () =>
        send($.Email.send, {
          to: reviewer.email,
          template: 'review-reminder',
          data: { document },
        })
      )

      // Wait again with shorter timeout
      const review = await $.waitFor(
        $.Review.submitted,
        {
          filter: (event) => event.documentId === document.$id && event.reviewerId === reviewer.id,
          timeout: 172800000, // 2 more days
        }
      )
      reviews.push(review)
    }
  }

  // Aggregate feedback
  await $.step('aggregate-feedback', async () => {
    const approved = reviews.filter((r) => r.status === 'approved').length
    const changesRequested = reviews.filter((r) => r.status === 'changes-requested').length
    const rejected = reviews.filter((r) => r.status === 'rejected').length

    if (rejected > 0) {
      await send($.Document.rejected, { documentId: document.$id, reviews })
    } else if (changesRequested > 0) {
      await send($.Document.needs_changes, { documentId: document.$id, reviews })
    } else {
      await send($.Document.approved, { documentId: document.$id, reviews })
    }
  })
})

Workflow State Machines

Define explicit state machines:

interface StateMachine {
  states: Record<string, State>
  initial: string
  context?: any
}

interface State {
  on: Record<string, Transition>
  entry?: () => Promise<void>
  exit?: () => Promise<void>
}

interface Transition {
  target: string
  guard?: (context: any) => boolean
  action?: (context: any) => Promise<any>
}

// Order state machine
const orderStateMachine: StateMachine = {
  initial: 'draft',
  states: {
    draft: {
      on: {
        submit: {
          target: 'pending',
          guard: (order) => order.items.length > 0,
          action: async (order) => {
            await send($.Order.submitted, order)
          },
        },
      },
    },
    pending: {
      entry: async () => {
        console.log('Order pending, waiting for payment')
      },
      on: {
        pay: {
          target: 'processing',
          action: async (order) => {
            const payment = await processPayment(order)
            return { ...order, paymentId: payment.id }
          },
        },
        cancel: {
          target: 'cancelled',
        },
      },
    },
    processing: {
      entry: async () => {
        console.log('Processing order')
      },
      on: {
        fulfill: {
          target: 'completed',
          action: async (order) => {
            await createShipment(order)
          },
        },
        fail: {
          target: 'failed',
        },
      },
    },
    completed: {
      entry: async () => {
        console.log('Order completed')
      },
      on: {
        return: {
          target: 'returned',
          guard: (order) => {
            // Can only return within 30 days
            const daysSince = (Date.now() - order.completedAt.getTime()) / (1000 * 60 * 60 * 24)
            return daysSince <= 30
          },
        },
      },
    },
    cancelled: {},
    failed: {
      on: {
        retry: {
          target: 'processing',
        },
      },
    },
    returned: {},
  },
}

// Workflow using state machine
const orderWorkflow = $.workflow('order', async (order) => {
  let currentState = orderStateMachine.initial
  let context = order

  const transition = async (event: string, data?: any) => {
    const state = orderStateMachine.states[currentState]
    const transition = state.on[event]

    if (!transition) {
      throw new Error(`Invalid transition ${event} from state ${currentState}`)
    }

    // Check guard condition
    if (transition.guard && !transition.guard(context)) {
      throw new Error(`Guard condition failed for ${event}`)
    }

    // Exit current state
    if (state.exit) {
      await state.exit()
    }

    // Execute transition action
    if (transition.action) {
      context = await transition.action({ ...context, ...data })
    }

    // Move to target state
    currentState = transition.target

    // Enter new state
    const newState = orderStateMachine.states[currentState]
    if (newState.entry) {
      await newState.entry()
    }

    // Update order state in database
    await db.Orders.update(order.$id, {
      status: currentState,
      ...context,
    })

    return currentState
  }

  // Handle events
  on($.Order.submitted, async () => await transition('submit'))
  on($.Order.paid, async (payment) => await transition('pay', { paymentId: payment.id }))
  on($.Order.fulfilled, async () => await transition('fulfill'))
  on($.Order.cancelled, async () => await transition('cancel'))

  return { currentState, context }
})

Workflow Orchestration

Parent-Child Workflows

Orchestrate complex workflows:

// Parent workflow
const ecommerceOrderWorkflow = $.workflow('ecommerce-order', async (order) => {
  // Start child workflows in parallel
  const [inventoryReservation, paymentProcessing, customerNotification] = await Promise.all([
    $.startChildWorkflow('reserve-inventory', order.items),
    $.startChildWorkflow('process-payment', {
      orderId: order.$id,
      amount: order.total,
    }),
    $.startChildWorkflow('send-confirmation', {
      email: order.customer.email,
      orderId: order.$id,
    }),
  ])

  // Wait for critical workflows
  const inventoryResult = await inventoryReservation.result()
  const paymentResult = await paymentProcessing.result()

  if (!paymentResult.success) {
    // Cancel inventory reservation
    await $.startChildWorkflow('release-inventory', inventoryReservation.data)
    throw new Error('Payment failed')
  }

  // Start fulfillment workflow
  const fulfillment = await $.startChildWorkflow('fulfill-order', {
    order,
    inventory: inventoryResult,
    payment: paymentResult,
  })

  return await fulfillment.result()
})

// Child workflow
const reserveInventoryWorkflow = $.workflow('reserve-inventory', async (items) => {
  const reservations = []

  for (const item of items) {
    const reservation = await $.step(`reserve-${item.productId}`, async () => {
      const product = await db.Products.get(item.productId)

      if (product.inventory < item.quantity) {
        throw new Error(`Insufficient inventory for ${product.name}`)
      }

      await db.Products.update(item.productId, {
        inventory: { $decrement: item.quantity },
        reserved: { $increment: item.quantity },
      })

      return {
        productId: item.productId,
        quantity: item.quantity,
        reservedAt: new Date(),
      }
    })

    reservations.push(reservation)
  }

  return reservations
})

Workflow Scheduling

Schedule workflow executions:

// Cron-based scheduling
const scheduledWorkflows = [
  {
    name: 'daily-report',
    schedule: '0 9 * * *', // 9 AM daily
    workflow: dailyReportWorkflow,
  },
  {
    name: 'weekly-cleanup',
    schedule: '0 2 * * 0', // 2 AM Sunday
    workflow: weeklyCleanupWorkflow,
  },
  {
    name: 'hourly-sync',
    schedule: '0 * * * *', // Every hour
    workflow: hourlySyncWorkflow,
  },
]

// Register scheduled workflows
scheduledWorkflows.forEach(({ name, schedule, workflow }) => {
  $.schedule(name, schedule, workflow)
})

// Dynamic scheduling
const scheduleWorkflow = async (workflow: any, runAt: Date, data: any) => {
  const delay = runAt.getTime() - Date.now()

  if (delay > 0) {
    await $.sleep(delay)
  }

  return await workflow.execute(data)
}

// Schedule order reminder for abandoned carts
on($.Cart.abandoned, async (cart) => {
  // Send first reminder after 1 hour
  await scheduleWorkflow(cartReminderWorkflow, addHours(new Date(), 1), cart)

  // Send second reminder after 24 hours
  await scheduleWorkflow(cartReminderWorkflow, addHours(new Date(), 24), cart)
})

Workflow Performance

Parallel Execution

Optimize workflow performance:

const parallelWorkflow = $.workflow('parallel-processing', async (data) => {
  // Sequential (slow)
  const slow = await $.step('sequential', async () => {
    const result1 = await operation1(data)
    const result2 = await operation2(data)
    const result3 = await operation3(data)
    return [result1, result2, result3]
  })

  // Parallel (fast)
  const fast = await $.step('parallel', async () => {
    return await Promise.all([operation1(data), operation2(data), operation3(data)])
  })

  // Parallel with concurrency limit
  const limited = await $.step('parallel-limited', async () => {
    const operations = Array.from({ length: 100 }, (_, i) => () => operation(data, i))

    // Process 10 at a time
    return await batchProcess(operations, 10)
  })

  return { slow, fast, limited }
})

// Batch processing helper
const batchProcess = async (operations: Function[], concurrency: number) => {
  const results = []
  const queue = [...operations]

  const worker = async () => {
    while (queue.length > 0) {
      const operation = queue.shift()
      if (operation) {
        const result = await operation()
        results.push(result)
      }
    }
  }

  // Create worker pool
  const workers = Array.from({ length: concurrency }, worker)
  await Promise.all(workers)

  return results
}

Workflow Caching

Cache workflow results:

const cachedWorkflow = $.workflow('expensive-operation', async (input) => {
  // Check cache
  const cacheKey = `workflow:expensive-operation:${JSON.stringify(input)}`
  const cached = await cache.get(cacheKey)

  if (cached) {
    return cached
  }

  // Execute expensive operation
  const result = await $.step('expensive', async () => {
    // Long-running computation
    return await complexCalculation(input)
  })

  // Cache result
  await cache.set(cacheKey, result, { ttl: 3600 })

  return result
})

Workflow Debouncing

Debounce high-frequency workflows:

const debouncedWorkflow = $.workflow('search-index-update', async (data) => {
  // Debounce: only execute if no new events for 5 seconds
  const debounceKey = `debounce:search-index:${data.entityId}`

  await $.debounce(debounceKey, 5000, async () => {
    await $.step('update-index', async () => {
      await updateSearchIndex(data.entityId)
    })
  })
})

// Debounce implementation
const debounceMap = new Map()

const debounce = async (key: string, delay: number, fn: Function) => {
  // Clear existing timer
  if (debounceMap.has(key)) {
    clearTimeout(debounceMap.get(key))
  }

  // Set new timer
  return new Promise((resolve) => {
    const timer = setTimeout(async () => {
      debounceMap.delete(key)
      const result = await fn()
      resolve(result)
    }, delay)

    debounceMap.set(key, timer)
  })
}

Workflow Testing

Unit Testing Workflows

Test individual workflow steps:

import { describe, it, expect, beforeEach, vi } from 'vitest'

describe('orderWorkflow', () => {
  beforeEach(async () => {
    // Reset database and mocks
    await db.test.reset()
    vi.clearAllMocks()
  })

  it('should process order successfully', async () => {
    // Mock dependencies
    const mockProcessPayment = vi.fn().mockResolvedValue({ success: true, id: 'payment-123' })
    const mockCreateShipment = vi.fn().mockResolvedValue({ id: 'shipment-123' })

    // Execute workflow
    const order = await db.Orders.create({
      customerId: 'customer-123',
      items: [{ productId: 'product-1', quantity: 2 }],
      total: 99.99,
    })

    const result = await orderWorkflow.execute(order, {
      mocks: {
        processPayment: mockProcessPayment,
        createShipment: mockCreateShipment,
      },
    })

    // Assertions
    expect(result.status).toBe('completed')
    expect(mockProcessPayment).toHaveBeenCalledWith(order)
    expect(mockCreateShipment).toHaveBeenCalledWith(order)
  })

  it('should handle payment failure', async () => {
    const mockProcessPayment = vi.fn().mockResolvedValue({ success: false })

    const order = await db.Orders.create({
      customerId: 'customer-123',
      items: [{ productId: 'product-1', quantity: 2 }],
      total: 99.99,
    })

    await expect(
      orderWorkflow.execute(order, {
        mocks: { processPayment: mockProcessPayment },
      })
    ).rejects.toThrow('Payment failed')

    // Verify order was cancelled
    const cancelled = await db.Orders.get(order.$id)
    expect(cancelled.status).toBe('cancelled')
  })

  it('should retry on transient failures', async () => {
    const mockOperation = vi
      .fn()
      .mockRejectedValueOnce(new Error('Transient error'))
      .mockResolvedValueOnce({ success: true })

    const order = await db.Orders.create({
      customerId: 'customer-123',
      items: [],
      total: 0,
    })

    const result = await orderWorkflow.execute(order, {
      mocks: { operation: mockOperation },
    })

    expect(mockOperation).toHaveBeenCalledTimes(2)
    expect(result.status).toBe('completed')
  })
})

Integration Testing Workflows

Test workflow interactions:

describe('Order Processing Integration', () => {
  it('should complete full order lifecycle', async () => {
    // Create test data
    await db.test.seed('products', 'customers', 'inventory')

    // Create order
    const order = await db.Orders.create({
      customerId: 'customer-1',
      items: [
        { productId: 'product-1', quantity: 2, price: 29.99 },
        { productId: 'product-2', quantity: 1, price: 49.99 },
      ],
      total: 109.97,
    })

    // Execute workflow
    const result = await orderWorkflow.execute(order)

    // Verify order completed
    expect(result.status).toBe('completed')

    // Verify inventory was updated
    const product1 = await db.Products.get('product-1')
    expect(product1.inventory).toBe(8) // 10 - 2

    // Verify payment was processed
    const payment = await db.Payments.findOne({
      where: { orderId: order.$id },
    })
    expect(payment.status).toBe('success')

    // Verify shipment was created
    const shipment = await db.Shipments.findOne({
      where: { orderId: order.$id },
    })
    expect(shipment.status).toBe('pending')

    // Verify email was sent
    const emails = await db.test.getEmails()
    expect(emails).toHaveLength(1)
    expect(emails[0].to).toBe('[email protected]')
    expect(emails[0].template).toBe('order-confirmation')
  })
})

Workflow Patterns Library

Retry Pattern

const retryPattern = {
  exponentialBackoff: (attempt: number) => Math.min(1000 * Math.pow(2, attempt), 30000),
  linearBackoff: (attempt: number) => 1000 * attempt,
  fixedBackoff: () => 5000,
}

const withRetry = async (
  fn: Function,
  maxAttempts = 3,
  backoff = retryPattern.exponentialBackoff
) => {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn()
    } catch (error) {
      if (attempt === maxAttempts - 1) throw error

      const delay = backoff(attempt)
      await $.sleep(delay)
    }
  }
}

Circuit Breaker Pattern

class CircuitBreaker {
  private failures = 0
  private lastFailureTime?: number
  private state: 'closed' | 'open' | 'half-open' = 'closed'

  constructor(
    private threshold: number = 5,
    private timeout: number = 60000,
    private halfOpenAttempts: number = 1
  ) {}

  async execute(fn: Function) {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailureTime! > this.timeout) {
        this.state = 'half-open'
      } else {
        throw new Error('Circuit breaker is open')
      }
    }

    try {
      const result = await fn()

      if (this.state === 'half-open') {
        this.reset()
      }

      return result
    } catch (error) {
      this.recordFailure()
      throw error
    }
  }

  private recordFailure() {
    this.failures++
    this.lastFailureTime = Date.now()

    if (this.failures >= this.threshold) {
      this.state = 'open'
    }
  }

  private reset() {
    this.failures = 0
    this.state = 'closed'
    this.lastFailureTime = undefined
  }
}

Bulkhead Pattern

class Bulkhead {
  private activeCount = 0
  private queue: Function[] = []

  constructor(private maxConcurrent: number) {}

  async execute(fn: Function) {
    if (this.activeCount >= this.maxConcurrent) {
      await this.enqueue(fn)
    }

    this.activeCount++

    try {
      return await fn()
    } finally {
      this.activeCount--
      this.processQueue()
    }
  }

  private async enqueue(fn: Function) {
    return new Promise((resolve) => {
      this.queue.push(async () => {
        const result = await fn()
        resolve(result)
      })
    })
  }

  private processQueue() {
    if (this.queue.length > 0 && this.activeCount < this.maxConcurrent) {
      const fn = this.queue.shift()
      if (fn) this.execute(fn)
    }
  }
}

Next Steps


Workflow Tip: Great workflows are resilient, observable, and composable. Design for failure and make workflows easy to monitor and debug.