.do
Patterns

Workflow Patterns

Event-driven business process patterns and orchestration

Workflow patterns define how business processes flow through your autonomous business.

Sequential Workflows

The simplest pattern: do A, then B, then C.

// @errors: 7006
// @strict: true
import $, { on, send } from 'sdk.do'

// Order fulfillment sequence
on($.Order.created, async (order) => {
  // Step 1: Validate
  const validation = await send($.Order.validate, { order })
  //    ^?

  if (!validation.valid) {
    await send($.Order.cancel, { order, reason: validation.error })
    return
  }

  // Step 2: Process payment
  const payment = await send($.Payment.process, {
    order,
    amount: order.totalPrice,
  })
  //    ^?

  if (payment.status !== 'succeeded') {
    await send($.Order.cancel, { order, reason: 'payment-failed' })
    return
  }

  // Step 3: Reserve inventory
  const reservation = await send($.Inventory.reserve, {
    items: order.orderedItem,
  })
  //    ^?

  // Step 4: Create shipment
  const shipment = await send($.Shipment.create, {
    order,
    items: reservation.items,
  })
  //    ^?

  // Step 5: Notify customer - all types are inferred
  await send($.Email.send, {
    to: order.customer.email,
    template: 'order-confirmed',
    data: { order, payment, shipment },
  })
})

When to use:

  • Simple, linear processes
  • Each step depends on the previous
  • Clear error handling at each stage

Parallel Workflows

Execute multiple operations simultaneously:

// Product launch workflow
on($.Product.launched, async (product) => {
  // Execute all in parallel
  await Promise.all([
    // Update inventory systems
    send($.Inventory.sync, { product }),

    // Generate marketing content
    send($.Marketing.createCampaign, { product }),

    // Notify sales team
    send($.Notification.send, {
      to: $.Role.SalesTeam,
      message: `New product launched: ${product.name}`,
    }),

    // Update website
    send($.Website.publish, { product }),

    // Social media announcements
    send($.Social.post, {
      platforms: ['twitter', 'linkedin'],
      content: await ai.generate('product-announcement', { product }),
    }),
  ])

  await send($.Product.launchCompleted, { product })
})

When to use:

  • Independent operations
  • Faster total execution time
  • Operations don't depend on each other

Conditional Workflows

Branch based on conditions:

// Smart order routing
on($.Order.created, async (order) => {
  const customer = order.customer

  // Route based on customer type
  if (customer.membershipLevel === 'premium') {
    // VIP processing
    await send($.Order.processPriority, {
      order,
      shipping: 'express',
      support: 'dedicated',
    })
  } else if (order.totalPrice > 1000) {
    // High-value order
    await send($.Order.processHighValue, {
      order,
      verification: 'enhanced',
      shipping: 'priority',
    })
  } else {
    // Standard processing
    await send($.Order.processStandard, { order })
  }
})

// AI-driven conditional logic
on($.SupportTicket.created, async (ticket) => {
  const analysis = await ai.analyze('ticket-complexity', { ticket })

  if (analysis.canAutoResolve) {
    // Autonomous resolution
    await send($.Ticket.autoResolve, { ticket, analysis })
  } else if (analysis.complexity === 'low') {
    // Route to tier 1 support
    await send($.Ticket.assignTier1, { ticket })
  } else if (analysis.complexity === 'medium') {
    // Route to tier 2 support
    await send($.Ticket.assignTier2, { ticket })
  } else {
    // Escalate to senior support
    await send($.Ticket.escalate, {
      ticket,
      reason: analysis.reasonForEscalation,
    })
  }
})

When to use:

  • Different paths for different scenarios
  • Business rules determine flow
  • Personalization based on context

Event Chain Workflows

Events trigger subsequent events:

// Subscription lifecycle chain
on($.Person.registered, async (person) => {
  // Create trial subscription
  const sub = await $.Service.create({
    customer: person,
    plan: 'trial',
    status: 'trialing',
    trialEnd: addDays(new Date(), 14),
  })

  await send($.Subscription.created, sub)
})

on($.Subscription.created, async (sub) => {
  // Provision account
  await send($.Account.provision, {
    customer: sub.customer,
    features: sub.plan.features,
  })
})

on($.Account.provisioned, async (account) => {
  // Send onboarding email
  await send($.Email.send, {
    to: account.customer.email,
    template: 'onboarding-start',
  })

  // Schedule onboarding tasks
  await send($.Onboarding.schedule, { account })
})

