skill
duraflows
A durable workflow runtime for TypeScript built on top of @camcima/finita.
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
README
synced Oct 25, 2026A 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, optionally generic over a
TStateunion for type-safecurrentState/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
versionbump fails startup, and in-flight instances stay pinned to the version they started on (or opt intoversionPolicy: "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:
| 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 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:
WorkflowInstanceStoreWorkflowHistoryStoreWorkflowTransactionRunner
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
| 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
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
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.