Skip to main content

Task Workflows

Advanced

Build complex multi-step workflows using durable steps, parallel fan-out, and event-driven patterns.

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.

tasks/process-order.ts
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.

Fan-out with batchTrigger
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:

Collect results
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

Use 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:

tasks/order-saga.ts
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

Design compensation functions to be idempotent. They may be called multiple times if the saga retries. Always check if the action has already been reversed before performing the rollback.

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.

Step Caching

Completed step results are cached by step name. If the same task run retries, steps that already completed are skipped and their cached result is returned instantly. This makes workflows both fault-tolerant and efficient.