Skip to content

camcima/duraflows

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

170 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

duraflows

CI codecov npm version License: MIT TypeScript Node.js CodeQL

A durable workflow runtime for TypeScript built on top of @camcima/finita. Supports named states, event-triggered transitions, sequential command execution with success/failure branching, timeout-driven transitions, mutable workflow context, and full audit history.

Designed as a family of packages:

Package Purpose
@duraflows/core Framework-agnostic runtime, types, and persistence interfaces
@duraflows/pg PostgreSQL persistence adapter using pg
@duraflows/kysely PostgreSQL persistence adapter using kysely (for apps already using kysely as query builder)
@duraflows/nestjs NestJS module integration with DI, services, and optional REST controllers

Pick @duraflows/pg for the simpler default (raw pg pool, minimal deps). Pick @duraflows/kysely when your app already uses kysely and you want workflow state to participate in the same transactions as the rest of your data.

Key Features

  • Declarative workflow definitions in plain TypeScript objects
  • Command execution with sequential fail-fast policy and success/failure branching
  • Timeout processing with persisted deadlines and batch processing
  • Mutable context accessible to commands, with state-defined patches merged on entry
  • Immutable metadata for identity labels that never change after creation
  • Full audit history of every transition with command results
  • Row-level locking for concurrent access safety
  • Persistence-agnostic core -- bring your own database library (pg, Prisma, Drizzle, TypeORM)
  • NestJS integration with dependency injection, services, and optional REST controllers
  • Mermaid diagram generation from workflow definitions with customizable options

Installation

# Core runtime (always required)
pnpm add @duraflows/core

# Pick ONE persistence adapter:
pnpm add @duraflows/pg pg          # raw pg pool
pnpm add @duraflows/kysely kysely  # kysely query builder (also needs pg as a peer dep)

# NestJS integration (if using NestJS)
pnpm add @duraflows/nestjs

Quick Example

Define a Workflow

flowchart TB

    classDef stateNode fill:#f1f5f9,stroke:#64748b,stroke-width:2px,color:#1e293b,font-size:20px

    _start@{ shape: sm-circ }
    new["<b>new</b>"]:::stateNode
    exportable["<b>exportable</b>"]:::stateNode
    exported["<b>exported</b>"]:::stateNode
    in_transit["<b>in_transit</b>"]:::stateNode
    delivered["<b>delivered</b>"]:::stateNode
    closed["<b>closed</b>"]:::stateNode
    cancelled["<b>cancelled</b>"]:::stateNode
    export_failed["<b>export_failed</b>"]:::stateNode
    _end@{ shape: framed-circle }

    _start --> new

    new__PaymentReceived(["PaymentReceived"])
    new --> new__PaymentReceived
    new__PaymentReceived --> exportable
    new__Cancel(["Cancel"])
    new --> new__Cancel
    new__Cancel --> cancelled

    exportable__Export(["Export"])
    exportable --> exportable__Export
    exportable__Export --> exported
    exportable__Export --> export_failed

    exported__onEnter(["🗲"])
    exported --> exported__onEnter
    exported__onEnter --> in_transit

    in_transit__Deliver(["Deliver"])
    in_transit --> in_transit__Deliver
    in_transit__Deliver --> delivered

    delivered__TimeOut(["TimeOut ⧖14d"])
    delivered --> delivered__TimeOut
    delivered__TimeOut --> closed

    export_failed__RetryExport(["RetryExport"])
    export_failed --> export_failed__RetryExport
    export_failed__RetryExport --> exportable

    closed --> _end
    cancelled --> _end

    linkStyle 0,1,3,5,8,10,12,14,16,17 stroke-width:3px
    linkStyle 2,4,6,9,11,13,15 stroke:#22c55e,stroke-width:3px
    linkStyle 7 stroke:#dc3545,stroke-width:3px,stroke-dasharray:5
Loading

Tip: This diagram was generated with toMermaidDiagram(orderWorkflow) from @duraflows/core. See Diagram Generation below.

import type { WorkflowDefinition } from "@duraflows/core";

