npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cflow/core

v0.2.1

Published

CFIS — Case Flow Instruction Set. A YAML-driven state machine engine for workflow orchestration.

Readme

CFIS Engine

Case Flow Instruction Set — A YAML-driven, domain-agnostic state machine engine for workflow orchestration.

Define your workflow as YAML. Write handlers in TypeScript. Let the engine manage state transitions, permissions, events, modules, and scheduling.


Table of Contents


Installation

npm install @cflow/core

Requirements: Node.js >= 18, TypeScript >= 5.7 (recommended)

Peer dependencies: None. Ships with yaml and reflect-metadata.


Quick Start

1. Define a workflow in YAML

version: "1.0"
entry: OPEN

metadata_keys:
  - name: assigned_to
    type: string
  - name: priority
    type: number

actions:
  - name: assign
    handler: AssignHandler
  - name: close
    handler: CloseHandler

states:
  - code: OPEN
    name: Open
    actions:
      - name: assign
        on_success:
          state: IN_PROGRESS

  - code: IN_PROGRESS
    name: In Progress
    actions:
      - name: close
        on_success:
          state: CLOSED

  - code: CLOSED
    name: Closed
    actions: []

2. Write handlers

import { BaseHandler, ActionResult } from '@cflow/core';

class AssignHandler extends BaseHandler {
  async execute(payload: { assigneeId: string }): Promise<ActionResult> {
    await this.setString('assigned_to', payload.assigneeId);
    return { success: true };
  }
}

class CloseHandler extends BaseHandler {
  async execute(): Promise<ActionResult> {
    await this.setString('resolution', 'completed');
    return { success: true };
  }
}

3. Create an engine and run it

import { Engine, InMemoryStore, silentLogger } from '@cflow/core';
import fs from 'fs';

const workflow = fs.readFileSync('./workflow.yaml', 'utf8');

const engine = new Engine({
  definition: workflow,
  store: new InMemoryStore(),
  handlers: { AssignHandler, CloseHandler },
  logger: silentLogger,
});

// Create a workflow instance
await engine.createInstance('ticket-1');

// Execute actions
await engine.executeAction('ticket-1', 'assign', { userId: 'u1' }, { assigneeId: 'u2' });
// State is now IN_PROGRESS

await engine.executeAction('ticket-1', 'close', { userId: 'u1' });
// State is now CLOSED

Core Concepts

| Concept | Description | |---|---| | Workflow | A YAML document defining states, actions, transitions, and metadata | | Instance | A running instance of a workflow (e.g. "case-42", "ticket-7") | | State | A named step in the workflow (e.g. OPEN, IN_REVIEW, CLOSED) | | Action | Something a user or system can do in a state (e.g. assign, approve) | | Handler | TypeScript class that executes the logic for an action | | Transition | Rule that determines the next state after an action succeeds/fails | | Module | A separate sub-workflow that can be entered and exited from the main flow | | Metadata | Key-value data attached to each instance (stored via MetadataStore) | | Auto Instruction | Timed/scheduled action that fires automatically in a state |


Workflow YAML Schema

Top-level structure

version: "1.0"              # Required. Schema version.
entry: ST001                 # Optional. Initial state code. Defaults to first state.

metadata_keys: [...]         # Optional. Declared metadata fields.
actions: [...]               # Required. Global action definitions.
states: [...]                # Required. At least one state.
imports: [...]               # Optional. Module imports.
global_permissions: [...]    # Optional. Role-based permission declarations.

States

states:
  - code: ST001              # Required. Unique state identifier.
    name: Initial Review     # Optional. Human-readable name.
    description: "..."       # Optional. Description.
    actions:                 # Actions available in this state.
      - name: approve
        required_permissions:
          - reviewer
        on_success:
          state: ST002
        on_failure:
          state: ERROR
    auto:                    # Optional. Scheduled auto-instructions.
      - job_name: reminder
        time: "48h"
        function: send_reminder

Actions

Actions are defined globally and referenced by name in states:

actions:
  - name: approve
    handler: ApproveHandler       # Maps to a registered handler class.
    permissions:                  # Optional. Roles required to execute.
      - reviewer
      - admin

Transitions

Transitions are declared on state actions via on_success, on_failure, and on_complete. Three equivalent YAML shapes are supported — all are normalized internally to TransitionRule[]:

Shape 1 — Direct (unconditional):

on_success:
  state: ST002

Shape 2 — Array of conditional rules:

on_success:
  - if: "approved == true"
    then:
      state: ST002
  - if: "true"
    then:
      state: ST003

