Step Chaining
Sequential durable execution
Fan-Out
Parallel task execution
Fan-In
Collect parallel results
Saga Pattern
Distributed transactions
Overview
Workflows are built using the Tasks SDK's durable step primitives. Each step.run() call is cached on completion — if the task retries, completed steps are skipped. This gives you fault-tolerant, resumable multi-step workflows with no extra infrastructure.
import { task } from '@sylphx/sdk/tasks'
// A sequential order-processing workflow
export const processOrder = task({
id: 'process-order',
run: async ({ orderId, userId }, { step }) => {
// Each step is durable — safe to retry
const order = await step.run('validate', () => validateOrder(orderId))
const charge = await step.run('charge', () => chargePayment(order))
const shipment = await step.run('fulfill', () => fulfillOrder(order, charge))
await step.run('notify', () => sendConfirmation(userId, shipment))
return { orderId, shipmentId: shipment.id }
},
})Chaining Steps
Chain steps sequentially. Each step receives the result of the previous one. Completed steps are never re-executed on retry.
import { task } from '@sylphx/sdk/tasks'
export const processOrder = task({
id: 'process-order',
retry: { maxAttempts: 3 },
run: async ({ orderId, userId, items }, { step }) => {
// Step 1 — validate
const order = await step.run('validate', () =>
validateOrder(orderId, items)
)
// Step 2 — reserve inventory (result passed forward)
const reservation = await step.run('reserve-inventory', () =>
reserveInventory(order.items)
)
// Step 3 — charge (uses results from steps 1 & 2)
const payment = await step.run('process-payment', () =>
chargePayment(order.total, reservation.id)
)
// Step 4 — create shipment
const shipment = await step.run('create-shipment', () =>
createShipment(order, reservation)
)
// Step 5 — notify customer
await step.run('send-confirmation', () =>
sendEmail({ to: order.email, shipment })
)
return { orderId, shipmentId: shipment.id, paymentId: payment.id }
},
})Fan-Out — Parallel Execution
Use batchTrigger() to dispatch multiple task runs in parallel, then use triggerAndWait() within steps to collect results.
import { task } from '@sylphx/sdk/tasks'
import { processItem } from '@/tasks/process-item'
export const batchProcess = task({
id: 'batch-process',
run: async ({ items }: { items: string[] }, { step }) => {
// Prepare
const batch = await step.run('prepare', () => prepareBatch(items))
// Fan-out: trigger N runs in parallel
const handles = await processItem.batchTrigger(
batch.items.map(itemId => ({ itemId }))
)
// Fan-in: wait for all runs to complete
const results = await step.run('collect', () =>
Promise.all(handles.runs.map(h => h.waitForCompletion()))
)
// Aggregate
const stats = {
total: results.length,
successful: results.filter(r => r.ok).length,
failed: results.filter(r => !r.ok).length,
}
await step.run('finalize', () => storeBatchResult(batch.id, stats))
return stats
},
})Fan-In — Aggregate Results
Collect and aggregate results from parallel runs inside a step.run(). Handle partial failures gracefully:
import { task } from '@sylphx/sdk/tasks'
import { processItem } from '@/tasks/process-item'
export const batchReport = task({
id: 'batch-report',
run: async ({ items }, { step }) => {
const handles = await processItem.batchTrigger(
items.map(id => ({ itemId: id }))
)
// Wait for all, ignore individual failures
const results = await step.run('aggregate', async () => {
const settled = await Promise.allSettled(
handles.runs.map(h => h.waitForCompletion())
)
return {
total: settled.length,
successful: settled.filter(r => r.status === 'fulfilled').length,
failed: settled.filter(r => r.status === 'rejected').length,
details: settled.map((r, i) => ({
itemId: items[i],
ok: r.status === 'fulfilled',
result: r.status === 'fulfilled' ? r.value : undefined,
})),
}
})
return results
},
})Handling Partial Failures
Promise.allSettled() inside a step to collect results without failing fast on the first error. Decide whether to abort or continue based on the failure rate.Saga / Compensation Pattern
Implement distributed transactions by tracking completed steps and running compensating actions on failure:
import { task } from '@sylphx/sdk/tasks'
export const orderSaga = task({
id: 'order-saga',
run: async ({ orderId, userId }, { step }) => {
let reservationId: string | null = null
let paymentId: string | null = null
try {
// Step 1 — reserve inventory
const reservation = await step.run('reserve-inventory', () =>
reserveInventory(orderId)
)
reservationId = reservation.id
// Step 2 — charge payment
const payment = await step.run('charge-payment', () =>
chargePayment(orderId, reservation.total)
)
paymentId = payment.id
// Step 3 — create shipment (may throw)
const shipment = await step.run('create-shipment', () =>
createShipment(orderId, reservationId)
)
// Step 4 — notify
await step.run('notify', () =>
sendConfirmation(userId, shipment.id)
)
return { success: true, shipmentId: shipment.id }
} catch (err) {
// Compensate in reverse order
if (paymentId) {
await step.run('refund-payment', () => refundPayment(paymentId!))
}
if (reservationId) {
await step.run('release-inventory', () => releaseInventory(reservationId!))
}
throw err // Re-throw so the run is marked failed
}
},
})Compensation Design
Best Practices
Keep Steps Focused
Each step.run() should do one thing. Smaller steps are easier to retry and debug.
Maximise Parallelism
Use Promise.all() with multiple step.run() calls for independent work.
Design for Failure
Every step can fail. Use try/catch with compensation steps for critical workflows that need rollback.
Idempotent Steps
Steps may run multiple times. Wrap external calls with idempotency keys or check-then-act patterns.