NestJS

This guide will walk through setting up your first workflow in a NestJS app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects.


Create Your NestJS Project

Start by creating a new NestJS project using the NestJS CLI.

npm i -g @nestjs/cli
nest new my-workflow-app

Enter the newly made directory:

cd my-workflow-app

Install workflow

npm i workflow @workflow/nest

Configure NestJS for ESM

NestJS with SWC uses ES modules. Add "type": "module" to your package.json:

package.json
{
  "name": "my-workflow-app",
  "type": "module",
  // ... rest of your config
}

When using ESM with NestJS, local imports must include the .js extension (e.g., import { AppModule } from './app.module.js'). This applies even though your source files are .ts.

Configure NestJS to use SWC

NestJS supports SWC as an alternative compiler for faster builds. The Workflow DevKit uses an SWC plugin to transform workflow files.

Install the required SWC packages:

npm i -D @swc/cli @swc/core

Ensure your nest-cli.json has SWC as the builder:

nest-cli.json
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "builder": "swc",
    "deleteOutDir": true
  }
}

Initialize SWC Configuration

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.

Add .swcrc to your .gitignore as it contains machine-specific absolute paths that shouldn't be committed.

Update package.json

Add scripts to regenerate the SWC configuration before builds:

package.json
{
  "scripts": {
    "prebuild": "npx @workflow/nest init --force",
    "build": "nest build",
    "start:dev": "npx @workflow/nest init --force && nest start --watch"
  }
}

Import the WorkflowModule

In your app.module.ts, import the WorkflowModule:

src/app.module.ts
import { Module } from '@nestjs/common';
import { WorkflowModule } from '@workflow/nest';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';

@Module({
  imports: [WorkflowModule.forRoot()], 
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

The WorkflowModule handles workflow bundle building and provides HTTP routing for workflow execution at .well-known/workflow/v1/.

Create Your First Workflow

Create a new file for our first workflow in the src/workflows directory:

Workflow files must be inside the src/ directory so they get compiled with the SWC plugin that enables the start() function to work correctly.

src/workflows/user-signup.ts
import { sleep } from "workflow";

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

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

  await sleep("5s"); // Pause for 5s - doesn't consume any resources
  await sendOnboardingEmail(user);

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

We'll fill in those functions next, but let's take a look at this code:

  • We define a workflow function with the directive "use workflow". Think of the workflow function as the orchestrator of individual steps.
  • The Workflow DevKit's sleep function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long.

Create Your Workflow Steps

Let's now define those missing functions.

src/workflows/user-signup.ts
import { FatalError } from "workflow";

// Our workflow function defined earlier

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

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

  // Full Node.js access - database calls, APIs, etc.
  return { id: crypto.randomUUID(), email };
}

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

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

  if (Math.random() < 0.3) {
    // By default, steps will be retried for unhandled errors
    throw new Error("Retryable!");
  }
}

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

  if (!user.email.includes("@")) {
    // To skip retrying, throw a FatalError instead
    throw new FatalError("Invalid Email");
  }

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

Taking a look at this code:

  • Business logic lives inside steps. When a step is invoked inside a workflow, it gets enqueued to run on a separate request while the workflow is suspended, just like sleep.
  • If a step throws an error, like in sendWelcomeEmail, the step will automatically be retried until it succeeds (or hits the step's max retry count).
  • Steps can throw a FatalError if an error is intentional and should not be retried.

We'll dive deeper into workflows, steps, and other ways to suspend or handle events in Foundations.

Create Your Controller

To invoke your new workflow, update your controller with a new endpoint:

src/app.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { start } from 'workflow/api';
import { handleUserSignup } from './workflows/user-signup.js';

@Controller()
export class AppController {
  @Post('signup')
  async signup(@Body() body: { email: string }) {
    await start(handleUserSignup, [body.email]);
    return { message: 'User signup workflow started' };
  }
}

This creates a POST endpoint at /signup that will trigger your workflow.

Run in development

To start your development server, run the following command in your terminal:

npm run start:dev

Once your development server is running, you can trigger your workflow by running this command in the terminal:

curl -X POST -H "Content-Type: application/json" -d '{"email":"hello@example.com"}' http://localhost:3000/signup

Check the NestJS development server logs to see your workflow execute as well as the steps that are being processed.

Additionally, you can use the Workflow DevKit CLI or Web UI to inspect your workflow runs and steps in detail.

# Open the observability Web UI
npx workflow web
# or if you prefer a terminal interface, use the CLI inspect command
npx workflow inspect runs
Workflow DevKit Web UI

Configuration Options

The WorkflowModule.forRoot() method accepts optional configuration:

WorkflowModule.forRoot({
  // Directory to scan for workflow files (default: ['src'])
  dirs: ['src'],

  // Output directory for generated bundles (default: '.nestjs/workflow')
  outDir: '.nestjs/workflow',

  // Skip building in production when bundles are pre-built
  skipBuild: false,
});

Deploying to production

Workflow DevKit apps currently work best when deployed to Vercel and needs no special configuration.

Check the Deploying section to learn how your workflows can be deployed elsewhere.

Next Steps