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 →