Symflow is a powerful workflow and state machine engine for Node.js, inspired by Symfony Workflow.
It allows you to define workflows, transition entities between states, and optionally log audit trails.
β Works like Sequelize models or Mongoose schemas
β Explicitly define workflows and retrieve them globally
β Supports event-driven transitions and audit trails
β No reliance on JSON or YAML configuration files
β Works with or without Express.js
- π¦ Introduction
- π Getting Started
- β‘ Using Symflow with Express.js
- π Features
- π₯ Event Handling in Symflow
- π API Reference
- π Roadmap
- π License
- π€ Contributing
- β Support
- π Workflow Definition Structure
- π Place Structure
- π Transition Structure
npm install symflow
You can define a workflow like a Sequelize model or Mongoose schema.
π src/workflows/order.workflow.ts
import { Symflow } from 'symflow';
export const OrderWorkflow = new Symflow({
name: 'order',
auditTrail: { enabled: true },
stateField: 'state',
initialState: ['draft'],
places: {
draft: {},
pending: {},
confirmed: {},
},
transitions: {
initiate: { from: ['draft'], to: ['pending'] },
confirm: { from: ['pending'], to: ['confirmed'] },
},
events: {
[WorkflowEventType.GUARD]: [
(event) => {
if (event.entity.userRole !== 'admin') {
console.log('β Access Denied: Only admins can approve orders.');
return false;
}
return true;
},
],
[WorkflowEventType.COMPLETED]: [
(event) => console.log(`β
Order transitioned to ${event.toState}`),
],
},
});
Once a workflow is defined, you can retrieve it from anywhere in your project.
import { Symflow } from "symflow";
const workflow = Symflow.use("order"); // Retrieve registered workflow
const order = { id: 1, state: ["draft"] };
workflow.apply(order, "initiate");
console.log(order.state); // Output: ["pending"]
const transitions = workflow.getAvailableTransitions(order);
console.log(transitions); // Output: ["confirm"]
if (workflow.canTransition(order, "confirm")) {
workflow.apply(order, "confirm");
}
console.log(order.state); // Output: ["confirmed"]
import { AuditTrail } from "symflow/audit-trail";
const logs = await AuditTrail.getAuditTrail("order", order.id);
console.log(logs);
Symflow does not require Express, but you can integrate it into your Express.js project.
π Project Structure
/your-express-app
βββ /src
β βββ server.ts # Express server
β βββ workflows # Folder for workflow definitions
β β βββ order.workflow.ts
βββ package.json # Your project's dependencies
β
Example API (src/server.ts
)
import express from "express";
import bodyParser from "body-parser";
import { Symflow } from "symflow";
import { AuditTrail } from "symflow/audit-trail";
import "./workflows/order.workflow"; // Ensures workflows are registered
const app = express();
const PORT = process.env.PORT || 3000;
app.use(bodyParser.json());
const entities: Record<number, { id: number; state: string[] }> = {
1: { id: 1, state: ["draft"] },
};
// πΉ Retrieve the registered workflow
const orderWorkflow = Symflow.use("order");
app.get("/entities/:id", (req, res) => {
const entityId = Number(req.params.id);
res.json(entities[entityId]);
});
app.post("/entities/:id/transition", async (req, res) => {
const entityId = Number(req.params.id);
const { transition } = req.body;
if (!orderWorkflow.canTransition(entities[entityId], transition)) {
return res.status(400).json({ error: "Transition not allowed" });
}
await orderWorkflow.apply(entities[entityId], transition);
res.json({ message: "Transition applied", entity: entities[entityId] });
});
app.listen(PORT, () => console.log(`π Server running at http://localhost:${PORT}`));
β Run the Express API
npx ts-node src/server.ts
β Test the API
curl http://localhost:3000/entities/1
curl -X POST http://localhost:3000/entities/1/transition -H "Content-Type: application/json" -d '{ "transition": "initiate" }'
curl http://localhost:3000/entities/1/audit-trail
Example
You can find a complete example of using Symflow with Express.js at: Symflow-Express Example
- Workflows are explicitly defined and can be retrieved globally.
- No automatic singleton behavior β workflows must be registered manually.
- State Machine: Enforces a single active state.
- Workflow: Allows multiple active states.
- Supports AND/OR conditions for complex transitions.
- Define external event listeners for transitions.
- Logs state changes to JSON files or a database.
- Works optionally with Express.js without modifying the core package.
Symflow allows you to hook into various workflow events using event listeners.
Event Type | Description |
---|---|
ANNOUNCE |
Fires before a transition begins. |
GUARD |
Prevents transitions if conditions are not met. |
LEAVE |
Fires before leaving a state. |
ENTER |
Fires before entering a state. |
TRANSITION |
Fires during a transition. |
COMPLETED |
Fires after a transition successfully completes. |
ENTERED |
Fires after a state is successfully entered. |
You can register event listeners to customize transition behavior.
import { Symflow, WorkflowEventType } from "symflow";
// Define the workflow
const workflowDefinition = {
name: "order_workflow",
stateField: "status",
initialState: ["draft"],
places: { draft: {}, pending: {}, confirmed: {} },
transitions: { approve: { from: ["draft"], to: ["pending"] } },
/* or */
events: {
[WorkflowEventType.GUARD]: [
(event) => {
if (event.entity.userRole !== "admin") {
console.log("β Access Denied: Only admins can approve orders.");
return false;
}
return true;
},
],
},
};
// Create a workflow instance
const workflow = new Symflow(workflowDefinition);
// Register a Guard event to prevent unauthorized transitions
workflow.on(WorkflowEventType.GUARD, (event) => {
console.log(`Checking guard for transition "${event.transition}"`);
if (event.entity.userRole !== "admin") {
console.log("β Access Denied: Only admins can approve orders.");
return false; // π« Prevent transition
}
return true;
});
// Sample order entity
const order = { id: 1, status: ["draft"], userRole: "customer" };
// Attempt transition
workflow.apply(order, "approve").catch((err) => console.log(err.message));
// Output: β Access Denied: Only admins can approve orders.
Metadata can be included in transitions and is accessible inside events.
workflow.on(WorkflowEventType.COMPLETED, (event) => {
console.log(`β
Transition "${event.transition}" completed!`);
console.log(`π Metadata:`, event.metadata); // β
Metadata is now accessible
});
You can use the COMPLETED
event to log successful state changes.
workflow.on(WorkflowEventType.COMPLETED, (event) => {
console.log(`β
Order ${event.entity.id} successfully transitioned to ${event.toState}`);
});
Symflow supports emitting events via Node.js EventEmitter
for full flexibility and code-splitting.
event-emitter.md
- Defines a new workflow that can be used globally.
- Defines a new workflow that can be used locally.
- Defines a new state machine that can be used locally.
Returns true
if the entity can transition.
Returns a list of available transitions.
Applies a state transition.
Retrieves past transitions.
π Upcoming Features
- Database support for audit trails (MongoDB, PostgreSQL)
- CLI Tool (
symflow list-workflows
) - Hot-reloading for workflow changes
- Real-time WebSocket events
MIT License. Free to use and modify.
Pull requests are welcome! Open an issue if you have feature requests.
If you like Symflow, give it a β on GitHub and npm.
π Symflow β The Simple & Flexible Workflow Engine for Node.js!
A workflow definition consists of the following properties:
Property | Type | Description |
---|---|---|
name |
string |
Unique name for the workflow. |
auditTrail |
boolean or { enabled: boolean } |
Enables or disables audit trail logging. |
stateField |
string |
The field in the entity that tracks state. |
initialState |
string or string[] |
The initial state(s) of the workflow. |
places |
Record<string, Place> |
A dictionary of valid places (states). |
transitions |
Record<string, Transition> |
A dictionary of allowed transitions. |
events |
Record<WorkflowEventType, WorkflowEventHandler<T>[]> |
Event listeners for workflow events. |
Each place (or state) in the workflow is defined as:
Property | Type | Description |
---|---|---|
metadata |
Record<string, any> (optional) |
Additional metadata for the place. |
β Example Place Definition:
places: {
draft: { metadata: { label: "Draft Order" } },
pending: { metadata: { label: "Awaiting Approval" } },
confirmed: { metadata: { label: "Confirmed Order" } }
}
Each transition defines how an entity moves between states.
Property | Type | Description |
---|---|---|
from |
string or string[] |
The state(s) the transition starts from. |
to |
string or string[] |
The state(s) the transition moves to. |
metadata |
Record<string, any> (optional) |
Additional metadata for the transition. |
β Example Transition Definition:
transitions: {
initiate: { from: ["draft"], to: ["pending"], metadata: { action: "User submits order" } },
confirm: { from: ["pending"], to: ["confirmed"], metadata: { action: "Admin confirms order" } }
}