
























Guide for framework authors to integrate Workflow SDK with custom frameworks or runtimes.
For users: If you just want to use Workflow SDK with an existing framework, check out the Getting Started guide instead. This page is for framework authors who want to integrate Workflow SDK with their framework or runtime.
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.).
Prerequisites: Before building a framework integration, we recommend reading How the Directives Work to understand the transformation system that powers Workflow SDK.
A framework integration has two main components:
flow.js, step.js, webhook.js)The purple boxes are what you implement—everything else is provided by Workflow SDK.
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).
Use the workflow CLI to generate the handler bundles. The CLI scans your workflows/ directory and creates flow.js, step.js, and webhook.js.
{
"scripts": {
"dev": "bun x workflow build && PORT=3152 bun run server.ts"
}
}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 for an example.
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 deliveryEach file exports a POST function that accepts Web standard Request objects.
Client mode transforms your application code to provide better DX. Add a Bun plugin to apply this transformation at runtime:
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" }],
],
},
},
});
return { contents: result.code, loader: "ts" };
});
},
});Activate the plugin in bunfig.toml:
preload = ["./workflow-plugin.ts"]What this does:
start()Why optional? Without client mode, you can still use workflows by manually constructing IDs or referencing the build manifest.
Wire up the generated handlers to HTTP endpoints using Bun.serve():
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.
Your integration must expose three HTTP endpoints. The generated handlers manage all protocol details—you just route requests.
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:
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.).
Route: POST /.well-known/workflow/v1/webhook/:token
Delivers webhook data to running workflows via createWebhook(). The :token parameter identifies which workflow run should receive the data.
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.
The Bun example demonstrates the core pattern. To adapt for your framework:
Option 1: Use the CLI (simplest)
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)
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.
If your framework integration lives in a subdirectory and your workflows import code from sibling workspace packages, pass projectRoot to BaseBuilder. Use the smallest directory that contains every workspace package imported by your workflows.
import { BaseBuilder } from "@workflow/cli/dist/lib/builders/base-builder";
class MyFrameworkBuilder extends BaseBuilder {
constructor(options: {
rootDir: string;
workspaceRoot?: string;
dev: boolean;
}) {
super({
dirs: ["workflows"],
workingDir: options.rootDir,
projectRoot: options.workspaceRoot ?? options.rootDir,
watch: options.dev,
});
}
override async build(): Promise<void> {
const inputFiles = await this.getInputFiles();
// ...
}
}Hook into your framework's build:
framework.hooks.hook("build:before", async () => {
await new MyFrameworkBuilder(framework).build();
});Add a loader/plugin for your bundler:
Rollup/Vite:
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" }]],
},
},
});
return { code: result.code, map: result.map };
},
};
}Webpack:
module.exports = {
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
use: "workflow-client-loader", // Similar implementation
},
],
},
};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:
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.
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.
@workflow/world-vercel)On Vercel, workflow handler functions are not accessible through public endpoints. Handlers use the same consumer function security mechanism that secures Vercel 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:
{
"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:
import { STEP_QUEUE_TRIGGER, WORKFLOW_QUEUE_TRIGGER } from "@workflow/builders";For self-hosted or non-Vercel deployments, you are responsible for securing the handler endpoints:
/.well-known/workflow/v1/* routesLearn more about building custom Worlds.
Create a test workflow:
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 existsStart your server and verify routes respond:
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)
import { start } from "workflow/api";
import { handleUserSignup } from "./workflows/test";
const run = await start(handleUserSignup, ["test@example.com"]);
console.log("Workflow started:", run.runId);此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。