Shape 3 — Conditions wrapper:

on_success:
  conditions:
    - if: "doc_count >= 3"
      then:
        state: ST002
    - if: "true"
      then:
        state: REJECTED

Transition targets can be:

| Target | Description | |---|---| | state: "ST002" | Move to a new state in the current scope | | module: "review" | Enter a sub-workflow module | | state: ":ST004" | Exit the current module and return to state ST004 in the parent |

Metadata Keys

Declare the metadata fields your workflow uses. Defaults are initialized automatically on createInstance():

metadata_keys:
  - name: status
    type: string        # Default: ""
  - name: count
    type: number        # Default: 0
  - name: approved
    type: boolean       # Default: false
  - name: config
    type: json          # Default: null
  - name: tags
    type: list          # Default: []
  - name: votes
    type: vote          # Default: []

Auto Instructions

Schedule actions or transitions to fire automatically when an instance enters a state:

auto:
  - job_name: escalation_timer    # Optional. Identifier for cancellation.
    time: "48h"                   # One-shot delay (supports s/m/h/d).
    function: escalate            # Action to execute.
    condition: "priority >= 3"    # Optional. Only fires if true.

  - job_name: heartbeat
    repeat: "12h"                 # Repeating interval.
    function: send_ping

  - time: "0s"                    # Fires immediately.
    next_state: ST002             # Direct transition (no handler).

Module Imports

imports:
  - name: review
    path: ./modules/review.yaml
  - name: translation
    path: ./modules/translation.yaml

Permissions

global_permissions:
  - role: admin
    can_view_all_cases: true
    can_perform_actions:
      - approve
      - reject
  - role: viewer
    can_view_all_cases: true

Engine API

Constructor (EngineConfig)

import { Engine } from '@cflow/core';

const engine = new Engine({
  // Required
  definition: yamlString,       // YAML string, WorkflowDefinition object, or NormalizedWorkflow
  store: myMetadataStore,       // MetadataStore implementation

  // Optional
  handlers: {                   // Custom handler classes
    MyHandler: MyHandlerClass,
  },
  modules: {                    // Sub-workflow modules
    review: reviewYaml,
  },
  eventStore: myEventStore,     // EventStore for audit trail
  permissions: myPermHook,      // PermissionHook function
  logger: myLogger,             // Logger implementation
  registerBuiltins: true,       // Register built-in handlers (default: true)

  // Event callbacks
  onTransition: (e) => { ... },
  onActionExecuted: (e) => { ... },
  onError: (e) => { ... },
  onModuleEnter: (e) => { ... },
  onModuleExit: (e) => { ... },
  onInstanceCreated: (e) => { ... },
});

createInstance()

await engine.createInstance('case-42', {
  initialState: 'CUSTOM_START',     // Optional. Override entry state.
  createdBy: 'user-1',             // Optional. Tracked in events.
  metadata: { priority: 5 },       // Optional. Pre-set metadata.
});
  • Sets the initial state
  • Initializes the module stack
  • Sets caller-provided metadata
  • Initializes declared metadata_keys with type-appropriate defaults
  • Emits instance:created event
  • Schedules any auto-actions for the initial state

executeAction()

const result = await engine.executeAction(
  'case-42',                              // Instance ID
  'approve',                              // Action name
  { userId: 'u1', roles: ['reviewer'] },  // User context
  { comment: 'Looks good' },             // Optional payload
);
// result: { success: true, data?: any, error?: string }

Execution flow:

  1. Resolves current scope (root or active module)
  2. Validates the action exists in the current state
  3. Checks permissions via the configured hook
  4. Creates a scoped metadata accessor for the handler
  5. Instantiates and executes the handler
  6. Evaluates on_success / on_failure transition rules
  7. Applies the first matching transition

Query Methods

// Read state
const state = await engine.getCurrentState('case-42');
const module = await engine.getCurrentModule('case-42');

// Introspect the definition
const allStates = engine.getAllStates();
const actions = engine.getStateActions('ST001');
const adminActions = engine.getStateActions('ST001', ['admin']);
const keys = engine.getMetadataKeys();
const def = engine.getDefinition();             // Root definition
const modDef = engine.getDefinition('review');  // Module definition

// Validate configuration
const missing = engine.validateHandlers();
// Returns: ["actionName → HandlerName"] for any unregistered handlers

// Access the handler registry
const registry = engine.getHandlerRegistry();

Events