on($.Subscription.trialEnding, async (sub) => {
  // 3 days before trial ends
  if (sub.usage.engagement === 'high') {
    // High engagement, offer paid plan
    await send($.Offer.send, {
      customer: sub.customer,
      offer: 'trial-to-paid',
      discount: 0.2,
    })
  } else {
    // Low engagement, offer help
    await send($.Meeting.schedule, {
      customer: sub.customer,
      type: 'success-call',
    })
  }
})

on($.Subscription.upgraded, async (sub) => {
  await send($.Account.updateFeatures, {
    customer: sub.customer,
    features: sub.newPlan.features,
  })

  await send($.Email.send, {
    to: sub.customer.email,
    template: 'upgrade-confirmation',
  })
})

When to use:

  • Multi-stage processes
  • State transitions
  • Decoupled systems
  • Each stage can be independent

Saga Pattern (Distributed Transactions)

Maintain consistency across multiple services with compensating actions:

// @errors: 7006
// @strict: true
import $, { on, send, db } from 'sdk.do'

type Order = {
  $id: string
  orderedItem: any[]
  totalPrice: number
  customer: { email: string }
}

// Order saga with compensations - TypeScript ensures type safety
async function processOrderSaga(order: Order) {
  // Saga state is fully typed
  const saga = {
    steps: [] as string[],
    compensations: [] as Array<() => Promise<void>>,
  }

  try {
    // Step 1: Reserve inventory
    const inventory = await send($.Inventory.reserve, {
      items: order.orderedItem,
    })
    //    ^?
    saga.steps.push('inventory-reserved')
    saga.compensations.push(async () => {
      await send($.Inventory.release, { reservation: inventory })
    })

    // Step 2: Process payment - payment result is typed
    const payment = await send($.Payment.process, {
      order,
      amount: order.totalPrice,
    })
    //    ^?
    saga.steps.push('payment-processed')
    saga.compensations.push(async () => {
      await send($.Payment.refund, { payment })
    })

    // Step 3: Create shipment - shipment result is typed
    const shipment = await send($.Shipment.create, {
      order,
      items: inventory.items,
    })
    //    ^?
    saga.steps.push('shipment-created')
    saga.compensations.push(async () => {
      await send($.Shipment.cancel, { shipment })
    })

    // Step 4: Send confirmation
    await send($.Email.send, {
      to: order.customer.email,
      template: 'order-confirmed',
      data: { order, payment, shipment },
    })

    // Success! Mark order as complete
    await send($.Order.completed, {
      order,
      saga: { steps: saga.steps },
    })
  } catch (error) {
    // Something failed, run compensations in reverse
    console.error('Order saga failed:', error)

    // TypeScript ensures compensations array is correctly typed
    for (const compensation of saga.compensations.reverse()) {
      try {
        await compensation()
      } catch (compError) {
        console.error('Compensation failed:', compError)
        // Log for manual intervention
        await db.create($.SagaError, {
          order,
          step: saga.steps[saga.compensations.indexOf(compensation)],
          error: compError,
        })
      }
    }

    await send($.Order.failed, {
      order,
      reason: (error as Error).message,
      saga: { steps: saga.steps, failed: true },
    })
  }
}

on($.Order.created, processOrderSaga)

When to use:

  • Distributed transactions
  • Need to maintain consistency
  • Operations span multiple services
  • Must handle partial failures

Long-Running Workflows

Workflows that span days, weeks, or months:

// Customer nurture campaign (spans weeks)
on($.Person.registered, async (person) => {
  // Day 0: Welcome
  await send($.Email.send, {
    to: person.email,
    template: 'welcome',
    data: { person },
  })

  // Day 1: Getting started guide
  await send($.Email.schedule, {
    to: person.email,
    template: 'getting-started',
    sendAt: addDays(new Date(), 1),
  })

  // Day 3: Feature highlight
  await send($.Email.schedule, {
    to: person.email,
    template: 'feature-highlight',
    sendAt: addDays(new Date(), 3),
    feature: await ai.recommend({
      type: $.Feature,
      for: person,
      strategy: 'most-relevant',
    }),
  })

  // Day 7: Check-in
  await send($.Email.schedule, {
    to: person.email,
    template: 'checkin-week1',
    sendAt: addDays(new Date(), 7),
  })

  // Day 14: Upgrade offer (if still on trial)
  await send($.Task.schedule, {
    action: async () => {
      const sub = await db.related(person, $.subscribes, $.Service)
      if (sub.status === 'trialing') {
        await send($.Offer.send, {
          customer: person,
          offer: 'trial-to-paid',
        })
      }
    },
    runAt: addDays(new Date(), 14),
  })

  // Day 30: Survey
  await send($.Email.schedule, {
    to: person.email,
    template: 'month1-survey',
    sendAt: addDays(new Date(), 30),
  })
})