const orderWorkflow: WorkflowDefinition = {
  name: "order",
  initialState: "new",
  states: {
    new: {
      context: { paymentStatus: "pending", isActive: true },
      events: {
        PaymentReceived: {
          targetState: "exportable",
        },
        Cancel: {
          targetState: "cancelled",
        },
      },
    },
    exportable: {
      context: { paymentStatus: "paid" },
      events: {
        Export: {
          targetState: "exported",
          errorState: "export_failed",
          commands: [{ name: "sendOrderToWarehouse" }],
        },
      },
    },
    exported: {
      context: { shipmentStatus: "shipped" },
      onEnter: {
        targetState: "in_transit",
        commands: [{ name: "notifyCustomer" }],
      },
    },
    in_transit: {
      events: {
        Deliver: { targetState: "delivered" },
      },
    },
    delivered: {
      context: { shipmentStatus: "delivered", isActive: false },
      events: {
        TimeOut: {
          targetState: "closed",
          timeout: { afterDays: 14 },
        },
      },
    },
    closed: {},
    cancelled: {
      context: { isActive: false },
    },
    export_failed: {
      events: {
        RetryExport: {
          targetState: "exportable",
        },
      },
    },
  },
};

Implement Command Handlers

import type { WorkflowCommand, CommandResult, WorkflowExecutionContext } from "@duraflows/core";

class SendOrderToWarehouseCommand implements WorkflowCommand {
  async execute(subject: unknown, ctx: WorkflowExecutionContext): Promise<CommandResult> {
    // Read immutable metadata
    const orderId = ctx.metadata.orderId as string;

    try {
      const shipment = await warehouseApi.createShipment(subject);

      // Write to mutable context — persisted after transition
      ctx.context.shipmentId = shipment.id;
      ctx.context.shippedAt = ctx.now.toISOString();

      return { ok: true, code: "SHIPPED" };
    } catch (err) {
      return { ok: false, code: "WH_ERROR", message: String(err) };
    }
  }
}

Use with NestJS

import { Module } from "@nestjs/common";
import { Pool } from "pg";
import { WorkflowModule } from "@duraflows/nestjs";
import { pgWorkflowProviders } from "@duraflows/pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

@Module({
  imports: [
    WorkflowModule.forRoot({
      workflows: [orderWorkflow],
      commands: [
        { name: "sendOrderToWarehouse", useClass: SendOrderToWarehouseCommand },
        { name: "notifyCustomer", useClass: NotifyCustomerCommand },
      ],
      persistence: pgWorkflowProviders(pool),
      enableControllers: true, // optional REST endpoints
    }),
  ],
})
export class AppModule {}
import { Injectable } from "@nestjs/common";
import { WorkflowService } from "@duraflows/nestjs";

@Injectable()
export class OrderService {
  constructor(private readonly workflowService: WorkflowService) {}

  async createOrder(orderData: CreateOrderDto) {
    const order = await this.orderRepo.create(orderData);

    const instance = await this.workflowService.createInstance({
      workflowName: "order",
      metadata: { orderId: order.uuid },
    });

    return { order, workflowInstanceUuid: instance.uuid };
  }

  async receivePayment(workflowInstanceUuid: string, order: Order) {
    // Get a handle — binds the UUID, no DB call
    const handle = this.workflowService.getHandle(workflowInstanceUuid);

    const result = await handle.triggerEvent("PaymentReceived", {
      subject: order,
      triggerMetadata: { source: "user", actor: order.customerUuid },
    });

    console.log(result.outcome); // "success"
    console.log(result.toState); // "exportable"

    // All operations go through the handle — no UUID repetition
    const events = await handle.getAvailableEvents();
    const history = await handle.getHistory({ limit: 10 });
  }
}

Use Without NestJS

import { WorkflowRuntime, InMemoryDefinitionRegistry, InMemoryCommandRegistry } from "@duraflows/core";
import { pgWorkflowProviders } from "@duraflows/pg";
import { Pool } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const persistence = pgWorkflowProviders(pool);

const definitionRegistry = new InMemoryDefinitionRegistry();
definitionRegistry.register(orderWorkflow);

const commandRegistry = new InMemoryCommandRegistry();
commandRegistry.register("sendOrderToWarehouse", new SendOrderToWarehouseCommand());
commandRegistry.register("notifyCustomer", new NotifyCustomerCommand());

const runtime = new WorkflowRuntime({
  definitionRegistry,
  commandRegistry,
  ...persistence,
  clock: { now: () => new Date() },
});

// Create an instance
const instance = await runtime.createInstance({
  workflowName: "order",
});

// Get a handle — binds the UUID, no DB call
const handle = runtime.getHandle(instance.uuid);

// Trigger an event through the handle
const result = await handle.triggerEvent("PaymentReceived", {
  subject: orderEntity,
});

// All operations go through the handle
const events = await handle.getAvailableEvents();
const current = await handle.getInstance();
const history = await handle.getHistory();

Diagram Generation

Generate Mermaid diagrams from any workflow definition:

import { toMermaidDiagram } from "@duraflows/core";

