JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 290
  • Score
    100M100P100Q98201F
  • License MIT

Workflow and State Machines for NestJS

Package Exports

  • @jescrich/nestjs-workflow
  • @jescrich/nestjs-workflow/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@jescrich/nestjs-workflow) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

NestJS Workflow Logo

NestJS Workflow & State Machine

A flexible workflow engine built on top of NestJS framework, enabling developers to create, manage, and execute complex workflows in their Node.js applications.

๐ŸŽฏ Live Examples & Demos

Explore fully functional examples with interactive visual demos in our dedicated examples repository:

๐Ÿ‘‰ View Examples Repository

The repository includes three comprehensive real-world examples:

  1. ๐Ÿš€ User Onboarding Workflow - Multi-step verification, KYC/AML compliance, risk assessment
  2. ๐Ÿ“ฆ Order Processing System - Complete e-commerce lifecycle with payment retry logic
  3. ๐Ÿ“Š Kafka-Driven Inventory - Real-time event-driven inventory management with Kafka integration

Each example features:

  • โœจ Interactive Visual Mode - See workflows in action with real-time state visualization
  • ๐ŸŽฎ Interactive Controls - Manually trigger transitions and explore different paths
  • ๐Ÿค– Automated Scenarios - Pre-built test cases demonstrating various workflow paths
  • ๐Ÿ“ Full Source Code - Production-ready implementations you can adapt

โžก๏ธ Get Started with Examples

Table of Contents

Features

  • Workflow Definitions: Define workflows using a simple, declarative syntax
  • State Management: Track and persist workflow states
  • Event-Driven Architecture: Built on NestJS's event system for flexible workflow triggers
  • Transition Rules: Configure complex transition conditions between workflow states
  • Extensible: Easily extend with custom actions, conditions, and triggers
  • TypeScript Support: Full TypeScript support with strong typing
  • Integration Friendly: Seamlessly integrates with existing NestJS applications
  • Kafka Integration: Easily integrate with Kafka for event-driven workflows
  • Stateless Design: Lightweight implementation with no additional storage requirements

Documentation: https://jescrich.github.io/libraries/docs/workflow/intro

Stateless Architecture

NestJS Workflow is designed with a stateless architecture, which offers several key benefits:

Benefits of Stateless Design

  • Simplicity: No additional database or storage configuration required
  • Domain-Driven: State is maintained within your domain entities where it belongs
  • Lightweight: Minimal overhead and dependencies
  • Scalability: Easily scales horizontally with your application
  • Flexibility: Works with any persistence layer or storage mechanism
  • Integration: Seamlessly integrates with your existing data model and repositories
  • The workflow engine doesn't maintain any state itself - instead, it operates on your domain entities, reading their current state and applying transitions according to your defined rules. This approach aligns with domain-driven design principles by keeping the state with the entity it belongs to.

This stateless design means you can:

Use your existing repositories and data access patterns Persist workflow state alongside your entity data Avoid complex synchronization between separate state stores Maintain transactional integrity with your domain operations

// Example of how state is part of your domain entity
export class Order {
  id: string;
  items: OrderItem[];
  totalAmount: number;
  status: OrderStatus; // The workflow state is a property of your entity
  
  // Your domain logic here
}

The workflow engine simply reads and updates this state property according to your defined transitions, without needing to maintain any separate state storage.

Installation

npm install @jescrich/nestjs-workflow

Or using yarn:

yarn add @jescrich/nestjs-workflow

Quick Start

๐ŸŽฎ Try the Interactive Demos First!

Before diving into code, experience workflows visually with our interactive demos:

# Quick demo setup
git clone https://github.com/jescrich/nestjs-workflow-examples.git
cd nestjs-workflow-examples/01-user-onboarding
npm install && npm run demo

You'll see an interactive workflow visualization like this:

โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ•‘  REGISTERED  โ•‘ --> โ”‚EMAIL_VERIFIEDโ”‚ --> โ”‚PROFILE_COMPLETEโ”‚
โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
      (current)            โ†“                      โ†“
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚   SUSPENDED  โ”‚     โ”‚IDENTITY_VERIFIEDโ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                               โ†“
                                         โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
                                         โ•‘  ACTIVE  โ•‘
                                         โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

๐Ÿš€ Explore All Examples

Module Registration

import { Module } from '@nestjs/common';
import { WorkflowModule } from '@jescrich/nestjs-workflow';

