workflow
Version:
Workflow SDK - Build durable, resilient, and observable workflows
261 lines (197 loc) • 9.01 kB
text/mdx
---
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 combined runtime handler.
prerequisites:
- /docs/foundations/workflows-and-steps
related:
- /worlds/building-a-world
---
<Callout>
**For users:** If you want to use Workflow SDK with an existing framework, see [Getting Started](/docs/getting-started). This page is for framework authors.
</Callout>
This guide uses Bun as a concrete example, but the same build and routing model applies to other JavaScript frameworks and runtimes.
<Callout type="info">
Read [How the Directives Work](/docs/how-it-works/code-transform) first if you are not familiar with the three compiler modes.
</Callout>
## Integration shape
A framework integration has two responsibilities:
1. **Build time:** transform workflow and step code, register the steps, and generate the combined flow handler.
2. **Runtime:** expose the generated flow and webhook handlers as HTTP routes.
```mermaid
flowchart TD
A["Source code<br/>'use workflow' / 'use step'"] --> B["Workflow builder"]
B --> C["Workflow-mode bundle"]
B --> D["Step registration bundle"]
D --> E["Combined flow handler"]
C --> E
B --> F["Webhook handler"]
E --> G["POST /.well-known/workflow/v1/flow"]
F --> H["/.well-known/workflow/v1/webhook/:token"]
style B fill:#a78bfa,stroke:#8b5cf6,color:#000
style G fill:#a78bfa,stroke:#8b5cf6,color:#000
style H fill:#a78bfa,stroke:#8b5cf6,color:#000
```
Steps do not have their own HTTP route. A queued step invocation contains `stepId` and `stepName` and is delivered to the flow handler on the workflow queue. The handler executes the registered step in the full host runtime, then replays the workflow in its sandboxed VM.
## Example: Bun integration
### 1. Generate the bundles
The standalone Workflow CLI scans `workflows/` and creates the combined flow handler, an internal step registration module, and the webhook handler.
```json title="package.json"
{
"scripts": {
"dev": "bun x workflow build && PORT=3152 bun run server.ts"
}
}
```
The default output is:
- `/.well-known/workflow/v1/flow.mjs`: the combined workflow and step queue consumer
- `/.well-known/workflow/v1/__step_registrations.mjs`: an internal module imported by `flow.mjs`; do not route to it
- `/.well-known/workflow/v1/webhook.mjs`: webhook delivery
<Callout>
Production integrations should extend `BaseBuilder` from `/builders` so they can participate in the framework's build, watch, and routing lifecycle.
</Callout>
### 2. Add the app-code transform
Applying the step-mode transform to application code gives it the workflow IDs used by `start()` and prevents accidental direct workflow execution. (Earlier releases used a separate `client` mode for this; it merged into `step` in 5.0.)
{/* @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();
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: "step" }],
],
},
},
});
return { contents: result.code, loader: "ts" };
});
},
});
```
Activate the plugin in `bunfig.toml`:
```toml title="bunfig.toml"
preload = ["./workflow-plugin.ts"]
```
### 3. Expose the HTTP routes
Only the combined flow handler and webhook handler are routable:
{/* @skip-typecheck: incomplete code sample */}
```typescript title="server.ts" lineNumbers
import * as flow from "./.well-known/workflow/v1/flow.mjs";
import * as webhook from "./.well-known/workflow/v1/webhook.mjs";
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: flow.POST,
},
"/.well-known/workflow/v1/webhook/:token": webhook,
"/": {
GET: async () => {
const email = `test-${crypto.randomUUID()}.com`;
const run = await start(handleUserSignup, [email]);
return Response.json({ runId: run.runId });
},
},
},
});
console.log(`Server listening on http://localhost:${server.port}`);
```
## Runtime routes
### Combined flow endpoint
**Route:** `POST /.well-known/workflow/v1/flow`
The handler consumes every workflow queue message. Depending on the payload and event log, it can:
- Start or replay workflow orchestration in the sandboxed VM.
- Execute a queued step in the host runtime.
- Continue replay inline after a step completes.
- Resume a run after a hook, webhook, sleep, retry, or recovery event.
### Webhook endpoint
**Route:** `/.well-known/workflow/v1/webhook/:token`
This handler delivers data to [`createWebhook()`](/docs/api-reference/workflow/create-webhook). Its generated file structure varies by framework; for example, Next.js uses `webhook/[token]/route.js`.
## Building with `BaseBuilder`
Use `createCombinedBundle()` so the flow bundle imports the generated step registrations. Calling `createWorkflowsBundle()` and `createStepsBundle()` independently does not create a complete runtime route.
{/* @skip-typecheck: incomplete framework adapter */}
```typescript title="my-framework-builder.ts" lineNumbers
import { join } from "node:path";
import { BaseBuilder } from "@workflow/builders";
class MyFrameworkBuilder extends BaseBuilder {
override async build(): Promise<void> {
const inputFiles = await this.getInputFiles();
const tsconfigPath = await this.findTsConfigPath();
const outputDir = join(this.config.workingDir, ".workflow");
await this.createCombinedBundle({
inputFiles,
stepsOutfile: join(outputDir, "__step_registrations.mjs"),
flowOutfile: join(outputDir, "flow.mjs"),
tsconfigPath,
format: "esm",
});
await this.createWebhookBundle({
outfile: join(outputDir, "webhook.mjs"),
});
}
}
```
If workflows import sibling workspace packages, set `projectRoot` to the smallest directory containing all imported packages.
{/* @skip-typecheck: partial constructor configuration */}
```typescript
super({
dirs: ["workflows"],
workingDir: options.rootDir,
projectRoot: options.workspaceRoot ?? options.rootDir,
watch: options.dev,
});
```
Your integration can expose physical files, virtual modules, or framework-native routes. In every case, the step registration module is a dependency of the flow handler, not a route of its own.
## Vercel queue configuration
On Vercel, configure the flow function as the sole queue consumer. `getWorkflowQueueTrigger()` handles the optional queue namespace and `WORKFLOW_SEQUENTIAL_REPLAYS=1` behavior.
```typescript
import { getWorkflowQueueTrigger } from "@workflow/builders";
const flowConfig = {
maxDuration: "max",
experimentalTriggers: [getWorkflowQueueTrigger()],
};
```
The generated trigger listens to one topic family:
```json title=".vc-config.json (excerpt)"
{
"experimentalTriggers": [
{
"type": "queue/v2beta",
"topic": "__wkf_workflow_*",
"consumer": "default",
"retryAfterSeconds": 5,
"initialDelaySeconds": 0
}
]
}
```
Both orchestration messages and step messages use this topic family. A step message is distinguished by its payload, not by a second topic or function.
If you construct the trigger yourself, add `maxConcurrency: 1` when `WORKFLOW_SEQUENTIAL_REPLAYS=1`. The exported `isSequentialReplaysEnabled()` helper implements that build-time check.
## Security
On Vercel, the flow handler uses [queue consumer security](https://vercel.com/docs/queues/concepts#consumer-function-security). For self-hosted worlds, protect the flow and webhook routes with the authentication, network controls, and rate limits appropriate to your queue transport.
## Testing an integration
After a build, verify that:
- the flow handler exists and imports or embeds the step registration bundle;
- the webhook handler exists;
- no step HTTP route or step queue trigger is generated;
- the flow function has the workflow queue trigger and the required maximum duration;
- starting a workflow executes a real step and resumes the workflow.
The flow route exposes a lightweight direct health mode:
```bash
curl -X POST "http://localhost:3000/.well-known/workflow/v1/flow?__health"
```
For an end-to-end queue check, use [`healthCheck()`](/docs/api-reference/workflow-runtime/health-check).