// Default options — clean overview
const diagram = toMermaidDiagram(orderWorkflow);

// Show command names and use left-to-right layout
const detailed = toMermaidDiagram(orderWorkflow, {
  showCommands: true,
  direction: "LR",
});

Options:

Option Default Description
showCommands false Show command names on event nodes
direction "TB" Diagram direction: "TB" (top-bottom) or "LR"

The output is valid Mermaid flowchart syntax. Paste it into any Mermaid renderer (GitHub markdown, mermaid.live, documentation tools) to visualize your workflow.

Database Setup

The @duraflows/pg package provides two ways to set up the database schema:

Option 1: Copy the reference migration

A ready-made dbmate migration is shipped at:

node_modules/@duraflows/pg/sql/dbmate/001_workflow_core.sql

Copy it into your migration directory. It uses gen_random_uuid() (PostgreSQL 13+) for history record UUIDs.

Option 2: Generate a migration with generateMigrationSql()

Use this to choose between gen_random_uuid() (PG 13+) and uuidv7() (PG 18+, time-ordered):

import { generateMigrationSql } from "@duraflows/pg";

// For PostgreSQL 18+ (time-ordered UUIDs)
const { up, down } = generateMigrationSql({ uuidStrategy: "uuidv7" });

// For PostgreSQL 13-17 (random UUIDs, the default)
const { up, down } = generateMigrationSql();

Paste the up and down SQL into your migration file.

Both options create two tables: workflow_instances and workflow_history.

Custom Persistence Adapters

The NestJS module and the core runtime are fully decoupled from pg. To use Prisma, Drizzle, TypeORM, or any other library, implement three interfaces from @duraflows/core:

  • WorkflowInstanceStore
  • WorkflowHistoryStore
  • WorkflowTransactionRunner

See the Persistence Guide for details and examples.

Documentation

Document Description
Getting Started Installation, database setup, first workflow
Workflow Definitions States, events, commands, timeouts, context and metadata
Core Runtime API WorkflowRuntime, compiler, validator, executors, diagram
Persistence Interfaces, pg adapter, custom adapters
NestJS Integration Module, services, controllers, DI tokens
Error Handling Error types, when they occur, how to handle them

AI Agent Skills

If you use AI coding agents (Claude Code, Cursor, Copilot, etc.), install the duraflows agent skills (shipped from this repo under skills/) to give your agent domain knowledge about duraflows APIs, patterns, and best practices:

npx skills add camcima/duraflows

This installs 5 skills that automatically activate when the agent detects duraflows-related code or workflow-building requests:

Skill Description
duraflows-developer API patterns, deterministic guardrails, and anti-patterns
duraflows-builder Translates natural-language requirements into workflow implementations
duraflows-tester Testing patterns: clock injection, in-memory stores, command mocking
duraflows-persistence-adapter Guide for implementing custom persistence adapters
duraflows-reviewer Code review checklist for workflow definitions and commands

Architecture

graph TD
    App[Application Code] --> NestJS["@duraflows/nestjs<br/>(NestJS adapter)"]
    App --> Core["@duraflows/core<br/>(runtime + types)"]
    NestJS --> Core
    PG["@duraflows/pg<br/>(PostgreSQL adapter)"] --> Core
    Custom["Your custom adapter<br/>(Prisma, Drizzle, etc.)"] -.-> Core
Loading

duraflows-nestjs depends only on duraflows-core interfaces. duraflows-pg is one persistence adapter. You can replace it with your own.

Security

CI Scanning

Tool Purpose Trigger
CodeQL Static analysis for security vulnerabilities Push/PR to main + weekly schedule
OSV-Scanner Dependency vulnerability scanning (production deps only) Push/PR to main
Dependabot Automated dependency and GitHub Actions updates Weekly PRs

Local Hooks (Lefthook)

Hook Tool Behavior
pre-commit Gitleaks Scans staged files for secrets (skipped if not installed)
pre-push Semgrep Lightweight code-security scan (skipped if not installed)

Manual Commands

# Secret scanning (requires gitleaks)
pnpm run security:secrets

# Dependency audit
pnpm run security:audit

Installing Local Tools (Optional)

Gitleaks and Semgrep are optional -- hooks skip gracefully if they are not installed.

# macOS
brew install gitleaks
brew install semgrep

# Linux (see each project's install docs)
# https://github.com/gitleaks/gitleaks#installing
# https://semgrep.dev/docs/getting-started/

License

MIT

About

A durable workflow runtime for TypeScript built on top of @camcima/finita.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages