---
title: Building a World
description: Implement the World interface to run workflows on any custom infrastructure.
type: guide
summary: Build a custom World adapter to run workflows on your own infrastructure.
prerequisites:
  - /docs/deploying
  - /docs/foundations/workflows-and-steps
related:
  - /worlds/local
  - /worlds/postgres
  - /worlds/vercel
---

# Building a World



A **World** is the abstraction that allows workflows to run on any infrastructure. It handles workflow storage, step execution queuing, and data streaming. You can implement the World interface to connect workflows to your own infrastructure.

<Callout>
  Before building a custom World, check the [Worlds Ecosystem](/worlds) page. There may already be a community implementation for your infrastructure.
</Callout>

<Callout type="info">
  **Reference implementation:** The [Postgres World source code](https://github.com/vercel/workflow/tree/main/packages/world-postgres) is a production-ready example of how to implement the World interface with a database backend and graphile-worker for queuing.
</Callout>

## What is a World?

A World connects workflows to the infrastructure that powers them. The World interface abstracts three core responsibilities:

* **Storage**: Persists workflow runs, steps, hooks, and the event log.
* **Queue**: Enqueues and processes workflow and step invocations.
* **Streamer**: Manages real-time data streams between workflows and clients.

{/* @skip-typecheck - interface definition, not runnable code */}

```typescript
interface World extends Storage, Queue, Streamer {
  start?(): Promise<void>;
  close?(): Promise<void>;
  getEncryptionKeyForRun?(run: WorkflowRun): Promise<Uint8Array | undefined>;
  getEncryptionKeyForRun?(runId: string, context?: Record<string, unknown>): Promise<Uint8Array | undefined>;
}
```

The optional `start()` method initializes background tasks (for example, queue polling). The optional `close()` method releases resources like connection pools and listeners. The optional `getEncryptionKeyForRun()` method returns the AES-256 key used to encrypt data for a run; if it is not implemented, encryption is disabled.

## The event log model

Workflow storage is built on an **append-only event log**. All state changes happen through events: you never modify runs, steps, or hooks directly. Instead, you create events that update the materialized state.

Events fall into three categories: run lifecycle events, step lifecycle events, and hook lifecycle events. See the [Event Sourcing](/docs/how-it-works/event-sourcing) documentation for a complete list of event types and their semantics.

## Storage interface

The Storage interface provides read access to materialized entities and write access through events:

{/* @skip-typecheck - interface definition, not runnable code */}

```typescript
interface Storage {
  runs: {
    get(id: string, params?: GetWorkflowRunParams): Promise<WorkflowRun>;
    list(params?: ListWorkflowRunsParams): Promise<PaginatedResponse<WorkflowRun>>;
  };

  steps: {
    get(runId: string | undefined, stepId: string, params?: GetStepParams): Promise<Step>;
    list(params: ListWorkflowRunStepsParams): Promise<PaginatedResponse<Step>>;
  };

  events: {
    // Create a new workflow run (runId may be client-provided or null for server generation)
    create(runId: string | null, data: RunCreatedEventRequest, params?: CreateEventParams): Promise<EventResult>;
    
    // Create an event for an existing run
    create(runId: string, data: CreateEventRequest, params?: CreateEventParams): Promise<EventResult>;
    
    list(params: ListEventsParams): Promise<PaginatedResponse<Event>>;
    listByCorrelationId(params: ListEventsByCorrelationIdParams): Promise<PaginatedResponse<Event>>;
  };

  hooks: {
    get(hookId: string, params?: GetHookParams): Promise<Hook>;
    getByToken(token: string, params?: GetHookParams): Promise<Hook>;
    list(params: ListHooksParams): Promise<PaginatedResponse<Hook>>;
  };
}
```

### Key implementation details

**Event creation:** When `events.create()` is called, your implementation must:

1. Persist the event to the event log.
2. Atomically update the affected entity (run, step, or hook).
3. Return both the created event and the updated entity.

**Run creation:** For `run_created` events, the `runId` parameter may be a client-provided string or `null`. When `null`, your World generates and returns a new `runId`.

**Hook tokens:** Hook tokens must be unique. If a `hook_created` event conflicts with an existing token, return a `hook_conflict` event instead and include the active hook owner's run ID as `eventData.conflictingRunId`.

**Automatic hook disposal:** When a workflow reaches a terminal state (`completed`, `failed`, or `cancelled`), automatically dispose of all associated hooks to release tokens for reuse.

## Queue interface

The Queue interface handles asynchronous execution of workflows and steps:

{/* @skip-typecheck - interface definition, not runnable code */}

```typescript
interface Queue {
  getDeploymentId(): Promise<string>;

  queue(
    queueName: ValidQueueName,
    message: QueuePayload,
    opts?: QueueOptions
  ): Promise<{ messageId: MessageId }>;

  createQueueHandler(
    queueNamePrefix: QueuePrefix,
    handler: (message: unknown, meta: { attempt: number; queueName: ValidQueueName; messageId: MessageId }) => Promise<void | { timeoutSeconds: number }>
  ): (req: Request) => Promise<Response>;
}
```

### Queue names

Queue names follow a specific pattern:

* `__wkf_workflow_<name>`: For workflow invocations
* `__wkf_step_<name>`: For step invocations

### Message payloads

Two types of messages flow through queues:

**Workflow invocations:**

{/* @skip-typecheck - interface definition, not runnable code */}

```typescript
interface WorkflowInvokePayload {
  runId: string;
  traceCarrier?: Record<string, string>;  // OpenTelemetry context
  requestedAt?: Date;
}
```

**Step invocations:**

{/* @skip-typecheck - interface definition, not runnable code */}

```typescript
interface StepInvokePayload {
  workflowName: string;
  workflowRunId: string;
  workflowStartedAt: number;
  stepId: string;
  traceCarrier?: Record<string, string>;
  requestedAt?: Date;
}
```

### Implementation considerations

* Messages must be delivered at-least-once.
* Support configurable retry policies.
* Track attempt counts for observability.
* Implement idempotency using the `idempotencyKey` option when provided.

## Streamer interface

The Streamer interface enables real-time data streaming:

{/* @skip-typecheck - interface definition, not runnable code */}

```typescript
interface Streamer {
  streamFlushIntervalMs?: number;

  streams: {
    write(
      runId: string,
      name: string,
      chunk: string | Uint8Array
    ): Promise<void>;

    writeMulti?(
      runId: string,
      name: string,
      chunks: (string | Uint8Array)[]
    ): Promise<void>;

    close(runId: string, name: string): Promise<void>;

    get(
      runId: string,
      name: string,
      startIndex?: number
    ): Promise<ReadableStream<Uint8Array>>;

    list(runId: string): Promise<string[]>;

    /** Paginated snapshot of stream chunks. */
    getChunks(
      runId: string,
      name: string,
      options?: { limit?: number; cursor?: string }
    ): Promise<{
      data: { index: number; data: Uint8Array }[];
      cursor: string | null;
      hasMore: boolean;
      done: boolean;
    }>;

    /** Lightweight metadata: tail index and completion flag. */
    getInfo(
      runId: string,
      name: string
    ): Promise<{ tailIndex: number; done: boolean }>;
  };
}
```

Streams are identified by a combination of `runId` and `name`. Each workflow run can have multiple named streams.
`writeMulti()` is an optional optimization for batching multiple writes.

`getChunks` returns a paginated snapshot of currently available chunks (unlike `get` which returns a live `ReadableStream` that waits for new chunks). `getInfo` returns the tail index (last chunk index, 0-based, or `-1` when empty) and whether the stream is complete, which is useful for resolving negative `startIndex` values into absolute positions.

## Process-wide state

Hold state that must be process-wide on `globalThis`, not at module scope.

A World is loaded in one of two ways, and only one of them gives your package a
single module instance:

* **Loaded at runtime.** `WORKFLOW_TARGET_WORLD=@your-org/world-foo` is resolved
  with `require()` at runtime, so Node's module cache dedupes it and one process
  holds one copy.
* **Bundled.** The host application's bundler compiles your package into its
  server build. Bundlers key module identity on `(resource, layer)`, and a
  framework routinely builds several server layers. Next.js compiles
  `instrument`, app-route, `ssr` and `edge` as separate module graphs. Your
  package is then compiled into each one, so a single process holds several
  copies of every one of your modules, each with its own module scope.

The two built-in worlds are bundled. A custom world is not today, but that is a
property of how it is loaded rather than of how it is written, and it can change
under you. `@workflow/world-vercel` was external until it wasn't, and every
module-scope variable in it silently became per-copy state.

So a top-level `let` or a `const` holding a `Map` is not the singleton it looks
like:

```typescript
// Wrong: one Map per copy. Writes from one part of the app are invisible to
// another, and a mutex like this simply stops mutually excluding.
const locks = new Map<string, Promise<void>>();
```

Reach for `globalThis` under a `Symbol.for()` key instead, so every copy shares
one object:

```typescript
type WorldState = { locks: Map<string, Promise<void>> };

const StateKey = Symbol.for('@your-org/world-foo//locks/v1');
const store = globalThis as typeof globalThis &
  Record<symbol, WorldState | undefined>;

const state: WorldState = (store[StateKey] ??= { locks: new Map() });
```

Version the key. Two releases of your package can end up in one process, and a
key without a version lets an older copy read a state object it does not
understand.

Inside this repository, `globalSingleton()` from `@workflow/utils` does exactly
this and is what the first-party worlds use; the hand-rolled form above is
written out so a world published outside this repository does not need the
dependency. `scripts/lint/module-scope-state.mjs` accepts either.

Better still, keep the state on the World instance your `createWorld()` returns.
Connection pools, caches, and open channels are usually per-World rather than
per-process, and instance state cannot be duplicated by a bundler. Reserve the
global for the few things that are genuinely process-wide: ID generators whose
sequence must not fork, and log-once latches.

## Reference implementations

Study these implementations for guidance:

* **[Local World](https://github.com/vercel/workflow/tree/main/packages/world-local)**: Filesystem-based reference for understanding the fundamentals
* **[Postgres World](https://github.com/vercel/workflow/tree/main/packages/world-postgres)**: Database-backed with graphile-worker for queuing

## Testing your World

The Workflow SDK includes an end-to-end (E2E) test suite that validates World implementations. Once your World is published to npm:

1. Add your world to [`worlds-manifest.json`](https://github.com/vercel/workflow/blob/main/worlds-manifest.json).
2. Open a pull request (PR) to the Workflow repository.
3. Continuous integration (CI) will automatically run the E2E test suite against your implementation.

Your world will then appear on the [Worlds Ecosystem](/worlds) page with its compatibility status and performance benchmarks.

## Publishing your World

1. **Package your World**: Export a default World instance from your package.
2. **Publish to npm**: Publish your package to npm.
3. **Add to the manifest**: Submit a PR adding your world to [`worlds-manifest.json`](https://github.com/vercel/workflow/blob/main/worlds-manifest.json).
4. **Document configuration**: Clearly document any required environment variables.

```json title="worlds-manifest.json"
{
  "package": "your-world-package",
  "repository": "https://github.com/you/your-world",
  "docs": "https://github.com/you/your-world#readme"
}
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)