Skip to content
Conduit

skill

duraflows

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

camcima/duraflowscommunitylistedunreviewed

Set up

Skills are installed by placing their folder where your agent loads skills.

The download comes from github.com/camcima/duraflows itself, not from our server, so what you get is exactly what its author published.

Install instructions come from the README

Skills do not use an MCP config. Follow the steps in the README below.

README

synced Oct 25, 2026

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:

PackagePurpose
@duraflows/coreFramework-agnostic runtime, types, and persistence interfaces
@duraflows/pgPostgreSQL persistence adapter using pg
@duraflows/kyselyPostgreSQL persistence adapter using kysely (for apps already using kysely as query builder)
@duraflows/nestjsNestJS 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, optionally generic over a TState union for type-safe currentState/fromState/toState
  • Durable command progress -- opt-in queued events checkpoint each command and recover unfinished work after a crash; setup and guarantees
  • Command execution with sequential fail-fast policy and success/failure branching
  • Timeout processing with persisted deadlines and batch processing; a timeout that keeps failing is retried with exponential backoff and then parked until an operator re-arms it
  • Event guards -- declarative preconditions that reject an event without running commands (outcome: "guard-rejected", recorded in history)
  • Observers notified after commit on every state entry, for audit trails and projections
  • Definition versioning -- a content change without a version bump fails startup, and in-flight instances stay pinned to the version they started on (or opt into versionPolicy: "latest")
  • Instance migration -- migrateInstances() moves instances between stored definition versions in batches, with state mapping, a context transform and a dry run
  • 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 pg  # kysely query builder (pg = your Postgres driver, not a declared peer)

# 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

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() },
});
await runtime.initialize(); // surfaces definition/version errors at boot

// 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:

OptionDefaultDescription
showCommandsfalseShow 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 migrations

Ready-made dbmate migrations are shipped at:

node_modules/@duraflows/pg/sql/dbmate/

Copy all files in that directory (001_workflow_core.sql, 002_replace_trigger_with_metadata.sql, 003_event_guards.sql, 004_definition_versions.sql, 005_timeout_retries.sql, 006_definition_version_index.sql) into your migration directory and apply them in order. 001 through 005 are required before deploying 6.x or 7.x, and 006 is recommended for 7.0.0 — the runtime reads and writes the definition_version columns, the workflow_definitions table and the timeout_* columns on every operation. Applying only 001 produces a schema the current runtime cannot write to — 002/003 add the metadata_json handling and the rejected_by column / guard-rejected outcome that the history store requires, 004 adds the workflow_definitions table and definition_version columns that the definition store and every instance/history write require, and 005 adds the timeout_attempts, timeout_retry_at, timeout_last_error and timeout_parked_at columns that every instance read and write uses — it must be applied before deploying 6.0.0. 006 (recommended, not required) adds an index that keeps the 7.0.0 startup executability check and listDefinitionVersions() cheap on large tables. The migrations use gen_random_uuid() (PostgreSQL 13+) for history record UUIDs.

Upgrading to 7.0.0: instances now execute the definition version they were stamped with instead of the latest registered one, by default. Set versionPolicy: "latest" on a definition to keep 6.x behavior. Read Upgrading to 7.0.0 before deploying: it covers idle instances reverting to older versions' rules, the startup check, mixed 6.x/7.0 workers and a pre-upgrade query. Instances stuck on an old version don't have to wait for it to drain -- see runtime.migrateInstances() (v7.1.0).

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 three tables: workflow_instances, workflow_history, and workflow_definitions.

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

Optionally implement the fourth, WorkflowDefinitionStore, to support definition versioning -- it's an optional field on WorkflowPersistenceProvider, so an adapter that omits it still compiles and runs, it just leaves definition versioning inert.

WorkflowInstanceStore.countInstances() and (for WorkflowDefinitionStore implementers) listVersions() are required as of 7.0.0. WorkflowInstanceStore.findInstanceUuids() (7.1.0) is optional: it lets migrateInstances() find candidates without an explicit instanceUuids list, and since 7.2.0 it also receives states/excludeStates as hints a store may honor or ignore.

See the Persistence Guide for details and examples.

Documentation

DocumentDescription
Getting StartedInstallation, database setup, first workflow
Workflow DefinitionsStates, events, commands, timeouts, context and metadata
Core Runtime APIWorkflowRuntime, compiler, validator, executors, diagram
PersistenceInterfaces, pg adapter, custom adapters
NestJS IntegrationModule, services, controllers, DI tokens
Error HandlingError 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:

SkillDescription
duraflows-developerAPI patterns, deterministic guardrails, and anti-patterns
duraflows-builderTranslates natural-language requirements into workflow implementations
duraflows-testerTesting patterns: clock injection, in-memory stores, command mocking
duraflows-persistence-adapterGuide for implementing custom persistence adapters
duraflows-reviewerCode 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

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

Security

CI Scanning

ToolPurposeTrigger
CodeQLStatic analysis for security vulnerabilitiesPush/PR to main + weekly schedule
OSV-ScannerDependency vulnerability scanning (production deps only)Push/PR to main
DependabotAutomated dependency and GitHub Actions updatesWeekly PRs

Local Hooks (Lefthook)

HookToolBehavior
pre-commitGitleaksScans staged files for secrets (skipped if not installed)
pre-pushSemgrepLightweight 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

Event idempotency

Enable pgWorkflowProviders(pool, { idempotency: true }) or the Kysely equivalent after applying optional migration 007_event_idempotency.sql. Retry a request with the same key:

await handle.triggerEvent("PaymentReceived", {
  subject: order,
  idempotencyKey: "payment-provider:event_123",
  idempotencyFingerprint: "payment:pay_456:amount:2500:currency:USD", // Optional input identity.
});

Committed duplicates return the original result without commands, history, or observers running again. A rolled-back execution remains retryable. Fingerprints are caller-supplied; changed subjects are not compared when they are omitted. See Event idempotency for guard/business-failure semantics, JSON results, retention, and external-effect limits.

↑↓ navigate openesc close