Skip to content

Instantly share code, notes, and snippets.

@haf
Created June 18, 2025 15:43
Show Gist options
  • Save haf/bc1380d35357417bd7a7dee825c44e4a to your computer and use it in GitHub Desktop.
Save haf/bc1380d35357417bd7a7dee825c44e4a to your computer and use it in GitHub Desktop.
restate-llm.md

Restate TypeScript SDK Reference Guide

This document provides a comprehensive guide to developing resilient applications with the Restate TypeScript SDK. It covers fundamental concepts, advanced patterns, and practical examples to help you build robust and scalable systems.


Page 1: Introduction to Restate & Core Concepts

Restate is a system designed to simplify the development of reliable, distributed applications. It provides durable execution, state management, and powerful primitives that turn familiar programming constructs into fault-tolerant, distributed building blocks. At its core, Restate ensures that your code, once invoked, runs to completion, even in the presence of failures.

Key Concepts

  • Durable Execution: Restate ensures that your code runs to completion by automatically retrying failed operations and recovering previously finished actions from a journal. This makes your business logic resilient to transient failures and infrastructure restarts without complex manual error handling.
  • Services: The fundamental building blocks of a Restate application. They are stateless and contain handlers that implement your business logic. They can be invoked via RPC from other services or external clients.
  • Virtual Objects: Stateful services with access to a built-in, consistent key-value store. Each object is keyed, and Restate guarantees that only one handler executes at a time for a given key, ensuring data consistency and preventing race conditions without manual locking. This is often referred to as the Virtual Actor model.
  • Workflows: A specialized type of virtual object for orchestrating long-running, durable processes. They have a main run handler that is guaranteed to execute exactly once per workflow instance ID. They can be queried for their status, signaled with external events, and awaited for their result.

Example: A Simple Durable Service

The following example demonstrates a service that durably executes a series of steps. If the service crashes, Restate will automatically restart it and replay its progress, ensuring that sendNotification is not called again and sendReminder is retried until it succeeds.

import * as restate from "@restatedev/restate-sdk";
import { sendNotification, sendReminder } from "./utils";

const greeter = restate.service({
  name: "Greeter",
  handlers: {
    greet: async (ctx: restate.Context, name: string) => {
      // Durably execute a set of steps; resilient against failures
      const greetingId = ctx.rand.uuidv4();
      await ctx.run("Notification", () => sendNotification(greetingId, name));
      await ctx.sleep(1000); // Durably sleep for 1 second
      await ctx.run("Reminder", () => sendReminder(greetingId, name));

      // Respond to the caller
      return `Hi ${name}!`;
    },
  },
});

restate.endpoint().bind(greeter).listen(9080);

Page 2: The Restate Context: Your Gateway to Durable Operations

The Context object (ctx) is the first parameter in every handler and is the primary interface for accessing Restate's features. Different handler types (service, object, workflow) receive specialized versions of the context with additional capabilities.

Core Context Features (Context)

  • ctx.run(): Executes a function durably, journaling its result to ensure it's not re-executed on retries. This is essential for all side effects.
  • ctx.serviceClient(): Creates a client for making reliable RPC calls to other services.
  • ctx.console: A logger that is aware of Restate's execution model. It automatically adds invocation context (like the invocation ID) to log messages and suppresses logs during retries to avoid noise.
  • ctx.rand: Provides deterministic random number and UUID generators, crucial for creating stable idempotency keys across retries.
    • ctx.rand.random(): Like Math.random() but deterministic.
    • ctx.rand.uuidv4(): Generates a deterministic UUID.
  • ctx.sleep(): A durable sleep that can pause a handler's execution for a specified duration.
  • ctx.awakeable(): Creates a durable promise that can be resolved or rejected by an external system, enabling reliable callback patterns.
  • ctx.promise(): (Workflow-only) Creates a durable promise scoped to the workflow instance for signaling between handlers.

Object Context (ObjectContext)

In addition to core features, ObjectContext provides access to the virtual object's key-value state store.

  • ctx.key: The unique key identifying the virtual object instance.
  • ctx.get(): Retrieves a value from the object's state.
  • ctx.set(): Sets a value in the object's state.
  • ctx.clear(): Deletes a key from the object's state.