The engine emits events through three channels simultaneously:

  1. EventEmitterengine.on('state:transition', handler)
  2. EventStore — persisted via eventStore.log(event) if configured
  3. Config callbacksonTransition, onActionExecuted, etc.

Event types:

| Event | Fired when | |---|---| | instance:created | createInstance() completes | | action:executed | A handler finishes (success or failure) | | state:transition | The instance moves to a new state | | module:entered | A sub-workflow module is entered | | module:exited | A sub-workflow module is exited | | engine:error | An internal error occurs | | auto:triggered | An auto-scheduled action fires |

// EventEmitter style
engine.on('state:transition', (event) => {
  console.log(`${event.instanceId}: ${event.from} → ${event.to}`);
});

// Callback style (via config)
const engine = new Engine({
  ...config,
  onTransition: (event) => {
    console.log(`${event.from} → ${event.to}`);
  },
});

Handlers

Writing a Handler

Extend BaseHandler and implement execute():

import { BaseHandler, ActionResult } from '@cflow/core';

class AssignCaseHandler extends BaseHandler {
  async execute(payload: { assigneeId: string }): Promise<ActionResult> {
    const previous = await this.getString('assigned_to');
    await this.setString('assigned_to', payload.assigneeId);

    this.logger.info(`Reassigned from ${previous} to ${payload.assigneeId}`);

    return {
      success: true,
      data: { previous, current: payload.assigneeId },
    };
  }
}

Every handler receives an injected HandlerContext with:

| Property | Type | Description | |---|---|---| | this.instanceId | InstanceId | The workflow instance being acted on | | this.user | UserContext | The user performing the action | | this.store | MetadataAccessor | Scoped metadata read/write | | this.logger | Logger | Logger instance |

BaseHandler Convenience Methods

All methods are protected and async:

| Method | Description | |---|---| | getString(key) | Get a string value | | setString(key, value) | Set a string value | | getNumber(key) | Get a number value | | setNumber(key, value) | Set a number value | | incrementNumber(key, by?) | Increment a number (default +1), returns new value | | getBoolean(key) | Get a boolean value | | setBoolean(key, value) | Set a boolean value | | getArray(key) | Get an array (returns [] if missing) | | appendToArray(key, item) | Append to an array | | removeFromArray(key, item) | Remove from an array | | getJson(key) | Get a JSON/object value | | setJson(key, value) | Set a JSON/object value | | mergeJson(key, partial) | Shallow-merge into an existing object | | get(key) | Generic get | | set(key, value) | Generic set | | getAll() | Get all metadata in current scope |

Built-in Handlers

Registered automatically unless registerBuiltins: false:

| Registry Name | Class | Payload | Description | |---|---|---|---| | SetMetaKeyAction | SetMetaKeyHandler | { metaKey, value } | Set any metadata key | | AppendToMetaArrayAction | AppendToMetaArrayHandler | { metaKey, value } | Append to array key | | IncrementMetaKeyAction | IncrementMetaKeyHandler | { metaKey, by? } | Increment numeric key | | VoteAction | VoteHandler | { metaKey, userId, voteValue } | Record a vote |

Handler Registry

The registry is instance-level (per engine), not global:

const registry = engine.getHandlerRegistry();

registry.register('MyHandler', MyHandlerClass);
registry.has('MyHandler');        // true
registry.get('MyHandler');        // MyHandlerClass
registry.list();                  // ['SetMetaKeyAction', ..., 'MyHandler']
registry.getMetadata('MyHandler'); // Decorator metadata (see below)
registry.clear();

Decorators

Attach UI metadata to handlers for dynamic form generation:

import { BaseHandler, HandlerMeta, UIField } from '@cflow/core';

@HandlerMeta({
  name: 'Assign Case',
  description: 'Assigns a case to a team member',
  category: 'Admin',
})
class AssignCaseHandler extends BaseHandler {
  @UIField({ label: 'Assignee', name: 'assigneeId', type: 'assign', required: true })
  assigneeId!: string;

  @UIField({ label: 'Priority', name: 'priority', type: 'select', options: ['low', 'medium', 'high'] })
  priority!: string;

  async execute(payload: any) {
    await this.setString('assigned_to', payload.assigneeId);
    return { success: true };
  }
}

Read metadata at runtime:

import { getHandlerMetadata } from '@cflow/core';

const meta = getHandlerMetadata(AssignCaseHandler);
// meta.handler → { name: 'Assign Case', description: '...', category: 'Admin' }
// meta.fields → { assigneeId: { label: 'Assignee', type: 'assign', ... }, ... }

