@workflow/nest
NestJS integration for Workflow SDK.
Installation
npm install @workflow/nest
pnpm add @workflow/nest
You also need to install the SWC packages required by NestJS's SWC builder:
npm install -D @swc/cli @swc/core
pnpm add -D @swc/cli @swc/core
Quick start
1. Initialize SWC configuration
After installing the package, run the init command to generate the SWC configuration:
npx @workflow/nest init
This creates a .swcrc file configured with the Workflow SWC plugin for client-mode transformations.
init writes only the settings the workflow transform needs (the plugin entry, decorator parsing and metadata, and module.type). Any other SWC configuration already in the file is preserved, and --force refreshes the resolved plugin path rather than replacing the file.
Important: Add .swcrc to your .gitignore as it contains machine-specific absolute paths:
echo '/.swcrc' >> .gitignore
2. Configure NestJS to use SWC
Ensure your nest-cli.json has SWC as the builder:
{/@skip-typecheck: Shows nest-cli.json configuration/}
{
"compilerOptions": {
"builder": "swc"
}
}
3. Import the WorkflowModule
In your app.module.ts:
{/@skip-typecheck: Shows WorkflowModule import/}
import { Module } from '@nestjs/common';
import { WorkflowModule } from '@workflow/nest';
@Module({
imports: [WorkflowModule.forRoot()],
})
export class AppModule {}
4. Create workflow files
Create workflow files in your src/ directory with "use workflow" and "use step" directives:
{/@skip-typecheck: Shows workflow file/}
export async function myStep(data: string) {
'use step';
return data.toUpperCase();
}
export async function myWorkflow(input: string) {
'use workflow';
const result = await myStep(input);
return result;
}
5. Add pre-build scripts
Add scripts to regenerate configuration before builds:
{
"scripts": {
"prebuild": "npx @workflow/nest init --force",
"build": "nest build"
}
}
Configuration options
{/@skip-typecheck: Shows WorkflowModule.forRoot options/}
WorkflowModule.forRoot({
dirs: ['src'],
outDir: '.nestjs/workflow',
skipBuild: false,
basePath: '/api',
manageWorldLifecycle: false,
preloadBundles: true,
moduleType: 'es6',
distDir: 'dist',
});
Options can come from other providers with forRootAsync:
{/@skip-typecheck: Shows WorkflowModule.forRootAsync options/}
WorkflowModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
basePath: config.get('API_PREFIX'),
}),
});
Raw request bodies
Create the app with rawBody so a signed webhook body reaches the workflow
byte-for-byte:
{/@skip-typecheck: Shows the NestFactory.create option/}
const app = await NestFactory.create(AppModule, { rawBody: true });
Without it the parsed body has to be re-serialized, which changes whitespace and
key order and breaks signature verification. A warning is logged once when that
fallback is used.
Dependency injection is not available in workflows and steps
Workflows and steps are compiled into separate bundles and do not run inside the
NestJS application, so the injector, your providers, request-scoped context,
guards, interceptors and the Nest Logger are all out of reach from
"use workflow" and "use step" code. A class imported into a step is a
different class object from the one your module registered, so app.get() on it
raises UnknownElementException; on Vercel the workflow function is a separate
function from the app entirely. Write steps as plain functions over their
arguments and keep DI in your controllers and providers.
Deploying to Vercel
NestJS is not a Vercel-native framework, so the Workflow SDK emits a
Vercel Build Output API directory
(.vercel/output) for it. This includes the combined workflow queue-consumer
function (registered with experimentalTriggers so Vercel Queue dispatches your
runs) alongside your NestJS app bundled as a catch-all function. Without it,
deployed workflow runs stay pending because nothing consumes the queue.
1. Add a serverless entry module
Create a _vercel/entry.ts that default-exports a Node request handler backed by
your NestJS app (the _vercel/ prefix avoids colliding with Vercel's automatic
api/ function detection). Import AppModule from the compiled dist/
output. nest build runs first, and its SWC pass emits the decorator metadata
NestJS DI relies on; importing raw src/ TypeScript would route the app back
through esbuild, which does not emit emitDecoratorMetadata. Also import
reflect-metadata at the top so DI metadata is registered:
{/@skip-typecheck: Shows the Vercel entry module shape/}
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../dist/app.module.js';
let ready: Promise<any> | undefined;
async function createHandler() {
const app = await NestFactory.create(AppModule);
await app.init();
return app.getHttpAdapter().getInstance();
}
export default async function handler(req: any, res: any) {
ready ??= createHandler();
const instance = await ready;
return instance(req, res);
}
2. The in-process build is skipped for you
No module change is needed: skipBuild defaults to true when the VERCEL
environment variable is set, because the Build Output already contains the
compiled workflow bundles and the deployed filesystem is read-only.
3. Wire up the build command
Add a vercel-build script that compiles the app and then emits the Build
Output. workflow-nest build emits the Vercel Build Output automatically when
the VERCEL env var is set (pass --vercel to force it locally):
{
"scripts": {
"vercel-build": "nest build && npx @workflow/nest build"
}
}
nest build (via SWC) compiles your app, including the decorator metadata and
the workflow client transform. Then, @workflow/nest build bundles the app and
the workflow functions into .vercel/output.
Note: Native addons (*.node) are not bundled or traced into the deployed
function, so NestJS apps that depend on native modules are not yet supported by
--vercel.
How it works
The @workflow/nest package provides:
- WorkflowModule: A NestJS module that handles workflow bundle building and HTTP routing
- WorkflowController: Handles workflow and step execution requests at
.well-known/workflow/v1/
- NestLocalBuilder: Builds workflow bundles (
steps.mjs and workflows.mjs) from your source files. Exposed at the @workflow/nest/builder subpath (not the package root, which stays free of build-time dependencies so importing WorkflowModule never adds the compiler to your runtime bundle).
- NestVercelBuilder: Emits a Vercel Build Output API directory for deploying on Vercel. Exposed at the
@workflow/nest/vercel-builder subpath.
- CLI: Generates
.swcrc configuration with the SWC plugin resolved and builds workflow bundles or the Vercel Build Output
Why the CLI?
NestJS uses its own SWC builder that reads configuration from .swcrc. The Workflow SWC plugin needs to be referenced by path in this file. The CLI resolves the plugin path from @workflow/nest's dependencies, eliminating the need for manual configuration or pnpm hoisting.
Technical details
When you run npx @workflow/nest init, it:
- Resolves the path to
@workflow/swc-plugin (bundled as a dependency of @workflow/nest)
- Generates
.swcrc with the absolute path to the plugin
- Configures client-mode transformation for workflow files
This approach ensures:
- No manual SWC plugin configuration required
- No pnpm hoisting configuration required in
.npmrc
- The plugin is always resolved from the correct location
Why workflows must be in src/
NestJS's SWC builder only compiles files within the sourceRoot directory (typically src/). For the workflow client-mode transform to work, workflow files must be in src/ so they get compiled with the SWC plugin that attaches workflowId properties needed by start().
API reference
WorkflowModule
{/@skip-typecheck: Shows WorkflowModule usage/}
import { WorkflowModule } from '@workflow/nest';
WorkflowModule.forRoot()
WorkflowModule.forRoot({
dirs: ['src/workflows'],
outDir: '.nestjs/workflow',
skipBuild: true,
moduleType: 'commonjs',
distDir: 'dist',
})
CLI commands
npx @workflow/nest init
npx @workflow/nest init --force
npx @workflow/nest build
npx @workflow/nest build --vercel
npx @workflow/nest --help
build options
--vercel | Emit a Vercel Build Output API directory (.vercel/output) with the workflow queue-consumer function. Implied when the VERCEL env var is set. | off (on under VERCEL) |
--dirs <dirs> | Comma-separated workflow source directories to scan. | src |
--entry <path> | Vercel app entry module that default-exports a Node request handler. | auto-detected (e.g. _vercel/entry.js) |
--out-dir <dir> | Output directory for local-dev bundles (ignored with --vercel). | .nestjs/workflow |
--module <type> | SWC module type: es6 or commonjs. | es6 |
--base-path <path> | Route prefix the app is served under. Must match setGlobalPrefix() / basePath. | none |
--sourcemap <mode> | esbuild sourcemap mode: true, false, inline, linked, external, both. | builder default |
--max-duration <secs> | maxDuration for the app function. | 300 |
--runtime <runtime> | Vercel runtime for the emitted functions, e.g. nodejs22.x. | platform default |
--app-function <name> | Name of the catch-all app function. | __nest |
License
Apache-2.0