npm packageMIT License

symflow

A Symfony-compatible workflow engine for TypeScript and Node.js. Run state machines and Petri nets anywhere JavaScript runs — no PHP required.

Everything from Symfony's Workflow, in TypeScript

Symfony-Compatible Runtime

State machines and Petri nets with Symfony's exact semantics. Markings, transitions, guards, and events — all in TypeScript.

Event System

Events fire in Symfony order: guard, leave, transition, enter, entered, completed, announce. Subscribe with typed listeners.

Pluggable Guards

Attach guard expressions to transitions and plug in your own evaluator. Wire up role checks, feature flags, or any custom authorization logic.

AND / OR Patterns

Petri-net AND-split (parallel fork), AND-join (synchronization), OR-split (exclusive choice), and XOR patterns for state machines.

Subject-Driven API

Mirrors Symfony's Workflow service. Pass your entity to can() and apply() — marking stores read and write the marking for you.

YAML / JSON / TypeScript

Round-trip import and export for every format. Read existing Symfony configs (including !php/const and !php/enum) or generate typed TypeScript modules.

!php/const & !php/enum

Symfony YAML using PHP constants and backed enums is resolved automatically. App\Enum\Status::Active becomes "Active".

Validation & Analysis

Catch unreachable places, dead transitions, and orphan nodes before runtime. Analyze structural patterns at a glance.

Standalone Engine

Create an engine, validate, apply transitions, listen to events.

engine.ts
import { WorkflowEngine, validateDefinition } from "symflow/engine";

const definition = {
    name: "order",
    type: "state_machine",
    places: [
        { name: "draft" },
        { name: "submitted" },
        { name: "approved" },
        { name: "fulfilled" },
    ],
    transitions: [
        { name: "submit", froms: ["draft"], tos: ["submitted"] },
        { name: "approve", froms: ["submitted"], tos: ["approved"] },
        { name: "fulfill", froms: ["approved"], tos: ["fulfilled"] },
    ],
    initialMarking: ["draft"],
};

// Validate
const { valid, errors } = validateDefinition(definition);

// Run
const engine = new WorkflowEngine(definition);
engine.apply("submit");
engine.getActivePlaces(); // ["submitted"]

// Listen to events
engine.on("entered", (event) => {
    console.log(`Entered via ${event.transition.name}`);
});

Subject-Driven API

Like Symfony — pass your entity, marking stores handle the rest.

workflow.ts
import { createWorkflow, propertyMarkingStore } from "symflow/subject";

interface Order {
    id: string;
    total: number;
    status: string;
}

const workflow = createWorkflow(definition, {
    markingStore: propertyMarkingStore("status"),
    guardEvaluator: (expr, { subject }) => {
        if (expr === "subject.total < 10000") {
            return subject.total < 10000;
        }
        return true;
    },
});

const order = { id: "ord_1", total: 500, status: "draft" };

workflow.apply(order, "submit");
console.log(order.status); // "submitted"

workflow.on("entered", (event) => {
    console.log(event.subject.id, event.transition.name);
});

Import only what you need

ImportContentsExtra deps
symflow/engineWorkflowEngine, validateDefinition, analyzeWorkflownone
symflow/subjectWorkflow<T>, createWorkflow, marking storesnone
symflow/yamlSymfony YAML import/export, !php/const, !php/enumjs-yaml
symflow/jsonJSON import/exportnone
symflow/typescriptTypeScript codegennone
symflow/react-flowReact Flow graph utilities@xyflow/react

Symfony Parity

WorkflowDefinition
Marking
Transition
can()
apply()
getEnabledTransitions()
guard events
leave events
transition events
enter / entered
completed / announce
state_machine
workflow (Petri net)
property marking store
method marking store
!php/const
!php/enum
YAML import/export

Design visually, run anywhere

Build your workflow in SymFlowBuilder, export it, and run it with symflow in Node.js, serverless functions, or the browser.