Supported UIField types: text, number, select, checkbox, date, textarea, assign, file, document.


Condition Evaluator

The engine uses a safe expression evaluator (no eval(), no Function()).

Format: left operator right

Operators: ==, !=, >, <, >=, <=

Value types:

  • Numbers: 42, 3.14, -5
  • Booleans: true, false
  • Null: null
  • Strings: "hello" or 'hello'
  • Data paths: status, user.level, doc_count

Special: The literal string "true" evaluates to true (used for unconditional/fallback transitions).

Examples:

# Boolean check
if: "approved == true"

# Numeric comparison
if: "doc_count >= 3"

# String comparison
if: "status == 'completed'"

# Nested path
if: "review.score > 7"

# Unconditional fallback (always matches)
if: "true"

Standalone usage:

import { evaluateCondition } from '@cflow/core';

evaluateCondition('count >= 5', { count: 10 }); // true
evaluateCondition('status == "done"', { status: 'done' }); // true

Modules (Sub-workflows)

Modules allow you to compose workflows from reusable sub-flows. When a module is entered, the engine pushes onto a module stack and switches to the module's states. When the module exits, it pops back to the parent.

Defining a module

# modules/review.yaml
version: "1.0"
entry: REV_START

actions:
  - name: complete_review
    handler: CompleteReviewHandler

states:
  - code: REV_START
    name: Review In Progress
    actions:
      - name: complete_review
        on_success:
          state: ":ST003"    # Exit module, return to ST003 in parent

Entering a module from root

# In the root workflow:
states:
  - code: ST002
    actions:
      - name: start_review
        on_success:
          module: review                    # Enter the "review" module
          expose_global_keys:               # Copy these keys into module scope
            - case_status
            - priority

Wiring it up

const engine = new Engine({
  definition: mainWorkflowYaml,
  store: myStore,
  modules: {
    review: reviewModuleYaml,   // YAML string, object, or NormalizedWorkflow
  },
  handlers: { CompleteReviewHandler },
});

Module stack

The engine supports nested modules (module A enters module B). The stack is persisted in metadata as _cfis.stack.

const currentModule = await engine.getCurrentModule('case-1'); // 'review' | null

Scoped metadata

Inside a module, handler metadata keys are prefixed with the module name (e.g. review:my_key). Global keys exposed via expose_global_keys are copied into the module scope on entry.


Plugins

Permissions Plugin

Two built-in permission hooks:

import { allowAll, roleBasedPermissions } from '@cflow/core';

// Allow everything (development/public workflows)
const engine = new Engine({ permissions: allowAll, ... });

// Role-based: user must have at least one required role
const engine = new Engine({ permissions: roleBasedPermissions, ... });

Custom hook:

const engine = new Engine({
  permissions: async (user, requiredPermissions, actionName, instanceId) => {
    // Your custom logic
    return user.roles?.includes('superadmin') ?? false;
  },
  ...
});

ACL

Per-instance access control backed by metadata:

import { AccessControlList, InMemoryStore } from '@cflow/core';

const store = new InMemoryStore();
const acl = new AccessControlList(store);

// User access
await acl.addUser('user-42', 'case-1');
await acl.hasAccess('user-42', 'case-1');  // true
await acl.removeUser('user-42', 'case-1');

// Org access
await acl.addOrg('org-1', 'case-1');
await acl.hasAccess('org:org-1', 'case-1'); // true

// Owner
await acl.updateOwner('case-1', 'user-1', 'user');
await acl.hasAccess('user-1', 'case-1');    // true (owner has implicit access)
await acl.getOwnerType('case-1');            // 'user'

// Cache management
acl.clearCache('case-1');  // Clear cache for one instance
acl.clearCache();          // Clear all caches

Logging

import { ConsoleLogger, VerboseLogger, silentLogger } from '@cflow/core';

// Console (default) — debug is silent, info/warn/error go to console
const logger = new ConsoleLogger('[myapp]');

// Verbose — includes debug output
const logger = new VerboseLogger('[myapp]');

// Silent — suppresses all output (ideal for tests)
const engine = new Engine({ logger: silentLogger, ... });

Custom logger: Implement the Logger interface:

interface Logger {
  debug(message: string, ...args: any[]): void;
  info(message: string, ...args: any[]): void;
  warn(message: string, ...args: any[]): void;
  error(message: string, ...args: any[]): void;
}

Store Interface

The engine is storage-agnostic. You provide a MetadataStore implementation:

