npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kowalski21/restate-saga

v0.3.0

Published

Saga pattern implementation for Restate durable workflows with automatic compensation

Readme

@kowalski21/restate-saga

GitHub

Saga pattern implementation for Restate durable workflows with automatic compensation.

Features

  • Automatic compensation - When a step fails, all previous steps are automatically rolled back in reverse order
  • Flexible step types - Choose between hybrid (createSagaStep) or strict (createSagaStepStrict) compensation modes
  • Global error registry - Register error classes that should always trigger compensation
  • Composable workflows - Embed workflows within workflows using runAsStep
  • Virtual Object support - Saga pattern for stateful keyed entities
  • Dependency Injection - Container-aware workflows with Awilix for per-request contexts (Directus, etc.) via defineSagaFactory
  • Type-safe - Full TypeScript support with type inference helpers

Installation

npm install @kowalski21/restate-saga

Peer dependency: Requires @restatedev/restate-sdk ^1.10.0

Quick Start

import {
  createSagaWorkflow,
  createSagaStep,
  StepResponse,
} from "@kowalski21/restate-saga";

// Define a step with compensation
const reserveInventory = createSagaStep({
  name: "ReserveInventory",
  run: async ({ input }) => {
    const reservation = await inventoryService.reserve(input.productId, input.quantity);
    return new StepResponse(
      { reservationId: reservation.id },  // Output
      { reservationId: reservation.id }   // Compensation data
    );
  },
  compensate: async (data) => {
    await inventoryService.release(data.reservationId);
  },
});

// Define another step
const chargePayment = createSagaStep({
  name: "ChargePayment",
  run: async ({ input }) => {
    const payment = await paymentService.charge(input.amount);
    return new StepResponse(
      { paymentId: payment.id },
      { paymentId: payment.id, amount: input.amount }
    );
  },
  compensate: async (data) => {
    await paymentService.refund(data.paymentId, data.amount);
  },
});

// Create the workflow
export const checkoutWorkflow = createSagaWorkflow(
  "CheckoutWorkflow",
  async (saga, input: { productId: string; quantity: number; amount: number }) => {
    // If chargePayment fails, reserveInventory.compensate() runs automatically
    const inventory = await reserveInventory(saga, input);
    const payment = await chargePayment(saga, { amount: input.amount });

    return { reservationId: inventory.reservationId, paymentId: payment.paymentId };
  }
);

Core Concepts

Saga Pattern

The saga pattern manages distributed transactions where each step has a corresponding compensation (undo) action. If a later step fails, all earlier compensations run in reverse order.

Step 1 → Step 2 → Step 3 (fails!)
                    ↓
         Compensate 2 ← Compensate 1

Step Types

createSagaStep (Hybrid)

Registers compensation before execution. Compensation runs even if the step fails partway through.

const step = createSagaStep({
  name: "CreateOrder",
  run: async ({ input }) => {
    // Compensation is already registered at this point
    const order = await orderService.create(input);
    return new StepResponse(output, compensationData);
  },
  compensate: async (data, { failed }) => {
    // `failed` tells you if the step threw an error
    await orderService.cancel(data.orderId);
  },
});

createSagaStepStrict (Medusa-style)

Registers compensation after success. Use when compensation requires data that only exists after completion.

const step = createSagaStepStrict({
  name: "CreateOrder",
  run: async ({ input }) => {
    const order = await orderService.create(input);
    return new StepResponse(
      { orderId: order.id },
      { orderId: order.id }  // Only available after success
    );
  },
  compensate: async (data) => {
    await orderService.cancel(data.orderId);
  },
});

Steps Without Compensation

For validation, read-only operations, or idempotent actions, omit the compensate function:

const validateInput = createSagaStep({
  name: "ValidateInput",
  run: async ({ input }) => {
    if (!input.email) {
      return StepResponse.permanentFailure("Email required", null);
    }
    return new StepResponse({ valid: true }, null);
  },
  // No compensate - validation has no side effects
});

Global Error Registry

Register error classes that should always trigger compensation without retrying:

import { registerTerminalErrors } from "@kowalski21/restate-saga";

class ValidationError extends Error {}
class NotFoundError extends Error {}

registerTerminalErrors([ValidationError, NotFoundError]);

