Workflows

A WorkflowSpec orchestrates multi-step processes. It defines the sequence of operations, handles failures with retries and compensation, and provides observability into long-running tasks.

Core concepts

Identifiers

Each workflow has a unique workflowId and a version. This allows you to run multiple versions of the same workflow simultaneously during migrations or A/B tests.

Steps

A workflow is composed of steps. Each step invokes a CapabilitySpec, passes inputs, and receives outputs. Steps can run sequentially or in parallel.

Transitions

Transitions define the flow between steps. They can be conditional (e.g., "if payment succeeds, go to step 3; otherwise, go to step 5") or unconditional.

Retries

If a step fails, the workflow can retry it with exponential backoff. You specify the maximum number of retries and the backoff strategy in the spec.

Compensation

When a workflow fails partway through, compensation steps undo the effects of completed steps (e.g., refunding a payment, releasing a reservation). This ensures consistency even in failure scenarios.

SLAs

You can define Service Level Agreements (SLAs) for each step or the entire workflow. If a step exceeds its SLA, the system can trigger alerts or escalations.

Example WorkflowSpec (TypeScript)

Here's a simplified example of a payment workflow in TypeScript:

import { defineWorkflow } from '@lssm/lib.contracts';
import { ValidatePaymentMethod, ChargePayment, SendEmail } from './specs';

export const PaymentFlow = defineWorkflow({
  meta: {
    name: 'payment.flow',
    version: 1,
    description: 'End-to-end payment processing',
    owners: ['team-payments'],
    tags: ['payments'],
    stability: 'stable',
  },
  steps: [
    {
      id: 'validate-payment',
      operation: ValidatePaymentMethod,
      inputs: (ctx, input) => ({
        userId: ctx.userId,
        paymentMethodId: input.paymentMethodId,
      }),
      retry: {
        maxAttempts: 3,
        backoff: 'exponential',
      },
      onSuccess: 'charge-payment',
      onFailure: 'notify-user',
    },
    {
      id: 'charge-payment',
      operation: ChargePayment,
      inputs: (ctx, input, steps) => ({
        amount: input.amount,
        paymentMethodId: input.paymentMethodId,
      }),
      compensation: 'refund-payment',
      onSuccess: 'send-receipt',
      onFailure: 'notify-admin',
    },
    {
      id: 'send-receipt',
      operation: SendEmail,
      inputs: (ctx, input, steps) => ({
        to: ctx.userEmail,
        template: 'payment-receipt',
        data: steps['charge-payment'].output,
      }),
    },
  ],
  sla: {
    maxDuration: 30000, // milliseconds
    alertOnBreach: true,
  },
});

Triggers

Workflows can be triggered in several ways:

  • Manual invocation – A user or system calls the workflow via an API endpoint.
  • Event-driven – The workflow starts automatically when a specific event occurs (e.g., a new order is created).
  • Scheduled – The workflow runs on a cron schedule (e.g., nightly batch processing).
  • Chained – One workflow can invoke another as a step.

Monitoring and versioning

ContractSpec automatically instruments workflows with telemetry. You can view:

  • Real-time execution status for each step
  • Historical run data and success/failure rates
  • Latency distributions and SLA compliance
  • Compensation events and retry attempts

When you update a workflow, you increment its version. Running workflows continue on their original version, while new invocations use the latest version. This allows safe, zero-downtime deployments.

Best practices

  • Keep steps idempotent – they should be safe to retry without side effects.
  • Define compensation for any step that modifies external state.
  • Use meaningful step IDs that describe the operation.
  • Set realistic SLAs and monitor them in production.
  • Test failure scenarios locally before deploying.