---
title: Encryption
description: Learn how Workflow SDK encrypts user data end-to-end in the event log.
type: conceptual
summary: Understand how workflow and step data is encrypted at rest.
prerequisites:
  - /docs/how-it-works/event-sourcing
related:
  - /docs/observability
  - /docs/deploying/world/vercel-world
---

<Callout>
This guide explains how Workflow SDK encrypts user data in the event log. Understanding these details is not required to use workflows — encryption is automatic and requires no code changes. For getting started, see the [getting started](/docs/getting-started) guides for your framework.
</Callout>

Workflow SDK supports automatic end-to-end encryption of all user data before it is written to the event log. When a `World` implementation provides encryption support, it is safe to pass sensitive data — such as API keys, tokens, or user credentials — as workflow inputs, step arguments, and return values. The storage backend only ever sees ciphertext.

Encryption support varies by `World` implementation. See the [Worlds](/worlds) page to check which worlds support this feature. `World` implementations opt into encryption by providing a `getEncryptionKeyForRun()` method — the core runtime will use it automatically when present.

## What Is Encrypted

All user data flowing through the event log is encrypted:

- **Workflow inputs** — arguments passed when starting a workflow
- **Workflow return values** — the final output of a workflow
- **Step inputs** — arguments passed to step functions
- **Step return values** — the result returned by step functions
- **Hook metadata** — data attached when creating a hook
- **Hook payloads** — data received by hooks and webhooks
- **Stream data** — each frame in a `ReadableStream` or `WritableStream`

Metadata such as workflow names, step names, entity IDs, timestamps, and lifecycle states are **not** encrypted. This allows the observability tools to display run structure and timelines without requiring decryption.

## How It Works

### Key Management

Each workflow run is encrypted with its own unique key, provided by the `World` implementation via `getEncryptionKeyForRun()`. How the key is generated and stored is up to the `World`.

For example, the [Vercel World](/docs/deploying/world/vercel-world) provides unique keys per run and execution environment, ensuring that a given run can only decrypt data from that run itself.

### Encryption Algorithm

Data is encrypted using **AES-256-GCM** via the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API):

- A random 12-byte nonce is generated for each encryption operation
- The GCM authentication tag provides integrity verification — any tampering with the ciphertext is detected
- The same plaintext produces different ciphertext each time due to the random nonce

## Decrypting Data

When viewing workflow runs through the observability tools, encrypted fields display as locked placeholders until you explicitly choose to decrypt them.

### Permissions

Decryption access is controlled by the `World` implementation. On Vercel, decryption follows the same permissions model as project environment variables — if you don't have permission to view environment variable values for a project, you won't be able to decrypt workflow data either. Each decryption request is recorded in your [Vercel audit log](https://vercel.com/docs/audit-log), giving your team full visibility into when and by whom workflow data was accessed.

### Web Dashboard

Click the **Decrypt** button in the run detail panel to decrypt all data fields. Decryption happens entirely in the browser via the Web Crypto API — the observability server retrieves the encryption key but never sees your plaintext data.

### CLI

Add the `--decrypt` flag to any `inspect` command:

```bash
# Inspect a specific run
npx workflow inspect run <run-id> --decrypt

# Inspect a specific step
npx workflow inspect step <step-id> --run <run-id> --decrypt

# List events for a run
npx workflow inspect events --run <run-id> --decrypt

# Inspect a specific stream
npx workflow inspect stream <stream-id> --run <run-id> --decrypt
```

Without `--decrypt`, encrypted fields display as `🔒 Encrypted` placeholders.

## Custom World Implementations

The core runtime encrypts data automatically when the `World` implementation provides a `getEncryptionKeyForRun()` method. The core runtime can call this method in two forms:

{/* @skip-typecheck - interface signature, not runnable code */}
```typescript
getEncryptionKeyForRun?(run: WorkflowRun): Promise<Uint8Array | undefined>;
getEncryptionKeyForRun?(
  runId: string,
  context?: Record<string, unknown>
): Promise<Uint8Array | undefined>;
```

Use `getEncryptionKeyForRun(run)` when the run entity already exists. Use `getEncryptionKeyForRun(runId, context?)` in runtime paths like `start()` where the run has not been created yet but the world may still need context such as `deploymentId`.

To add encryption support to a custom `World`:

1. Implement `getEncryptionKeyForRun()` on your `World` class, handling both call shapes
2. Return the raw 32-byte key as a `Uint8Array` — the core runtime uses it for AES-256-GCM operations
3. Ensure the same key is returned for the same run ID across invocations (for decryption during replay)

```typescript
import type { WorkflowRun, World } from "@workflow/world";

export const getEncryptionKeyForRun: World["getEncryptionKeyForRun"] = async (
  run,
  context
) => {
  const runId = typeof run === "string" ? run : run.runId;
  const deploymentId =
    typeof run === "string"
      ? (context?.deploymentId as string | undefined)
      : run.deploymentId;

  return await lookupRunKey(runId, deploymentId);
};

async function lookupRunKey(
  runId: string,
  deploymentId?: string
): Promise<Uint8Array | undefined> {
  // Look up or derive the encryption key for this run
  // Return undefined to skip encryption
  return new Uint8Array(32);
}
```

The [Vercel World](/docs/deploying/world/vercel-world) implementation uses HKDF derivation from a deployment-scoped key, but any consistent key management scheme will work.