// Now any step that throws these will trigger compensation
const myStep = createSagaStep({
  name: "MyStep",
  run: async ({ input }) => {
    throw new ValidationError("Invalid input"); // → Triggers compensation
  },
});

Composing Workflows

Use runAsStep to embed a workflow within another, sharing the compensation context:

const paymentWorkflow = createSagaWorkflow("PaymentWorkflow", async (saga, input) => {
  const auth = await authorizePayment(saga, input);
  const capture = await capturePayment(saga, { authId: auth.id });
  return { paymentId: capture.id };
});

const orderWorkflow = createSagaWorkflow("OrderWorkflow", async (saga, input) => {
  const order = await createOrder(saga, input);

  // Payment workflow's compensations join this saga
  const payment = await paymentWorkflow.runAsStep(saga, { amount: order.total });

  // If shipping fails, both order AND payment are compensated
  const shipment = await createShipment(saga, { orderId: order.id });

  return { orderId: order.id, paymentId: payment.paymentId };
});

Calling Restate Services

Saga workflows can call other Restate services, Virtual Objects, and workflows using the typed client helpers.

Calling a Restate Service

Use serviceClient to call regular Restate services from within a saga step:

import { createSagaStep, StepResponse, serviceClient } from "@kowalski21/restate-saga";
import { inventoryService } from "./services/inventory.js";

const checkAndReserve = createSagaStep({
  name: "CheckAndReserve",
  run: async ({ ctx, input }) => {
    // Call an external Restate service
    const inventory = serviceClient(ctx, inventoryService);
    const stock = await inventory.checkStock({ productId: input.productId });

    if (stock.available < input.quantity) {
      return StepResponse.permanentFailure("Insufficient stock", null);
    }

    const reservation = await inventory.reserve({
      productId: input.productId,
      quantity: input.quantity,
    });

    return new StepResponse(
      { reservationId: reservation.id },
      { reservationId: reservation.id }
    );
  },
  compensate: async (data) => {
    // Compensation logic...
  },
});

Calling a Virtual Object

Use objectClient to call keyed Virtual Objects:

import { objectClient } from "@kowalski21/restate-saga";
import { walletObject } from "./objects/wallet.js";

const debitWallet = createSagaStep({
  name: "DebitWallet",
  run: async ({ ctx, input }) => {
    // Call a Virtual Object by key
    const wallet = objectClient(ctx, walletObject, input.userId);
    const result = await wallet.debit({ amount: input.amount });

    return new StepResponse(
      { transactionId: result.transactionId },
      { userId: input.userId, amount: input.amount }
    );
  },
  compensate: async (data) => {
    // Refund on failure...
  },
});

Remote Workflow Calls vs runAsStep

There are two ways to call another saga workflow:

| Method | Compensation | Use When | |--------|--------------|----------| | workflowClient | Independent - child handles its own rollback | Workflows should succeed/fail independently | | runAsStep | Shared - child's compensations join parent | All-or-nothing transaction across workflows |

Remote call (independent compensation):

import { workflowClient } from "@kowalski21/restate-saga";
import { notificationWorkflow } from "./workflows/notification.js";

const orderWorkflow = createSagaWorkflow("OrderWorkflow", async (saga, input) => {
  const order = await createOrder(saga, input);

  // Remote call - if notification fails, it handles its own compensation
  // Order workflow continues or fails independently
  const notifyClient = workflowClient(saga.ctx, notificationWorkflow);
  await notifyClient.run({ userId: input.userId, message: "Order created" });

  return { orderId: order.id };
});

Embedded call (shared compensation):

import { paymentWorkflow } from "./workflows/payment.js";

const orderWorkflow = createSagaWorkflow("OrderWorkflow", async (saga, input) => {
  const order = await createOrder(saga, input);

  // Embedded - payment compensations join this saga's stack
  // If shipping fails later, payment is also rolled back
  const payment = await paymentWorkflow.runAsStep(saga, { amount: order.total });

  const shipment = await createShipment(saga, { orderId: order.id });

  return { orderId: order.id, paymentId: payment.paymentId };
});

Fire-and-Forget Calls

Use send clients for async calls that don't wait for completion:

import { serviceSendClient, workflowSendClient, objectSendClient } from "@kowalski21/restate-saga";

