---
title: Framework Integrations
description: Guide for framework authors to integrate Workflow SDK with custom frameworks or runtimes.
type: guide
summary: Build a custom framework integration using the Workflow SDK compiler and runtime.
prerequisites:
  - /docs/foundations/workflows-and-steps
related:
  - /docs/deploying/building-a-world
---

# Framework Integrations



<Callout>
  **For users:** If you just want to use Workflow SDK with an existing framework, check out the [Getting Started](/docs/getting-started) guide instead. This page is for framework authors who want to integrate Workflow SDK with their framework or runtime.
</Callout>

This guide walks you through building a framework integration for Workflow SDK using Bun as a concrete example. The same principles apply to any JavaScript runtime (Node.js, Deno, Cloudflare Workers, etc.).

<Callout type="info">
  **Prerequisites:** Before building a framework integration, we recommend reading [How the Directives Work](/docs/how-it-works/code-transform) to understand the transformation system that powers Workflow SDK.
</Callout>

## What You'll Build

A framework integration has two main components:

1. **Build-time**: Generate workflow handler files (`flow.js`, `step.js`, `webhook.js`)
2. **Runtime**: Expose these handlers as HTTP endpoints in your application server

<Mermaid
  chart="flowchart TD
    A[&#x22;Source Code<br/>'use workflow'&#x22;] --> B[&#x22;Workflow Builder&#x22;]
    B --> C[&#x22;SWC Transform&#x22;]
    C --> D[&#x22;Step Mode&#x22;]
    C --> E[&#x22;Workflow Mode&#x22;]
    C --> F[&#x22;Client Mode&#x22;]
    D --> G[&#x22;Generated Handlers<br/>step.js&#x22;]
    E --> H[&#x22;Generated Handlers<br/>flow.js&#x22;]
    B --> L[&#x22;Generated Handlers<br/>webhook.js&#x22;]
    F --> I[&#x22;Used by framework loader&#x22;]
    G --> J[&#x22;HTTP Server<br/>(Your Runtime)&#x22;]
    H --> J
    L --> J

    style B fill:#a78bfa,stroke:#8b5cf6,color:#000
    style I fill:#a78bfa,stroke:#8b5cf6,color:#000
    style J fill:#a78bfa,stroke:#8b5cf6,color:#000"
/>

The purple boxes are what you implement—everything else is provided by Workflow SDK.

## Example: Bun Integration

Let's build a complete integration for Bun. Bun is unique because it serves as both a runtime (needs code transformations) and a framework (provides `Bun.serve()` for HTTP routing).

<Callout type="info">
  A working example can be [found here](https://github.com/vercel/workflow-examples/tree/main/custom-adapter). For a production-ready reference, see the [Next.js integration](https://github.com/vercel/workflow/tree/main/packages/next).
</Callout>

### Step 1: Generate Handler Files

Use the `workflow` CLI to generate the handler bundles. The CLI scans your `workflows/` directory and creates `flow.js`, `step.js`, and `webhook.js`.

```json title="package.json"
{
  "scripts": {
    "dev": "bun x workflow build && PORT=3152 bun run server.ts"
  }
}
```

<Callout>
  **For production integrations:** Instead of using the CLI, extend the `BaseBuilder` class directly in your framework plugin. This gives you control over file watching, custom output paths, and framework-specific hooks. See the [Next.js plugin](https://github.com/vercel/workflow/tree/main/packages/next) for an example.
</Callout>

**What gets generated:**

* `/.well-known/workflow/v1/flow.js` - Handles workflow execution (workflow mode transform)
* `/.well-known/workflow/v1/step.js` - Handles step execution (step mode transform)
* `/.well-known/workflow/v1/webhook.js` - Handles webhook delivery

Each file exports a `POST` function that accepts Web standard `Request` objects.

### Step 2: Add Client Mode Transform (Optional)

Client mode transforms your application code to provide better DX. Add a Bun plugin to apply this transformation at runtime:

{/* @skip-typecheck: incomplete code sample */}

```typescript title="workflow-plugin.ts" lineNumbers
import { plugin } from "bun";
import { transform } from "@swc/core";

plugin({
  name: "workflow-transform",
  setup(build) {
    build.onLoad({ filter: /\.(ts|tsx|js|jsx)$/ }, async (args) => {
      const source = await Bun.file(args.path).text();

      // Optimization: Skip files that do not have any directives
      if (!source.match(/(use step|use workflow)/)) {
        return { contents: source };
      }

      const result = await transform(source, {
        filename: args.path,
        jsc: {
          experimental: {
            plugins: [
              [require.resolve("@workflow/swc-plugin"), { mode: "client" }], // [!code highlight]
            ],
          },
        },
      });

      return { contents: result.code, loader: "ts" };
    });
  },
});
```

Activate the plugin in `bunfig.toml`:

```toml title="bunfig.toml"
preload = ["./workflow-plugin.ts"]
```

**What this does:**

* Attaches workflow IDs to functions for use with `start()`
* Provides TypeScript type safety
* Prevents accidental direct execution of workflows

**Why optional?** Without client mode, you can still use workflows by manually constructing IDs or referencing the build manifest.

### Step 3: Expose HTTP Endpoints

Wire up the generated handlers to HTTP endpoints using `Bun.serve()`:

{/* @skip-typecheck: incomplete code sample */}

```typescript title="server.ts" lineNumbers
import flow from "./.well-known/workflow/v1/flow.js";
import step from "./.well-known/workflow/v1/step.js";
import * as webhook from "./.well-known/workflow/v1/webhook.js";

import { start } from "workflow/api";
import { handleUserSignup } from "./workflows/user-signup.js";

const server = Bun.serve({
  port: process.env.PORT,
  routes: {
    "/.well-known/workflow/v1/flow": {
      POST: (req) => flow.POST(req),
    },
    "/.well-known/workflow/v1/step": {
      POST: (req) => step.POST(req),
    },
    // webhook exports handlers for GET, POST, DELETE, etc.
    "/.well-known/workflow/v1/webhook/:token": webhook,

    // Example: Start a workflow
    "/": {
      GET: async (req) => {
        const email = `test-${crypto.randomUUID()}@test.com`;
        const run = await start(handleUserSignup, [email]);
        return Response.json({
          message: "User signup workflow started",
          runId: run.runId,
        });
      },
    },
  },
});

console.log(`Server listening on http://localhost:${server.port}`);
```

**That's it!** Your Bun integration is complete.

## Understanding the Endpoints

Your integration must expose three HTTP endpoints. The generated handlers manage all protocol details—you just route requests.

### Workflow Endpoint

**Route:** `POST /.well-known/workflow/v1/flow`

Executes workflow orchestration logic. The workflow function is "rendered" multiple times during execution—each time it progresses until it encounters the next step.

**Called when:**

* Starting a new workflow
* Resuming after a step completes
* Resuming after a webhook or hook triggers
* Recovering from failures

### Step Endpoint

**Route:** `POST /.well-known/workflow/v1/step`

Executes individual atomic operations within workflows. Each step runs exactly once per execution (unless retried due to failure). Steps have full runtime access (Node.js APIs, file system, databases, etc.).

### Webhook Endpoint

**Route:** `POST /.well-known/workflow/v1/webhook/:token`

Delivers webhook data to running workflows via [`createWebhook()`](/docs/api-reference/workflow/create-webhook). The `:token` parameter identifies which workflow run should receive the data.

<Callout type="info">
  The webhook file structure varies by framework. Next.js generates `webhook/[token]/route.js` to leverage App Router's dynamic routing, while other frameworks generate a single `webhook.js` handler.
</Callout>

## Adapting to Other Frameworks

The Bun example demonstrates the core pattern. To adapt for your framework:

### Build-Time

**Option 1: Use the CLI** (simplest)

```bash
workflow build
```

This will default to scanning the `./workflows` top-level directory for workflow files, and will output bundled files directly into your working directory.

**Option 2: Extend `BaseBuilder`** (recommended)

{/* @skip-typecheck: @workflow/cli internal module */}

```typescript lineNumbers
import { BaseBuilder } from "@workflow/cli/dist/lib/builders/base-builder";

class MyFrameworkBuilder extends BaseBuilder {
  constructor(options) {
    super({
      dirs: ["workflows"],
      workingDir: options.rootDir,
      watch: options.dev,
    });
  }

  override async build(): Promise<void> {
    const inputFiles = await this.getInputFiles();

    await this.createWorkflowsBundle({
      outfile: "/path/to/.well-known/workflow/v1/flow.js",
      format: "esm",
      inputFiles,
    });

    await this.createStepsBundle({
      outfile: "/path/to/.well-known/workflow/v1/step.js",
      format: "esm",
      inputFiles,
    });

    await this.createWebhookBundle({
      outfile: "/path/to/.well-known/workflow/v1/webhook.js",
    });
  }
}
```

If your framework supports virtual server routes and dev mode watching, make sure to adapt accordingly. Please open a PR to the Workflow SDK if the base builder class is missing necessary functionality.

Hook into your framework's build:

{/* @skip-typecheck: incomplete code sample */}

```typescript title="pseudocode.ts" lineNumbers
framework.hooks.hook("build:before", async () => {
  await new MyFrameworkBuilder(framework).build();
});
```

### Runtime (Client Mode)

Add a loader/plugin for your bundler:

**Rollup/Vite:**

```typescript lineNumbers
export function workflowPlugin() {
  return {
    name: "workflow-client-transform",
    async transform(code, id) {
      if (!code.match(/(use step|use workflow)/)) return null;

      const result = await transform(code, {
        filename: id,
        jsc: {
          experimental: {
            plugins: [[require.resolve("@workflow/swc-plugin"), { mode: "client" }]], // [!code highlight]
          },
        },
      });

      return { code: result.code, map: result.map };
    },
  };
}
```

**Webpack:**

```javascript lineNumbers
module.exports = {
  module: {
    rules: [
      {
        test: /\.(ts|tsx|js|jsx)$/,
        use: "workflow-client-loader", // Similar implementation
      },
    ],
  },
};
```

### HTTP Server

Route the three endpoints to the generated handlers. The exact implementation depends on your framework's routing API.

In the bun example above, we left routing to the user. Essentially, the user has to serve routes like this:

{/* @skip-typecheck: incomplete code sample */}

```typescript title="server.ts" lineNumbers
import flow from "./.well-known/workflow/v1/flow.js";
import step from "./.well-known/workflow/v1/step.js";
import * as webhook from "./.well-known/workflow/v1/webhook.js";

// Expose the 3 generated routes
const server = Bun.serve({
  routes: {
    "/.well-known/workflow/v1/flow": {
      POST: (req) => flow.POST(req),
    },
    "/.well-known/workflow/v1/step": {
      POST: (req) => step.POST(req),
    },
    // webhook exports handlers for GET, POST, DELETE, etc.
    "/.well-known/workflow/v1/webhook/:token": webhook,
  },
});
```

Production framework integrations should handle this routing in the plugin instead of leaving it to the user, and this depends on each framework's unique implementaiton.
Check the Workflow SDK source code for examples of production framework implementations.
In the future, the Workflow SDK will emit more routes under the `.well-known/workflow` namespace.

## Security

The workflow and step handler endpoints are invoked by the world's queuing infrastructure, not by end users. How they're secured depends on which world you're deploying to.

### Vercel (`@workflow/world-vercel`)

On Vercel, workflow handler functions are not accessible through public endpoints. Handlers use the same [consumer function security](https://vercel.com/docs/queues/concepts#consumer-function-security) mechanism that secures [Vercel Queues](https://vercel.com/docs/queues) consumers.

During the build step, the Workflow SDK automatically configures each handler as a queue consumer by writing `experimentalTriggers` to the function's `.vc-config.json`:

```json title=".vc-config.json (generated by Workflow SDK)"
{
  "experimentalTriggers": [
    {
      "type": "queue/v2beta",
      "topic": "__wkf_step_*",
      "consumer": "default",
      "retryAfterSeconds": 5,
      "initialDelaySeconds": 0
    }
  ]
}
```

Two queue topics are created per deployment:

| Handler     | Topic              | Description                                       |
| ----------- | ------------------ | ------------------------------------------------- |
| `step.func` | `__wkf_step_*`     | Step execution (long-running, `maxDuration: max`) |
| `flow.func` | `__wkf_workflow_*` | Workflow orchestration (`maxDuration: 60`)        |

If you're building a framework integration that targets Vercel, you should write these triggers into the `.vc-config.json` for each generated function. The `STEP_QUEUE_TRIGGER` and `WORKFLOW_QUEUE_TRIGGER` constants are exported from `@workflow/builders` for this purpose:

```typescript
import { STEP_QUEUE_TRIGGER, WORKFLOW_QUEUE_TRIGGER } from "@workflow/builders";
```

### Custom implementations

For self-hosted or non-Vercel deployments, you are responsible for securing the handler endpoints:

* **Framework middleware** — Add authentication (API keys, JWT, OIDC) in front of the `/.well-known/workflow/v1/*` routes
* **Network-level security** — Deploy handlers behind a VPC, private network, or firewall rules so only your queue infrastructure can reach them
* **Rate limiting** — Add request validation and rate limiting to prevent abuse

Learn more about [building custom Worlds](/docs/deploying/building-a-world).

## Testing Your Integration

### 1. Test Build Output

Create a test workflow:

```typescript title="workflows/test.ts" lineNumbers
import { sleep, createWebhook } from "workflow";

export async function handleUserSignup(email: string) {
  "use workflow";

  const user = await createUser(email);
  await sendWelcomeEmail(user);

  await sleep("5s");

  const webhook = createWebhook();
  await sendOnboardingEmail(user, webhook.url);

  await webhook;
  console.log("Webhook Resolved");

  return { userId: user.id, status: "onboarded" };
}

async function createUser(email: string) {
  "use step";

  console.log(`Creating a new user with email: ${email}`);

  return { id: crypto.randomUUID(), email };
}

async function sendWelcomeEmail(user: { id: string; email: string }) {
  "use step";

  console.log(`Sending welcome email to user: ${user.id}`);
}

async function sendOnboardingEmail(user: { id: string; email: string }, callback: string) {
  "use step";

  console.log(`Sending onboarding email to user: ${user.id}`);

  console.log(`Click this link to resolve the webhook: ${callback}`);
}

```

Run your build and verify:

* `.well-known/workflow/v1/flow.js` exists
* `.well-known/workflow/v1/step.js` exists
* `.well-known/workflow/v1/webhook.js` exists

### 2. Test HTTP Endpoints

Start your server and verify routes respond:

```bash
curl -X POST http://localhost:3000/.well-known/workflow/v1/flow
curl -X POST http://localhost:3000/.well-known/workflow/v1/step
curl -X POST http://localhost:3000/.well-known/workflow/v1/webhook/test
```

(Should respond but not trigger meaningful code without authentication/proper workflow run)

### 3. Run a Workflow End-to-End

```typescript
import { start } from "workflow/api";
import { handleUserSignup } from "./workflows/test";

const run = await start(handleUserSignup, ["test@example.com"]);
console.log("Workflow started:", run.runId);
```


## Sitemap
[Overview of all docs pages](/sitemap.md)