// Register a workflow
@Module({
  imports: [
    WorkflowModule.register({
      name: 'simpleworkflow',
      definition: orderWorkflowDefinition,
    }),
  ],
})
export class AppModule {}

Define a Workflow

import { WorkflowDefinition } from '@jescrich/nestjs-workflow';

// Define your entity and state/event enums
export enum OrderEvent {
  Create = 'order.create',
  Submit = 'order.submit',
  Update = 'order.update',
  Complete = 'order.complete',
  Fail = 'order.fail',
  Cancel = 'order.cancel',
}

export enum OrderStatus {
  Pending = 'pending',
  Processing = 'processing',
  Completed = 'completed',
  Failed = 'failed',
}

export class Order {
  id: string;
  name: string;
  price: number;
  items: string[];
  status: OrderStatus;
}

// Create workflow definition
const orderWorkflowDefinition: WorkflowDefinition<Order, any, OrderEvent, OrderStatus> = {
  states: {
    finals: [OrderStatus.Completed, OrderStatus.Failed],
    idles: [OrderStatus.Pending, OrderStatus.Processing, OrderStatus.Completed, OrderStatus.Failed],
    failed: OrderStatus.Failed,
  },
  transitions: [
    {
      from: OrderStatus.Pending,
      to: OrderStatus.Processing,
      event: OrderEvent.Submit,
      conditions: [(entity: Order, payload: any) => entity.price > 10],
    },
    {
      from: OrderStatus.Pending,
      to: OrderStatus.Pending,
      event: OrderEvent.Update,
      actions: [
        async (entity: Order, payload: any) => {
          entity.price = payload.price;
          entity.items = payload.items;
          return entity;
        },
      ],
    },
    {
      from: OrderStatus.Processing,
      to: OrderStatus.Completed,
      event: OrderEvent.Complete,
    },
    {
      from: OrderStatus.Processing,
      to: OrderStatus.Failed,
      event: OrderEvent.Fail,
    },
  ],
  entity: {
    new: () => new Order(),
    update: async (entity: Order, status: OrderStatus) => {
      entity.status = status;
      return entity;
    },
    load: async (urn: string) => {
      // In a real application, load from database
      return new Order();
    },
    status: (entity: Order) => entity.status,
    urn: (entity: Order) => entity.id,
  },
};

Use the Workflow in a Service

import { Injectable } from '@nestjs/common';
import { WorkflowService } from '@jescrich/nestjs-workflow';
import { Order, OrderEvent, OrderStatus } from './order.model';

@Injectable()
export class OrderService {
  constructor(
    private readonly workflowService: WorkflowService<Order, any, OrderEvent, OrderStatus>,
  ) {}
  
  async createOrder() {
    const order = new Order();
    order.id = 'order-123';
    order.name = 'Order 123';
    order.price = 100;
    order.items = ['Item 1', 'Item 2', 'Item 3'];
    order.status = OrderStatus.Pending;
    
    return order;
  }
  
  async submitOrder(id: string) {
    // Emit an event to trigger workflow transition
    const result = await this.workflowService.emit({ 
      urn: id, 
      event: OrderEvent.Submit 
    });
    
    return result;
  }
  
  async updateOrder(id: string, price: number, items: string[]) {
    // Emit an event with payload to update the order
    const result = await this.workflowService.emit({
      urn: id,
      event: OrderEvent.Update,
      payload: {
        price: price,
        items: items,
      },
    });
    
    return result;
  }
}

Configuring Actions and Conditions

NestJS Workflow provides two different approaches for configuring actions and conditions in your workflows:

1. Inline Functions in Transitions

You can define actions and conditions directly in the transition definition as shown in the example above:

{
  from: OrderStatus.Pending,
  to: OrderStatus.Processing,
  event: OrderEvent.Submit,
  conditions: [(entity: Order, payload: any) => entity.price > 10],
  actions: [
    async (entity: Order, payload: any) => {
      // Perform action
      return entity;
    },
  ],
}

2. Using Decorators (Class-based approach)

For more complex workflows, you can use a class-based approach with decorators:

import { Injectable } from '@nestjs/common';
import { WorkflowAction, OnEvent, OnStatusChanged } from '@jescrich/nestjs-workflow';

@Injectable()
@WorkflowAction()
export class OrderActions {
  // Handler triggered on specific event
  @OnEvent({ event: OrderEvent.Submit })
  execute(params: { entity: Order; payload: any }): Promise<Order> {
    const { entity, payload } = params;
    entity.price = entity.price * 100;
    return Promise.resolve(entity);
  }