// Subscription renewal workflow (spans months)
on($.Subscription.created, async (sub) => {
  // 7 days before renewal
  await send($.Task.schedule, {
    action: async () => {
      await send($.Email.send, {
        to: sub.customer.email,
        template: 'renewal-reminder',
        daysUntil: 7,
      })
    },
    runAt: addDays(sub.currentPeriodEnd, -7),
  })

  // Day of renewal
  await send($.Task.schedule, {
    action: async () => {
      await send($.Subscription.renew, { subscription: sub })
    },
    runAt: sub.currentPeriodEnd,
  })
})

When to use:

  • Time-based workflows
  • Scheduled actions
  • Customer lifecycle management
  • Recurring processes

Human-in-the-Loop Workflows

Workflows that require human approval:

// Purchase approval workflow
on($.PurchaseOrder.created, async (po) => {
  if (po.totalPrice > 10000) {
    // Requires approval
    const approval = await send($.Approval.request, {
      type: 'purchase-order',
      item: po,
      approver: $.Role.ProcurementManager,
      reason: 'High-value purchase',
      amount: po.totalPrice,
    })

    // Wait for approval (blocking)
    await approval.waitFor()

    if (approval.status === 'approved') {
      await send($.PurchaseOrder.submit, { po })
    } else {
      await send($.PurchaseOrder.cancel, {
        po,
        reason: approval.rejectionReason,
      })
    }
  } else {
    // Auto-approve small purchases
    await send($.PurchaseOrder.submit, { po })
  }
})

// Content moderation workflow
on($.Content.submitted, async (content) => {
  // AI pre-screening
  const screening = await ai.analyze('content-safety', { content })

  if (screening.safe && screening.confidence > 0.95) {
    // High confidence it's safe, auto-approve
    await send($.Content.approve, { content, method: 'auto' })
  } else if (screening.flagged) {
    // Definitely problematic, auto-reject
    await send($.Content.reject, {
      content,
      reason: screening.concerns,
    })
  } else {
    // Uncertain, human review
    await send($.Content.queueForReview, {
      content,
      priority: screening.urgency,
      concerns: screening.potentialIssues,
    })
  }
})

on($.Content.reviewed, async (review) => {
  if (review.decision === 'approved') {
    await send($.Content.publish, { content: review.content })
  } else {
    await send($.Content.reject, {
      content: review.content,
      reason: review.reason,
    })
  }
})

When to use:

  • High-stakes decisions
  • Compliance requirements
  • Edge cases AI can't handle
  • Training data for AI

Error Handling Patterns

Retry with Backoff

async function processWithRetry(action: () => Promise<any>, maxRetries = 3) {
  let attempt = 0

  while (attempt < maxRetries) {
    try {
      return await action()
    } catch (error) {
      attempt++

      if (attempt >= maxRetries) {
        throw error
      }

      // Exponential backoff
      const delay = Math.pow(2, attempt) * 1000
      await new Promise((resolve) => setTimeout(resolve, delay))

      console.log(`Retry attempt ${attempt} after ${delay}ms`)
    }
  }
}

on($.Payment.process, async (payment) => {
  await processWithRetry(async () => {
    return await paymentGateway.charge(payment)
  }, 3)
})

Dead Letter Queue

on($.Order.created, async (order) => {
  try {
    await processOrder(order)
  } catch (error) {
    // After retries fail, send to DLQ
    await db.create($.DeadLetter, {
      type: 'order-processing',
      payload: order,
      error: error.message,
      attempts: 3,
      timestamp: new Date(),
    })

    // Notify humans
    await send($.Notification.send, {
      to: $.Role.Operations,
      priority: 'high',
      message: `Order ${order.orderNumber} failed processing`,
      action: { review: order.$id },
    })
  }
})

Circuit Breaker

// @errors: 7006
// @strict: true
import { send } from 'sdk.do'

// Generic circuit breaker with full type safety
class CircuitBreaker {
  private failures = 0
  private lastFailure?: Date
  private state: 'closed' | 'open' | 'half-open' = 'closed'