const completeOrder = createSagaStep({
  name: "CompleteOrder",
  run: async ({ ctx, input }) => {
    // Fire-and-forget: send email notification
    const emailService = serviceSendClient(ctx, emailService);
    await emailService.send({ to: input.email, template: "order-complete" });

    // Fire-and-forget: trigger analytics workflow
    const analytics = workflowSendClient(ctx, analyticsWorkflow);
    await analytics.run({ event: "order_completed", orderId: input.orderId });

    // Fire-and-forget: update user stats object
    const userStats = objectSendClient(ctx, userStatsObject, input.userId);
    await userStats.incrementOrderCount();

    return new StepResponse({ completed: true }, null);
  },
});

Virtual Objects

Create stateful entities with saga support:

import { createSagaVirtualObject } from "@kowalski21/restate-saga";

const wallet = createSagaVirtualObject(
  "Wallet",
  {
    // Exclusive handlers with saga support
    transfer: async (saga, ctx, input) => {
      const debit = await debitAccount(saga, { amount: input.amount });
      const credit = await creditAccount(saga, { toAccount: input.to, amount: input.amount });
      return { success: true };
    },
  },
  {
    // Shared handlers (read-only, no saga)
    getBalance: async (ctx) => {
      return await ctx.get("balance") || 0;
    },
  }
);

External Client Usage

Use InferServiceType to create a type-safe client with @restatedev/restate-sdk-clients.

See 01-basic-checkout.ts for a complete example.

// examples/01-basic-checkout.ts
import { createSagaWorkflow, InferServiceType } from "@kowalski21/restate-saga";

export const checkoutWorkflow = createSagaWorkflow(
  "CheckoutWorkflow",
  async (saga, input: { productId: string; quantity: number; amount: number; ... }) => {
    const inventory = await reserveInventory(saga, { ... });
    const payment = await chargePayment(saga, { ... });
    const shipment = await createShipment(saga, { ... });
    return { reservationId, paymentId, trackingNumber };
  }
);

// Export the type for external clients
export type CheckoutWorkflow = InferServiceType<typeof checkoutWorkflow>;
// client.ts
import * as clients from "@restatedev/restate-sdk-clients";
import type { CheckoutWorkflow } from "./examples/01-basic-checkout.js";

const restateClient = clients.connect({ url: "http://localhost:8080" });

// Type-safe client usage - name is constrained to "CheckoutWorkflow"
const result = await restateClient
  .serviceClient<CheckoutWorkflow>({ name: "CheckoutWorkflow" })
  .run({ productId: "89", quantity: 34, amount: 40 });

// TypeScript error if you use the wrong name:
// .serviceClient<CheckoutWorkflow>({ name: "WrongName" })
// Error: Type '"WrongName"' is not assignable to type '"CheckoutWorkflow"'

Container-Aware Workflows (Dependency Injection)

For applications like Directus where services need per-request context (accountability, schema), use the container-aware workflow pattern with Awilix.

Basic Container Workflow

import { createContainer, asValue } from "awilix";
import {
  createContainerWorkflow,
  createContainerStep,
  StepResponse,
} from "@kowalski21/restate-saga";

// 1. Define your services type
interface AppServices {
  ordersService: ItemsService;
  inventoryService: ItemsService;
  database: Knex;
}

// 2. Create container-aware steps
const createOrder = createContainerStep<AppServices>()({
  name: "CreateOrder",
  run: async (saga, input: { userId: string; items: any[] }) => {
    // saga.services is typed and available
    const orderId = await saga.services.ordersService.createOne({
      user: input.userId,
      items: input.items,
    });
    return new StepResponse({ orderId }, { orderId });
  },
  compensate: async (saga, data) => {
    // saga.services available in compensation too
    if ("orderId" in data) {
      await saga.services.ordersService.deleteOne(data.orderId);
    }
  },
});

// 3. Create and configure container
const container = createContainer<AppServices>();
container.register({
  ordersService: asValue(new ItemsService("orders", context)),
  inventoryService: asValue(new ItemsService("inventory", context)),
  database: asValue(knex),
});

// 4. Create workflow with container
const orderWorkflow = createContainerWorkflow(
  container,
  "OrderWorkflow",
  async (saga, input: { userId: string; items: any[] }) => {
    const order = await createOrder(saga, input);
    return { orderId: order.orderId };
  }
);