  // Handler triggered when status changes
  @OnStatusChanged({ from: OrderStatus.Pending, to: OrderStatus.Processing })
  onStatusChanged(params: { entity: Order; payload: any }): Promise<Order> {
    const { entity, payload } = params;
    entity.name = 'Status changed to processing';
    return Promise.resolve(entity);
  }
}

Then include these action classes in your workflow definition:

const definition: WorkflowDefinition<Order, any, OrderEvent, OrderStatus> = {
  actions: [OrderActions],
  // ...other properties
  states: {
    finals: [OrderStatus.Completed, OrderStatus.Failed],
    idles: [OrderStatus.Pending, OrderStatus.Processing, OrderStatus.Completed, OrderStatus.Failed],
    failed: OrderStatus.Failed,
  },
  transitions: [
    {
      from: OrderStatus.Pending,
      to: OrderStatus.Processing,
      event: OrderEvent.Submit,
    },
    // Other transitions
  ],
  // ...
};

Execution Order with @OnEvent

You can control the execution order of multiple handlers for the same event:

@Injectable()
@WorkflowAction()
export class OrderActions {
  @OnEvent({ event: OrderEvent.Submit, order: 1 })
  firstHandler(params: { entity: Order; payload: any }): Promise<Order> {
    // Executes first
    return Promise.resolve(params.entity);
  }

  @OnEvent({ event: OrderEvent.Submit, order: 2 })
  secondHandler(params: { entity: Order; payload: any }): Promise<Order> {
    // Executes second
    return Promise.resolve(params.entity);
  }
}

Error Handling with @OnStatusChanged

By default, if a status change handler fails, the workflow will transition to the failed state:

@OnStatusChanged({ from: OrderStatus.Pending, to: OrderStatus.Processing })
onStatusChanged(params: { entity: Order; payload: any }): Promise<Order> {
  // If this throws an error, the workflow will move to the failed state
  throw new Error("This will cause transition to failed state");
}

You can disable this behavior by setting failOnError: false:

@OnStatusChanged({ 
  from: OrderStatus.Pending, 
  to: OrderStatus.Processing, 
  failOnError: false 
})
onStatusChanged(params: { entity: Order; payload: any }): Promise<Order> {
  // If this throws an error, the workflow will continue to the next state
  throw new Error("This error will be logged but won't affect the workflow");
}

Remember to register your action classes as providers in your module:

@Module({
  imports: [
    WorkflowModule.register({
      name: 'orderWorkflow',
      definition,
    }),
  ],
  providers: [OrderActions],
})
export class OrderModule {}

Kafka Integration

NestJS Workflow now supports integration with Apache Kafka, allowing your workflows to react to Kafka events and trigger state transitions based on messages from your event streaming platform.

Setting Up Kafka Integration

To configure your workflow to listen to Kafka events, you need to add a kafka property to your workflow definition:

const orderWorkflowDefinition: WorkflowDefinition<Order, any, OrderEvent, OrderStatus> = {
  // ... other workflow properties
  states: {
    finals: [OrderStatus.Completed, OrderStatus.Failed],
    idles: [OrderStatus.Pending, OrderStatus.Processing, OrderStatus.Completed, OrderStatus.Failed],
    failed: OrderStatus.Failed,
  },
  transitions: [
    // Your transitions here
  ],
  
  // Kafka configuration
  kafka: {
    brokers: 'localhost:9092',
    events: [
      { topic: 'orders.submitted', event: OrderEvent.Submit },
      { topic: 'orders.completed', event: OrderEvent.Complete },
      { topic: 'orders.failed', event: OrderEvent.Fail }
    ]
  },
  
  entity: {
    // Entity configuration
    new: () => new Order(),
    update: async (entity: Order, status: OrderStatus) => {
      entity.status = status;
      return entity;
    },
    load: async (urn: string) => {
      // Load entity from storage
      return new Order();
    },
    status: (entity: Order) => entity.status,
    urn: (entity: Order) => entity.id
  }
};

How It Works

When you configure Kafka integration:

  1. The workflow engine will connect to the specified Kafka brokers
  2. It will subscribe to the topics you've defined in the events array
  3. When a message arrives on a subscribed topic, the workflow engine will:
    • Map the topic to the corresponding workflow event
    • Extract the entity URN from the message
    • Load the entity using your defined entity.load function
    • Emit the mapped workflow event with the Kafka message as payload