Workflow Context (WorkflowContext)

Workflows have the most extensive context, combining the features of ObjectContext with workflow-specific operations. Shared workflow handlers (signal and query) use WorkflowSharedContext, which has a read-only view of state.


Page 3: Durable Execution: ctx.run() and Retries

The cornerstone of Restate's resilience is durable execution. Any interaction with the outside world (APIs, databases, etc.) or any non-deterministic code must be wrapped in ctx.run() to ensure correctness during retries.

How ctx.run() Works

When you wrap code in ctx.run(), Restate does the following:

  1. On first execution: It executes the code block and journals the result (or the error).
  2. On success: It proceeds to the next step.
  3. On failure: The entire invocation is retried.
  4. On replay: Instead of re-executing the code block, Restate replays the journaled result, ensuring the handler's logic continues from a consistent state.

Fine-Grained Retry Control (RunOptions)

You can customize the retry behavior for ctx.run() by providing a RunOptions object. This is useful for handling external systems that might have specific rate limits or failure characteristics.

Example: A ctx.run() with a custom retry policy

await ctx.run(
  "Call a flaky API",
  () => callFlakyApi(),
  {
    // These options configure an exponential backoff strategy.
    initialRetryInterval: { seconds: 1 }, // Start with a 1-second delay
    maxRetryInterval: { minutes: 1 },    // Cap the delay at 1 minute
    maxRetryAttempts: 5,                 // Give up after 5 attempts
    retryIntervalFactor: 2.0             // Double the delay after each attempt
  }
);

If the action does not succeed after all attempts, ctx.run() will throw a TerminalError.


Page 4: Services, Virtual Objects, and Workflows

Restate applications are composed of one or more of these component types, each serving a different purpose.

Service

A stateless component for implementing request-response logic.

export const checkoutService = restate.service({
  name: "CheckoutService",
  handlers: {
    handle: async (ctx: restate.Context, request: { userId: string; tickets: string[] }) => {
      // ... business logic
      return true;
    },
  }
});

Virtual Object

A stateful component, keyed by an ID, for modeling entities like user carts, tickets, or IoT devices.

export const ticketObject = restate.object({
  name: "TicketObject",
  handlers: {
    reserve: async (ctx: restate.ObjectContext) => {
      const status = await ctx.get<TicketStatus>("status") ?? TicketStatus.Available;
      if (status === TicketStatus.Available) {
        ctx.set("status", TicketStatus.Reserved);
        return true;
      }
      return false;
    },
    // Read-only, concurrent handler
    getStatus: restate.handlers.object.shared(async (ctx: restate.ObjectSharedContext) => {
        return await ctx.get<TicketStatus>("status");
    })
  }
});

Workflow

An orchestrator for long-running, multi-step processes.

export const signupWorkflow = restate.workflow({
  name: "usersignup",
  handlers: {
    run: async (ctx: restate.WorkflowContext, user: { name: string; email: string }) => {
      // ... workflow logic
    },
    click: async (ctx: restate.WorkflowSharedContext, request: { secret: string }) => {
      // ... signal handler logic
    },
  },
});

Page 5: State Management: Durable Key-Value Store

Virtual objects and workflows provide a durable key-value store, accessed via the context (ctx). Restate manages the persistence and consistency of this state.

State Operations

  • ctx.get<T>(name: string): Promise<T | null>: Retrieves the state for a given key.
  • ctx.set<T>(name: string, value: T): void: Sets the state for a key.
  • ctx.clear(name: string): void: Deletes a key.
  • ctx.stateKeys(): Promise<string[]>: Returns all keys for the current object.

Exclusive vs. Shared Handlers

By default, handlers in a virtual object are exclusive, meaning Restate ensures only one is executing for a given object key at any time. This serializes access and prevents race conditions.

You can declare a handler as shared using restate.handlers.object.shared(). Shared handlers can execute concurrently but have read-only access to state (ctx.get() is allowed, but ctx.set() and ctx.clear() are not). This is ideal for query-style methods.


Page 6: Reliable Communication Patterns

Restate simplifies inter-service communication by making it durable and reliable.

Request-Response (RPC)