// 5. Register with Restate
restate.endpoint().bind(orderWorkflow).listen();

Scoped Workflow Factory (Recommended for Directus)

The defineSagaFactory pattern lets you define scope creation logic once and reuse it across all workflows. This is the recommended approach for Directus and similar applications.

import { createContainer, asValue, asFunction } from "awilix";
import { defineSagaFactory, StepResponse } from "@kowalski21/restate-saga";

// =============================================================================
// 1. Define Cradle Types
// =============================================================================

// Root cradle - app-level singletons
interface RootCradle {
  knex: Knex;
  logger: Logger;
}

// Scoped cradle - request-specific services
interface AppCradle extends RootCradle {
  accountability: Accountability | null;
  schema: SchemaOverview;
  ordersService: ItemsService;
  paymentsService: ItemsService;
  shipmentsService: ItemsService;
}

// Context passed in workflow input
interface DirectusContext {
  accountability: Accountability | null;
  schema: SchemaOverview;
}

// =============================================================================
// 2. Define Factory (once per application)
// =============================================================================

export const { createWorkflow, createStep } = defineSagaFactory<RootCradle, AppCradle>({
  // Called per-workflow invocation to create scoped container
  createScope: (rootContainer, input: { _ctx?: DirectusContext }) => {
    const ctx = input._ctx!;
    const scope = rootContainer.createScope<AppCradle>();

    // Register request-specific values
    scope.register({
      accountability: asValue(ctx.accountability),
      schema: asValue(ctx.schema),
    });

    // Register services that depend on request context
    scope.register({
      ordersService: asFunction(({ knex, schema, accountability }) =>
        new ItemsService("orders", { knex, schema, accountability })
      ).scoped(),
      paymentsService: asFunction(({ knex, schema, accountability }) =>
        new ItemsService("payments", { knex, schema, accountability })
      ).scoped(),
      shipmentsService: asFunction(({ knex, schema, accountability }) =>
        new ItemsService("shipments", { knex, schema, accountability })
      ).scoped(),
    });

    return scope;
  },

  // Optional: control scope disposal (default: true)
  disposeScope: true,
});

// =============================================================================
// 3. Define Steps (types inferred from factory)
// =============================================================================

const createOrder = createStep({
  name: "CreateOrder",
  run: async (saga, input: { customerId: string; items: unknown[] }) => {
    // saga.services is AppCradle - fully typed!
    const order = await saga.services.ordersService.createOne({
      customer: input.customerId,
      items: input.items,
      status: "pending",
    });
    saga.services.logger.info(`Order created: ${order.id}`);
    return new StepResponse({ orderId: order.id }, { orderId: order.id });
  },
  compensate: async (saga, data) => {
    if ("orderId" in data) {
      await saga.services.ordersService.deleteOne(data.orderId);
    }
  },
});

const processPayment = createStep({
  name: "ProcessPayment",
  run: async (saga, input: { orderId: string; amount: number }) => {
    const payment = await saga.services.paymentsService.createOne({
      order: input.orderId,
      amount: input.amount,
      status: "charged",
    });
    return new StepResponse({ paymentId: payment.id }, { paymentId: payment.id });
  },
  compensate: async (saga, data) => {
    if ("paymentId" in data) {
      await saga.services.paymentsService.deleteOne(data.paymentId);
    }
  },
});

// =============================================================================
// 4. Define Workflows (clean - no type annotations needed)
// =============================================================================

interface CheckoutInput {
  customerId: string;
  items: unknown[];
  amount: number;
  _ctx?: DirectusContext;
}

export const checkoutWorkflow = createWorkflow(
  "DirectusCheckout",
  async (saga, input: CheckoutInput) => {
    const order = await createOrder(saga, {
      customerId: input.customerId,
      items: input.items,
    });

    const payment = await processPayment(saga, {
      orderId: order.orderId,
      amount: input.amount,
    });

    return { orderId: order.orderId, paymentId: payment.paymentId };
  }
);

// =============================================================================
// 5. Setup (at application startup)
// =============================================================================

// Create root container with singletons
const rootContainer = createContainer<RootCradle>();
rootContainer.register({
  knex: asValue(database),
  logger: asValue(logger),
});

// Instantiate workflow with root container
const workflow = checkoutWorkflow(rootContainer);