interface MetadataStore {
  getValue(instanceId: InstanceId, key: string): Promise<any>;
  setValue(instanceId: InstanceId, key: string, value: any): Promise<void>;
  getAllMetadata(instanceId: InstanceId): Promise<Record<string, any>>;
  appendToArray(instanceId: InstanceId, key: string, item: any): Promise<void>;
  removeFromArray(instanceId: InstanceId, key: string, item: any): Promise<void>;
}

Built-in: InMemoryStore (for testing and prototyping).

Production examples: PostgreSQL adapter, Redis adapter, MongoDB adapter — implement the 5 methods above.

Optional EventStore for audit trail:

interface EventStore {
  log(event: EngineEvent): Promise<void>;
  getEvents(instanceId: InstanceId): Promise<EngineEvent[]>;
  getRecentEvents(instanceId: InstanceId, limit: number): Promise<EngineEvent[]>;
}

Built-in: InMemoryEventStore (for testing).


Testing

TestHarness

A batteries-included helper for writing workflow tests:

import { TestHarness } from '@cflow/core';

const harness = new TestHarness({
  workflow: myYaml,
  handlers: { MyHandler: MyHandlerClass },
  modules: { review: reviewYaml },       // Optional
  verbose: false,                        // Optional. Default: false (silent).
});

// Create an instance (default ID: 1)
await harness.createInstance();

// Execute actions (default user: admin)
await harness.execute('approve', { comment: 'ok' });

// Assertions
expect(await harness.getState()).toBe('APPROVED');
expect(await harness.getMeta('assigned_to')).toBe('u1');

// Introspection
harness.dumpStore();                          // All metadata as plain object
harness.getEvents();                          // All emitted events
harness.getEventsOfType('state:transition');  // Filter by type

// Reset between tests
harness.reset();

InMemoryStore helpers

const store = new InMemoryStore();
store.dump('case-1');        // All metadata as plain object
store.hasInstance('case-1'); // boolean
store.clear();               // Wipe everything

InMemoryEventStore helpers

const events = new InMemoryEventStore();
events.all();                       // All events
events.ofType('state:transition');  // Filtered by type
events.count;                       // Total event count
events.clear();                     // Wipe everything

Project Structure

@cflow/core - project structure:
├── src/
│   ├── index.ts                    # Public API exports
│   ├── types.ts                    # All type definitions
│   ├── schema/
│   │   ├── parse.ts                # YAML parsing
│   │   ├── validate.ts             # Structural validation
│   │   └── normalize.ts            # Transition normalization
│   ├── runtime/
│   │   ├── Engine.ts               # Core engine (extends EventEmitter)
│   │   ├── ConditionEvaluator.ts   # Safe expression evaluator
│   │   ├── ModuleStack.ts          # Module nesting stack
│   │   ├── ScopedAccessor.ts       # Scoped metadata access
│   │   └── Scheduler.ts            # Auto-action scheduling
│   ├── handlers/
│   │   ├── BaseHandler.ts          # Abstract handler base class
│   │   ├── HandlerRegistry.ts      # Per-engine handler map
│   │   ├── builtins.ts             # SetMetaKey, Append, Increment, Vote
│   │   └── decorators.ts           # @HandlerMeta, @UIField
│   ├── plugins/
│   │   ├── permissions.ts          # allowAll, roleBasedPermissions
│   │   ├── acl.ts                  # AccessControlList
│   │   └── logging.ts              # Console, Verbose, silent loggers
│   └── testing/
│       ├── InMemoryStore.ts        # Map-based MetadataStore
│       ├── InMemoryEventStore.ts   # Array-based EventStore
│       └── TestHarness.ts          # Quick-start test helper
├── tests/
│   ├── fixtures.ts                 # Shared YAML fixtures
│   ├── schema.parse.test.ts
│   ├── schema.validate.test.ts
│   ├── schema.normalize.test.ts
│   ├── condition-evaluator.test.ts
│   ├── engine.test.ts
│   ├── handlers.test.ts
│   ├── decorators.test.ts
│   ├── plugins.test.ts
│   ├── runtime-utils.test.ts
│   ├── test-harness.test.ts
│   └── e2e.test.ts
├── package.json
├── tsconfig.json
└── vitest.config.ts

Scripts

npm run build        # Compile TypeScript → dist/
npm run dev          # Compile in watch mode
npm run clean        # Remove dist/
npm test             # Run all tests (vitest)
npm run test:watch   # Run tests in watch mode

License

MIT