Use a service client to make a request to another handler and await its response. Restate guarantees the call will be delivered and executed at least once.

  • ctx.serviceClient()
  • ctx.objectClient()
  • ctx.workflowClient()

Example: Idempotent RPC

To ensure exactly-once semantics, especially when interacting with external systems, provide an idempotency key.

const products = restateClient.objectClient<ProductService>({ name: "product" }, productId);
const reservation = await products.reserve(
  // Restate deduplicates requests with the same idempotency key
  Opts.from({ idempotencyKey: reservationId })
);

One-Way Messaging (Send)

Use a send client to fire-and-forget a message to another handler. Restate guarantees eventual delivery.

  • ctx.serviceSendClient()
  • ctx.objectSendClient()
  • ctx.workflowSendClient()

Example: Delayed One-Way Message

// Expire a ticket reservation after 15 minutes
ctx.objectSendClient(cartObject, ctx.key, { delay: { minutes: 15 } })
   .expireTicket(ticketId);

This demonstrates how to use Restate as a durable, delayed task queue.


Page 7: Timers, Promises, and Asynchronous Coordination

Restate provides powerful primitives for managing time and coordinating asynchronous operations.

Durable Timers

ctx.sleep(duration) creates a durable timer. The handler execution will suspend and resume after the specified duration, even across failures and restarts.

Awakeables: Callbacks from the Outside World

Awakeables bridge the gap between Restate and external asynchronous systems. A handler can create an awakeable, pass its id to an external system, and await its promise. The external system can then use that ID to call back into Restate and resolve the promise.

Example: Stripe Webhook Integration

// In the payment handler:
const { id: intentWebhookId, promise: intentPromise } = ctx.awakeable<Stripe.PaymentIntent>();
const paymentIntent = await ctx.run("stripe call", () =>
  stripe_utils.createPaymentIntent({
    // ...
    // Pass the awakeable ID in the payment metadata
    intentWebhookId,
  })
);

// If Stripe's response is "processing", we wait for the webhook
if (paymentIntent.status === "processing") {
  const processedPaymentIntent = await intentPromise; // Await the webhook callback
  // ... continue logic
}

// In the webhook handler:
const paymentIntent = event.data.object as Stripe.PaymentIntent;
const webhookPromiseId = paymentIntent.metadata.restate_callback_id;
if (webhookPromiseId) {
  // Resolve the awakeable to resume the waiting handler
  ctx.resolveAwakeable(webhookPromiseId, paymentIntent);
}

Durable Promises: Workflow Signaling

ctx.promise<T>(name) (in a workflow context) creates a promise scoped to that workflow instance. It can be used for reliable signaling between the workflow's run handler and its shared handlers.


Page 8: Error Handling and Sagas

Restate provides robust error handling capabilities. By default, any thrown error causes the invocation to be retried.

Terminal Errors

If an error is not transient and should not be retried (e.g., "invalid credit card"), you should throw a TerminalError. This stops the retry loop and fails the invocation immediately.

if (!paid) {
  // This will not be retried.
  throw new restate.TerminalError("Payment was rejected.", { errorCode: 400 });
}

Other specialized terminal errors include TimeoutError and CancelledError.

The Saga Pattern for Compensations

The Saga pattern is a way to manage data consistency across multiple services in a distributed transaction. If a step in the process fails with a terminal error, previously completed steps must be compensated for (undone). Restate's durable execution makes implementing sagas straightforward.

Example: Trip Booking Saga

In this pattern, for each successful action (e.g., booking a flight), we add a corresponding compensating action (e.g., canceling the flight) to a list. If a later step fails terminally, we execute the compensating actions in reverse order.

const bookingWorkflow = restate.service({
  name: "BookingWorkflow",
  handlers: {
    run: async (ctx: restate.Context, req: BookingRequest) => {
      const compensations: (() => Promise<void>)[] = [];

      try {
        await ctx.run("Book flight", () => flightClient.book(req.customerId, req.flight));
        compensations.push(() => ctx.run("Cancel flight", () => flightClient.cancel(req.customerId)));

        await ctx.run("Book car", () => carRentalClient.book(req.customerId, req.car));
        compensations.push(() => ctx.run("Cancel car", () => carRentalClient.cancel(req.customerId)));
        
        // This hotel booking will fail terminally
        await ctx.run("Book hotel", () => hotelClient.book(req.customerId, req.hotel));
        compensations.push(() => ctx.run("Cancel hotel", () => hotelClient.cancel(req.customerId)));

      } catch (e) {
        if (e instanceof restate.TerminalError) {
          // Execute all compensations in reverse order
          for (const compensation of compensations.reverse()) {
            await compensation();
          }
        }
        throw e;
      }
    },
  },
});