// Register with Restate
restate.endpoint().bind(workflow).listen(9080);

// =============================================================================
// 6. Usage (in Directus endpoint)
// =============================================================================

export default defineEndpoint((router, context) => {
  router.post("/checkout", async (req, res) => {
    const result = await restateClient
      .serviceClient(workflow)
      .run({
        customerId: req.body.customerId,
        items: req.body.items,
        amount: req.body.amount,
        _ctx: {
          accountability: req.accountability,
          schema: req.schema,
        },
      });

    res.json(result);
  });
});

Scope Disposal Strategies

Control when and how scoped containers are disposed:

const factory = defineSagaFactory<RootCradle, AppCradle>({
  createScope: ...,

  // Always dispose after workflow (default)
  disposeScope: true,

  // Never auto-dispose (manage lifecycle manually)
  disposeScope: false,

  // Only dispose on successful completion
  disposeScope: "on-success",

  // Custom disposal logic
  disposeScope: async (scope, error) => {
    if (!error) {
      await scope.dispose();
    }
    // Log errors, cleanup resources, etc.
  },
});

Nested Workflows Share Parent Scope

When calling workflow handlers directly, they share the parent's scoped container:

// Define a reusable handler
const paymentHandler = async (
  saga: Parameters<Parameters<typeof createWorkflow>[1]>[0],
  input: { orderId: string; amount: number }
) => {
  const payment = await processPayment(saga, input);
  return { paymentId: payment.paymentId };
};

// Can also be exposed as a standalone workflow
export const paymentWorkflow = createWorkflow("Payment", paymentHandler);

// Parent workflow calls handler directly (shares scope)
export const orderWorkflow = createWorkflow(
  "Order",
  async (saga, input: OrderInput) => {
    const order = await createOrder(saga, input);

    // Direct call - shares parent's scope, compensations join parent stack
    const payment = await paymentHandler(saga, {
      orderId: order.orderId,
      amount: input.amount,
    });

    return { orderId: order.orderId, paymentId: payment.paymentId };
  }
);

Factory Pattern (Per-Request Containers)

For more control over container creation, use defineContainerWorkflow directly:

import {
  defineContainerWorkflow,
  defineContainerRestateWorkflow,
  createContainerStep,
  StepResponse,
} from "@kowalski21/restate-saga";

// Define your services type
interface DirectusServices {
  ordersService: ItemsService;
  customersService: ItemsService;
  paymentsService: ItemsService;
}

// Define steps (reusable across all requests)
const createOrder = createContainerStep<DirectusServices>()({
  name: "CreateOrder",
  run: async (saga, input: { customerId: string; items: any[] }) => {
    const order = await saga.services.ordersService.createOne({
      customer: input.customerId,
      items: input.items,
      status: "pending",
    });
    return new StepResponse({ orderId: order.id }, { orderId: order.id });
  },
  compensate: async (saga, data) => {
    if ("orderId" in data) {
      await saga.services.ordersService.deleteOne(data.orderId);
    }
  },
});

const processPayment = createContainerStep<DirectusServices>()({
  name: "ProcessPayment",
  run: async (saga, input: { orderId: string; amount: number }) => {
    const payment = await saga.services.paymentsService.createOne({
      order: input.orderId,
      amount: input.amount,
      status: "completed",
    });
    return new StepResponse({ paymentId: payment.id }, { paymentId: payment.id });
  },
  compensate: async (saga, data) => {
    if ("paymentId" in data) {
      await saga.services.paymentsService.updateOne(data.paymentId, {
        status: "refunded",
      });
    }
  },
});

// Define workflow factory (no container bound yet)
const createOrderWorkflow = defineContainerWorkflow<DirectusServices>()(
  "OrderWorkflow",
  async (saga, input: { customerId: string; items: any[]; amount: number }) => {
    const order = await createOrder(saga, {
      customerId: input.customerId,
      items: input.items,
    });

    const payment = await processPayment(saga, {
      orderId: order.orderId,
      amount: input.amount,
    });

    return { orderId: order.orderId, paymentId: payment.paymentId };
  }
);