Complete Example with Kafka Integration

import { Injectable, Module } from '@nestjs/common';
import { WorkflowModule, WorkflowDefinition, WorkflowService } from '@jescrich/nestjs-workflow';

// Define your entity and state/event enums
export enum OrderEvent {
  Create = 'order.create',
  Submit = 'order.submit',
  Complete = 'order.complete',
  Fail = 'order.fail',
}

export enum OrderStatus {
  Pending = 'pending',
  Processing = 'processing',
  Completed = 'completed',
  Failed = 'failed',
}

export class Order {
  id: string;
  name: string;
  price: number;
  items: string[];
  status: OrderStatus;
}

// Create workflow definition with Kafka integration
const orderWorkflowDefinition: WorkflowDefinition<Order, any, OrderEvent, OrderStatus> = {
  states: {
    finals: [OrderStatus.Completed, OrderStatus.Failed],
    idles: [OrderStatus.Pending, OrderStatus.Processing, OrderStatus.Completed, OrderStatus.Failed],
    failed: OrderStatus.Failed,
  },
  transitions: [
    {
      from: OrderStatus.Pending,
      to: OrderStatus.Processing,
      event: OrderEvent.Submit,
      conditions: [(entity: Order, payload: any) => entity.price > 10],
    },
    {
      from: OrderStatus.Processing,
      to: OrderStatus.Completed,
      event: OrderEvent.Complete,
    },
    {
      from: OrderStatus.Processing,
      to: OrderStatus.Failed,
      event: OrderEvent.Fail,
    },
  ],
  
  // Kafka configuration
  kafka: {
    brokers: 'localhost:9092',
    events: [
      { topic: 'orders.submitted', event: OrderEvent.Submit },
      { topic: 'orders.completed', event: OrderEvent.Complete },
      { topic: 'orders.failed', event: OrderEvent.Fail }
    ]
  },
  
  entity: {
    new: () => new Order(),
    update: async (entity: Order, status: OrderStatus) => {
      entity.status = status;
      return entity;
    },
    load: async (urn: string) => {
      // In a real application, load from database
      const order = new Order();
      order.id = urn;
      order.status = OrderStatus.Pending;
      return order;
    },
    status: (entity: Order) => entity.status,
    urn: (entity: Order) => entity.id
  }
};

@Module({
  imports: [
    WorkflowModule.register({
      name: 'orderWorkflow',
      definition: orderWorkflowDefinition,
    }),
  ],
})
export class AppModule {}

Message Format

The Kafka messages should include the entity URN so that the workflow engine can load the correct entity. For example:

{
  "urn": "order-123",
  "price": 150,
  "items": ["Item 1", "Item 2"]
}

With this setup, your workflow will automatically react to Kafka messages and trigger the appropriate state transitions based on your workflow definition.

Entity Service Implementation

NestJS Workflow allows you to implement an EntityService to manage your entity's lifecycle and state. This provides a cleaner separation of concerns between your workflow logic and entity management.

Creating an EntityService

Instead of defining entity operations inline in your workflow definition, you can create a dedicated service:

import { Injectable } from '@nestjs/common';
import { EntityService } from '@jescrich/nestjs-workflow';
import { Order, OrderStatus } from './order.model';
import { OrderRepository } from './order.repository';

@Injectable()
export class OrderEntityService extends EntityService<Order, OrderStatus> {
  constructor(private readonly orderRepository: OrderRepository) {
    super();
  }

  // Create a new entity instance
  new(): Promise<Order> {
    return Promise.resolve(new Order());
  }

  // Update entity status
  async update(entity: Order, status: OrderStatus): Promise<Order> {
    entity.status = status;
    return this.orderRepository.save(entity);
  }

  // Load entity by URN
  async load(urn: string): Promise<Order> {
    const order = await this.orderRepository.findByUrn(urn);
    if (!order) {
      throw new Error(`Order with URN ${urn} not found`);
    }
    return order;
  }

  // Get current status
  status(entity: Order): OrderStatus {
    return entity.status;
  }

  // Get entity URN
  urn(entity: Order): string {
    return entity.id;
  }
}

Registering the EntityService

Register your EntityService as a provider in your module:

@Module({
  imports: [
    TypeOrmModule.forFeature([OrderEntity]),
  ],
  providers: [
    OrderEntityService,
    OrderRepository,
  ],
  exports: [OrderEntityService],
})
export class OrderModule {}

Using EntityService with Workflow

There are two ways to use your EntityService with a workflow:

1. Reference in Workflow Definition

import { Module } from '@nestjs/common';
import { WorkflowModule } from '@jescrich/nestjs-workflow';
import { OrderEntityService } from './order-entity.service';

const orderWorkflowDefinition: WorkflowDefinition<Order, any, OrderEvent, OrderStatus> = {
  states: {
    finals: [OrderStatus.Completed, OrderStatus.Failed],
    idles: [OrderStatus.Pending, OrderStatus.Processing, OrderStatus.Completed, OrderStatus.Failed],
    failed: OrderStatus.Failed,
  },
  transitions: [
    // Your transitions here
  ],
  
  // Reference your EntityService class instead of inline functions
  entity: OrderEntityService,
};

@Module({
  imports: [
    WorkflowModule.register({
      name: 'orderWorkflow',
      definition: orderWorkflowDefinition,
    }),
  ],
})
export class AppModule {}

2. Inject into WorkflowService

You can also inject your EntityService directly when creating a WorkflowService instance:

@Injectable()
export class OrderService {
  private workflowService: WorkflowService<Order, any, OrderEvent, OrderStatus>;
  
  constructor(
    private readonly moduleRef: ModuleRef,
    private readonly orderEntityService: OrderEntityService
  ) {
    const workflowDefinition = {
      states: {
        finals: [OrderStatus.Completed, OrderStatus.Failed],
        idles: [OrderStatus.Pending, OrderStatus.Processing, OrderStatus.Completed, OrderStatus.Failed],
        failed: OrderStatus.Failed,
      },
      transitions: [
        // Your transitions here
      ],
      
      // You can still include entity here, but it will be overridden by the injected service
      entity: {
        new: () => new Order(),
        // other methods...
      }
    };
    
    this.workflowService = new WorkflowService(
      workflowDefinition,
      this.moduleRef,
      this.orderEntityService // Inject the entity service
    );
  }
  
  // Your service methods using workflowService
}

Benefits of Using EntityService

Using a dedicated EntityService provides several advantages:

  1. Separation of Concerns: Keep entity management logic separate from workflow definitions
  2. Dependency Injection: Leverage NestJS dependency injection for your entity operations
  3. Reusability: Use the same EntityService across multiple workflows
  4. Testability: Easier to mock and test your entity operations
  5. Database Integration: Cleanly integrate with your database through repositories

This approach is particularly useful for complex applications where entities are stored in databases and require sophisticated loading and persistence logic.

๐Ÿ“š Examples & Learning Resources

Interactive Examples Repository

The best way to learn is by exploring our comprehensive examples repository which includes:

1. User Onboarding Workflow Example

Demonstrates a real-world user registration and verification system:

  • Progressive profile completion with automatic transitions
  • Multi-factor authentication flows
  • Risk assessment integration
  • Compliance checks (KYC/AML)
  • States: REGISTERED โ†’ EMAIL_VERIFIED โ†’ PROFILE_COMPLETE โ†’ IDENTITY_VERIFIED โ†’ ACTIVE

2. E-Commerce Order Processing Example

Complete order lifecycle management system:

  • Payment processing with retry logic
  • Inventory reservation and management
  • Multi-state shipping workflows
  • Refund and return handling
  • States: CREATED โ†’ PAYMENT_PENDING โ†’ PAID โ†’ PROCESSING โ†’ SHIPPED โ†’ DELIVERED

3. Kafka-Driven Inventory Management

Event-driven inventory system with Kafka integration:

  • Real-time stock level updates via Kafka events
  • Automatic reorder triggering
  • Quality control and quarantine workflows
  • Multi-warehouse support
  • Special states for QUARANTINE, AUDITING, DAMAGED, EXPIRED

Running the Examples

# Clone the examples repository
git clone https://github.com/jescrich/nestjs-workflow-examples.git
cd nestjs-workflow-examples

# Install all examples
npm run install:all

# Run interactive demos with visual workflow diagrams
npm run demo:user-onboarding    # User onboarding demo
npm run demo:order-processing   # Order processing demo
npm run demo:kafka-inventory    # Kafka inventory demo

The interactive demos feature:

  • ASCII-art workflow visualization showing current state and possible transitions
  • Real-time state updates as you interact with the workflow
  • Menu-driven interface to trigger events and explore different paths
  • Automated scenarios to demonstrate various workflow patterns

Advanced Usage

For more advanced usage, including custom actions, conditions, and event handling, please check the documentation and explore the examples repository.