Page 9: Event-Driven Architecture with Kafka

Restate can serve as a sink for Kafka topics, enabling you to build robust, transactional event processing applications. The key from the Kafka message is used to route the event to a specific virtual object instance, ensuring ordered, sequential processing for that key.

Configuration

You configure Restate to consume from a Kafka topic and invoke a specific service handler. This is done via the Restate admin API or a configuration file.

Example: restate.toml

[[ingress.kafka-clusters]]
name = "my-cluster"
brokers = ["PLAINTEXT://kafka:9092"]

Example: Subscribing a Handler

curl localhost:9070/subscriptions -H 'content-type: application/json' \
-d '{
    "source": "kafka://my-cluster/social-media-posts",
    "sink": "service://userFeed/processPost"
}'

Publishing to Kafka

While Restate doesn't have a direct Kafka producer API in the SDK, you can publish events to Kafka from within a ctx.run() block using a standard Kafka client library like kafkajs. This ensures the message is sent durably as part of the invocation's transactional guarantees. The food-ordering example demonstrates this pattern by having a simulated driver app publish location updates.

// From food-ordering/app/src/order-app/clients/kafka_publisher.ts
import { Kafka } from "kafkajs";

export class Kafka_publisher {
  // ... Kafka producer setup ...

  public async send(driverId: string, location: Location) {
    // ... connect logic ...
    await this.producer.send({
      topic: "driver-updates",
      messages: [{key: driverId, value: JSON.stringify(location)}],
    });
  }
}

// The send call is wrapped in ctx.run() within the handler
// From food-ordering/app/src/order-app/external/driver_mobile_app_sim.ts
await ctx.run(() => kafkaPublisher.send(ctx.key, location));

Page 10: Connecting from the Outside: Clients

External applications, such as a web frontend or another backend service, can interact with Restate services using the @restatedev/restate-sdk-clients package.

Using the Client

The client provides a typesafe way to invoke handlers.

Example: Calling a Workflow from an external script

import * as restate from "@restatedev/restate-sdk-clients";
import type { SignupApi } from "./3_workflows.ts";

async function signupUser(userId: string, name: string, email: string) {
  const rs = restate.connect({ url: "http://localhost:8080" });
  const workflowClient = rs.workflowClient<SignupApi>({ name: "usersignup" }, userId);
  
  const response = await workflowClient.workflowSubmit({ name, email });

  if (response.status != "Accepted") {
    throw new Error("User ID already taken");
  }

  // Await the final result of the workflow
  const userVerified = await workflowClient.workflowAttach();
}

Idempotency

When calling from external systems, it's crucial to use idempotency keys to prevent duplicate operations in case of client-side retries or network issues.

const reservation = await products.reserve({
  idempotencyKey: reservationId // A unique key for this specific operation
});

Page 11: End-to-End Testing with Testcontainers

The @restatedev/restate-sdk-testcontainers package simplifies end-to-end testing by providing a way to programmatically start and manage a Restate server instance within your tests.

Setting up a Test Environment

RestateTestEnvironment starts a Docker container with the Restate server and binds your services to it.

Example: A Jest Test for a Virtual Object

import { RestateTestEnvironment } from "@restatedev/restate-sdk-testcontainers";
import { exampleObject } from "../src/example_object";
import * as clients from "@restatedev/restate-sdk-clients";