// In Directus endpoint - create with request-specific container
export default defineEndpoint((router, context) => {
  router.post("/orders", async (req, res) => {
    const { accountability, schema } = req;

    // Create container with request-specific services
    const container = createContainer<DirectusServices>();
    container.register({
      ordersService: asValue(new ItemsService("orders", { schema, accountability, knex: context.database })),
      customersService: asValue(new ItemsService("customers", { schema, accountability, knex: context.database })),
      paymentsService: asValue(new ItemsService("payments", { schema, accountability, knex: context.database })),
    });

    // Create workflow instance with this container
    const workflow = createOrderWorkflow(container);

    // Register with Restate
    restate.endpoint().bind(workflow).listen(9080);

    // Or invoke via Restate client
    const client = restate.connect({ url: "http://localhost:8080" });
    const result = await client.serviceClient(workflow).run(req.body);

    res.json(result);
  });
});

Nested Container Workflows

When using runAsStep, nested workflows inherit the parent's saga context (including services):

// Define payment workflow factory
const createPaymentWorkflow = defineContainerWorkflow<DirectusServices>()(
  "PaymentWorkflow",
  async (saga, input: { orderId: string; amount: number }) => {
    const payment = await processPayment(saga, input);
    return { paymentId: payment.paymentId };
  }
);

// Define order workflow that nests payment
const createOrderWorkflow = defineContainerWorkflow<DirectusServices>()(
  "OrderWorkflow",
  async (saga, input) => {
    const order = await createOrder(saga, input);

    // Get payment workflow instance (must be created with same container)
    // Note: paymentWorkflow must be in scope - see setup below
    const payment = await paymentWorkflow.runAsStep(saga, {
      orderId: order.orderId,
      amount: input.amount,
    });

    return { orderId: order.orderId, paymentId: payment.paymentId };
  }
);

// Setup: create both workflows with same container
router.post("/orders", async (req, res) => {
  const container = createRequestContainer(req);

  // Create both with same container
  const paymentWorkflow = createPaymentWorkflow(container);
  const orderWorkflow = createOrderWorkflow(container);

  // Register both
  restate.endpoint()
    .bind(orderWorkflow)
    .bind(paymentWorkflow)
    .listen(9080);
});

Important: When using runAsStep, the nested workflow uses the parent's saga context. The container used to create the nested workflow only matters when invoked directly via Restate.

Type Inference Helpers

Extract types from container workflows for use with clients:

import {
  InferContainerServiceType,
  InferContainerCradle,
  InferContainerInput,
  InferContainerOutput,
  InferContainerWorkflow,
} from "@kowalski21/restate-saga";

const orderWorkflow = createContainerWorkflow(container, "OrderWorkflow", handler);

// Individual type extraction
type OrderService = InferContainerServiceType<typeof orderWorkflow>;
type OrderCradle = InferContainerCradle<typeof orderWorkflow>;
type OrderInput = InferContainerInput<typeof orderWorkflow>;
type OrderOutput = InferContainerOutput<typeof orderWorkflow>;

// Or get all at once
type Order = InferContainerWorkflow<typeof orderWorkflow>;
// Order.Name       = "OrderWorkflow"
// Order.Input      = { customerId: string; items: any[]; amount: number }
// Order.Output     = { orderId: string; paymentId: string }
// Order.Cradle     = DirectusServices
// Order.ServiceType = restate.ServiceDefinition<"OrderWorkflow", ...>

// Use with Restate client
const result = await client
  .serviceClient<OrderService>({ name: "OrderWorkflow" })
  .run(input);

Restate Workflows (Long-Running)

For long-running workflows with signals and queries:

const createLongRunningWorkflow = defineContainerRestateWorkflow<DirectusServices>()(
  "ApprovalWorkflow",
  async (saga, ctx, input: { orderId: string }) => {
    const order = await createOrder(saga, input);

    // Wait for approval signal (durable promise)
    const approved = await ctx.promise<boolean>("approval");

    if (!approved) {
      throw new restate.TerminalError("Order rejected");
    }

    const shipment = await createShipment(saga, { orderId: order.orderId });
    return { orderId: order.orderId, shipmentId: shipment.id };
  },
  {
    // Additional handlers (signals/queries)
    approve: async (ctx, input: { approved: boolean }) => {
      ctx.resolvePromise("approval", input.approved);
    },
    getStatus: async (ctx) => {
      return ctx.promise<string>("status");
    },
  }
);

API Reference

Steps

  • createSagaStep(opts) - Create a step with hybrid compensation
  • createSagaStepStrict(opts) - Create a step with strict compensation
  • StepResponse - Response class for step results
  • StepResponse.permanentFailure(message, data) - Create a failure response

Workflows

  • createSagaWorkflow(name, handler, options?) - Create a saga workflow service
  • createSagaRestateWorkflow(name, run, handlers?, options?) - Create a Restate Workflow with saga support

Virtual Objects

  • createSagaVirtualObject(name, handlers, sharedHandlers?, options?) - Create a Virtual Object with saga support

Error Registry

  • registerTerminalErrors(errorClasses) - Register error classes as terminal
  • unregisterTerminalErrors(errorClasses) - Unregister error classes
  • clearTerminalErrors() - Clear all registered errors
  • setGlobalErrorMapper(mapper) - Set a custom error mapper

Client Helpers

  • InferServiceType<T> - Extract service type for use with external clients
  • serviceClient(ctx, definition) - Create a typed client for Restate services
  • serviceSendClient(ctx, definition) - Create a fire-and-forget client for Restate services
  • workflowClient(ctx, definition) - Create a typed client for saga workflows
  • workflowSendClient(ctx, definition) - Create a fire-and-forget client for saga workflows
  • objectClient(ctx, definition, key) - Create a typed client for Virtual Objects
  • objectSendClient(ctx, definition, key) - Create a fire-and-forget client for Virtual Objects

Container / Dependency Injection (Awilix)

  • defineSagaFactory<RootCradle, ScopedCradle>(config) - Create a scoped workflow factory (recommended)
  • createContainerStep<TCradle>() - Create a step with DI support
  • createContainerStepStrict<TCradle>() - Create a strict step with DI support
  • createContainerWorkflow(container, name, handler, options?) - Create a workflow with container
  • createContainerRestateWorkflow(container, name, run, handlers?, options?) - Create a Restate Workflow with container
  • defineContainerWorkflow<TCradle>() - Factory pattern for per-request containers
  • defineContainerRestateWorkflow<TCradle>() - Factory pattern for per-request Restate Workflows

Scoped Factory Types

  • SagaFactory<RootCradle, ScopedCradle> - Return type of defineSagaFactory
  • SagaFactoryConfig<RootCradle, ScopedCradle> - Configuration for defineSagaFactory
  • ScopeDisposalStrategy - Disposal options: true | false | "on-success" | custom function

Container Type Helpers

  • InferContainerServiceType<T> - Extract Restate service type for clients
  • InferContainerCradle<T> - Extract the services (TCradle) type
  • InferContainerInput<T> - Extract workflow input type
  • InferContainerOutput<T> - Extract workflow output type
  • InferContainerName<T> - Extract workflow name literal
  • InferContainerWorkflow<T> - Extract all types as an object

Nested Sagas

  • runNestedSaga(saga, handler) - Run inline saga logic with shared compensation
  • createSagaModule(handler) - Create a reusable saga module

Examples

See the examples/ directory for complete working examples:

| Example | Description | |---------|-------------| | 01-basic-checkout.ts | Simple e-commerce checkout with multi-step compensation and InferServiceType usage | | 02-user-registration.ts | Registration flow with validation and optional compensation | | 03-composed-workflows.ts | Workflow composition using runAsStep | | 04-virtual-object.ts | Stateful wallet entity with saga support | | 05-strict-compensation.ts | Hybrid vs strict compensation modes | | 06-error-handling.ts | Error registry, mappers, and handling strategies | | 07-container-workflow.ts | Container-aware workflows with Awilix DI | | 08-directus-factory.ts | Factory pattern for Directus per-request containers | | 09-saga-factory.ts | Scoped workflow factory with defineSagaFactory (recommended for Directus) |

To run an example:

# Start Restate server
restate-server

# Run the example
npx ts-node examples/01-basic-checkout.ts

# Register with Restate
restate deployments register http://localhost:9080

# Invoke the workflow
curl -X POST http://localhost:8080/CheckoutWorkflow/run \
  -H "Content-Type: application/json" \
  -d '{"productId": "SKU123", "quantity": 2, "amount": 99.99, "currency": "USD", "address": "123 Main St"}'

License

MIT