  // Generic execute method preserves return type
  async execute<T>(action: () => Promise<T>): Promise<T> {
    //              ^?
    if (this.state === 'open') {
      // Check if enough time has passed
      if (Date.now() - this.lastFailure!.getTime() > 60000) {
        this.state = 'half-open'
      } else {
        throw new Error('Circuit breaker is open')
      }
    }

    try {
      const result = await action()
      //    ^?

      // Success, reset circuit
      if (this.state === 'half-open') {
        this.state = 'closed'
        this.failures = 0
      }

      return result
    } catch (error) {
      this.failures++
      this.lastFailure = new Date()

      if (this.failures >= 5) {
        this.state = 'open'
        await send($.Alert.send, {
          type: 'circuit-breaker-open',
          service: 'payment-gateway',
        })
      }

      throw error
    }
  }
}

const paymentCircuit = new CircuitBreaker()

// TypeScript infers the return type through the circuit breaker
on($.Payment.process, async (payment) => {
  const result = await paymentCircuit.execute(async () => {
    return await paymentGateway.charge(payment)
  })
  //    ^?
})

Workflow Composition

Combine simple workflows into complex ones:

// @errors: 7006
// @strict: true
import $, { on, send } from 'sdk.do'

type Person = {
  $id: string
  email: string
  name: string
}

type Account = {
  $id: string
  owner: Person
  features: string[]
}

// Compose workflows with proper typing
async function onboardCustomer(customer: Person) {
  // Run multiple workflows in sequence - each step is typed
  await provisionAccount(customer)
  await sendWelcomeEmail(customer)
  await createInitialTasks(customer)
  await scheduleFollowUps(customer)
}

// Each workflow function has explicit return types
async function provisionAccount(customer: Person): Promise<Account> {
  const account = await $.Account.create({
    owner: customer,
    features: ['feature-a', 'feature-b'],
  })
  //    ^?

  await send($.Account.provisioned, account)
  return account
}

async function sendWelcomeEmail(customer: Person): Promise<void> {
  await send($.Email.send, {
    to: customer.email,
    template: 'welcome',
  })
}

async function createInitialTasks(customer: Person): Promise<void> {
  // Implementation...
}

async function scheduleFollowUps(customer: Person): Promise<void> {
  // Implementation...
}

// Type-safe composition - TypeScript verifies customer type
on($.Person.registered, onboardCustomer)

Best Practices

1. Keep Workflows Focused

// Good: Single responsibility
on($.Order.created, processPayment)
on($.Payment.processed, updateInventory)
on($.Inventory.updated, createShipment)

// Avoid: God workflow
on($.Order.created, async (order) => {
  // 500 lines of code
})

2. Make Workflows Idempotent

// Idempotent: Can be run multiple times safely
on($.Payment.process, async (payment) => {
  // Check if already processed
  const existing = await db.get($.Payment, payment.$id)

  if (existing.status === 'processed') {
    return existing // Already done
  }

  // Process payment
  const result = await gateway.charge(payment)

  await db.update($.Payment, payment.$id, {
    status: 'processed',
    result,
  })

  return result
})

3. Log Everything

on($.Order.created, async (order) => {
  await db.create($.WorkflowLog, {
    workflow: 'order-processing',
    orderId: order.$id,
    stage: 'started',
    timestamp: new Date(),
  })

  try {
    await processOrder(order)

    await db.create($.WorkflowLog, {
      workflow: 'order-processing',
      orderId: order.$id,
      stage: 'completed',
      timestamp: new Date(),
    })
  } catch (error) {
    await db.create($.WorkflowLog, {
      workflow: 'order-processing',
      orderId: order.$id,
      stage: 'failed',
      error: error.message,
      timestamp: new Date(),
    })

    throw error
  }
})

4. Use Timeouts

async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), timeoutMs)
  })

  return Promise.race([promise, timeout])
}

on($.Payment.process, async (payment) => {
  try {
    await withTimeout(
      gateway.charge(payment),
      30000 // 30 second timeout
    )
  } catch (error) {
    if (error.message === 'Timeout') {
      await send($.Payment.timeout, { payment })
    }
    throw error
  }
})

Summary

Workflow patterns enable:

  • Sequential - Step-by-step processes
  • Parallel - Concurrent operations
  • Conditional - Branching logic
  • Event Chain - Multi-stage processes
  • Saga - Distributed transactions
  • Long-Running - Time-based workflows
  • HITL - Human approval gates
  • Error Handling - Retry, DLQ, circuit breakers

Choose the right pattern for your business process.


Next: Event-Driven Patterns →