describe("ExampleObject", () => {
  let restateTestEnvironment: RestateTestEnvironment;
  let restateIngress: clients.Ingress;

  // Start the Restate environment before all tests
  beforeAll(async () => {
    restateTestEnvironment = await RestateTestEnvironment.start((restateServer) =>
      restateServer.bind(exampleObject)
    );
    restateIngress = clients.connect({ url: restateTestEnvironment.baseUrl() });
  }, 20_000);

  // Stop the environment after all tests
  afterAll(async () => {
    await restateTestEnvironment.stop();
  });

  it("should increment the counter correctly", async () => {
    // Directly manipulate the object's state for test setup
    const state = restateTestEnvironment.stateOf(exampleObject, "Sarah");
    await state.set("count", 123);

    // Invoke the handler
    const greet = await restateIngress.objectClient(exampleObject, "Sarah").greet();

    // Assert the result and the new state
    expect(greet).toBe("Hello Sarah! Counter: 123");
    expect(await state.get("count")).toBe(124);
  });
});

This approach allows you to test the full, durable behavior of your application in an isolated environment.


Page 12: Deployment Strategies (Node.js & AWS Lambda)

Restate services can be deployed in various environments.

Node.js Server

The simplest way to deploy is as a standalone Node.js application using the built-in HTTP2 server.

// From templates/node/src/app.ts
import * as restate from "@restatedev/restate-sdk";
// ... service definitions ...
restate.endpoint().bind(greeter).listen(9080);

This can then be containerized using a Dockerfile.

AWS Lambda with CDK

Restate services can be deployed as AWS Lambda functions. The @restatedev/restate-cdk package provides constructs to simplify deploying both your service and a self-hosted Restate server or connecting to Restate Cloud.

Example: CDK Stack

This CDK stack deploys a Lambda function and registers it with a Restate Cloud environment.

// From integrations/deployment-lambda-cdk/lib/lambda-ts-cdk-stack.ts
import * as restate from "@restatedev/restate-cdk";
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
// ...

export class LambdaTsCdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    // Define the Lambda function
    const handler = new lambda_nodejs.NodejsFunction(this, "GreeterService", {
      // ... lambda configuration ...
      entry: "lib/lambda/handler.ts",
    });
    
    // Connect to your Restate Cloud environment
    const restateEnvironment = new restate.RestateCloudEnvironment(this, "RestateCloud", {
      environmentId: process.env.RESTATE_ENV_ID! as restate.EnvironmentId,
      apiKey: new secrets.Secret(this, "RestateCloudApiKey", {
        secretStringValue: cdk.SecretValue.unsafePlainText(process.env.RESTATE_API_KEY!),
      }),
    });

    // Deploy the service to the environment
    const deployer = new restate.ServiceDeployer(this, "ServiceDeployer");
    deployer.deployService("Greeter", handler.currentVersion, restateEnvironment);
  }
}

Page 13: Key SDK Types and Interfaces

Understanding these core types is key to effectively using the SDK.

  • Serde (Serializer/Deserializer): Defines how data is converted to and from Uint8Array for communication with Restate. The SDK provides a default JSON serde (restate.serde.json), a binary serde (restate.serde.binary), and the ability to define custom serdes. It is used in handler definitions and client calls to specify data formats.

    • restate.serde.json<T>(): For any JSON-serializable type.
    • restate.serde.binary(): For raw Uint8Array payloads.
    • restate.serde.string(): For plain strings.
    • restate.serde.empty(): For void/empty payloads.
  • Duration: Represents a time duration, used for ctx.sleep() and delayed calls. It can be created from milliseconds, seconds, minutes, etc. Example: restate.Duration.fromMinutes(15). A simple number can also be used, which is interpreted as milliseconds.

  • RestateError: The base class for all errors originating from the Restate SDK.

  • TerminalError: A subclass of RestateError that signals a non-retriable failure. When a handler throws a TerminalError, Restate will not attempt to retry the invocation and will immediately fail the operation.

  • TimeoutError and CancelledError: Specific types of TerminalError thrown when a RestatePromise times out (via .orTimeout()) or when an invocation is cancelled.

  • Wasm* types (e.g., WasmVM, WasmInput, WasmFailure): These are low-level types from the WebAssembly core (sdk_shared_core_wasm_bindings.d.ts). You will typically not interact with these directly. They represent the boundary between the TypeScript SDK and the underlying Rust-based state machine, managing the protocol and journaling logic. The GenericHandler uses these types to process the raw invocation stream from the Restate server.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment