@zola_do/workflow-engine
v0.2.9
Published
State machine workflow converter for NestJS
Downloads
874
Maintainers
Readme
@zola_do/workflow-engine
State machine workflow converter for NestJS. Converts UI JSON (node/edge graph) to XState-compatible state machine definitions.
Overview
@zola_do/workflow-engine provides:
- UI JSON Conversion — Transform React Flow / node-editor JSON to state machines
- XState Compatibility — Output suitable for XState or similar libraries
- Node Type Support — Handles start, parallel, and custom node types
- State Transitions — Automatic edge-to-transition mapping
Installation
# Install individually
npm install @zola_do/workflow-engine
# Or via meta package
npm install @zola_do/nestjs-sharedQuick Start
1. Register Service
import { Module } from '@nestjs/common';
import { WorkflowEngineService } from '@zola_do/workflow-engine';
@Module({
providers: [WorkflowEngineService],
exports: [WorkflowEngineService],
})
export class WorkflowModule {}2. Convert Workflow
import { Injectable } from '@nestjs/common';
import { WorkflowEngineService } from '@zola_do/workflow-engine';
@Injectable()
export class WorkflowController {
constructor(private readonly workflowEngine: WorkflowEngineService) {}
@Post('workflows/:id/compile')
async compileWorkflow(@Param('id') id: string) {
const uiJson = await this.workflowService.getUiJson(id);
const stateMachine = this.workflowEngine.convertToStateMachine(uiJson);
return {
definition: stateMachine,
xstateReady: true,
};
}
}Workflow Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Workflow Conversion Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ UI Editor (React Flow) │
│ ┌─────────────────────────────────────────┐ │
│ │ nodes: [ │ │
│ │ { id: '1', type: 'start', ... }, │ │
│ │ { id: '2', type: 'task', ... }, │ │
│ │ { id: '3', type: 'end', ... } │ │
│ │ ] │ │
│ │ edges: [ │ │
│ │ { source: '1', target: '2' }, │ │
│ │ { source: '2', target: '3' } │ │
│ │ ] │ │
│ └────────────────────┬────────────────────┘ │
│ │ │
│ │ convertToStateMachine() │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ State Machine Definition │ │
│ │ │ │
│ │ { │ │
│ │ id: 'workflow-uuid', │ │
│ │ initial: 'Start', │ │
│ │ states: { │ │
│ │ Start: { │ │
│ │ on: { NEXT: 'Task' } │ │
│ │ }, │ │
│ │ Task: { │ │
│ │ on: { DONE: 'End' } │ │
│ │ }, │ │
│ │ End: { type: 'final' } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘Input Format
Node Types
interface WorkflowNode {
id: string;
type: 'start' | 'end' | 'task' | 'condition' | 'parallel' | string;
data: {
label?: string;
[key: string]: any;
};
position?: { x: number; y: number };
}Edge Types
interface WorkflowEdge {
id?: string;
source: string; // Source node ID
target: string; // Target node ID
label?: string; // Event/transition name
data?: {
condition?: string; // For conditional edges
[key: string]: any;
};
}Example UI JSON
const uiJson = {
nodes: [
{
id: 'node-1',
type: 'start',
data: { label: 'Start' },
position: { x: 100, y: 100 },
},
{
id: 'node-2',
type: 'task',
data: { label: 'Review Application' },
position: { x: 300, y: 100 },
},
{
id: 'node-3',
type: 'condition',
data: { label: 'Approved?' },
position: { x: 500, y: 100 },
},
{
id: 'node-4',
type: 'task',
data: { label: 'Send Approval Email' },
position: { x: 700, y: 50 },
},
{
id: 'node-5',
type: 'task',
data: { label: 'Send Rejection Email' },
position: { x: 700, y: 150 },
},
{
id: 'node-6',
type: 'end',
data: { label: 'End' },
position: { x: 900, y: 100 },
},
],
edges: [
{ source: 'node-1', target: 'node-2' },
{ source: 'node-2', target: 'node-3' },
{ source: 'node-3', target: 'node-4', label: 'YES' },
{ source: 'node-3', target: 'node-5', label: 'NO' },
{ source: 'node-4', target: 'node-6' },
{ source: 'node-5', target: 'node-6' },
],
};Output Format
State Machine Structure
interface StateMachine {
id: string; // UUID
initial: string; // Initial state name
states: {
[stateName: string]: {
type?: 'final' | 'atomic';
on?: {
[event: string]: string | StateMachine;
};
meta?: {
type?: string;
[key: string]: any;
};
states?: {
[nestedState: string]: any;
};
};
};
}Example Output
const stateMachine = {
id: '550e8400-e29b-41d4-a716-446655440000',
initial: 'Start',
states: {
Start: {
on: { NEXT: 'Review Application' },
meta: { type: 'start' },
},
'Review Application': {
on: { NEXT: 'Approved?' },
meta: { type: 'task' },
},
'Approved?': {
on: {
YES: 'Send Approval Email',
NO: 'Send Rejection Email',
},
meta: { type: 'condition' },
},
'Send Approval Email': {
on: { NEXT: 'End' },
meta: { type: 'task' },
},
'Send Rejection Email': {
on: { NEXT: 'End' },
meta: { type: 'task' },
},
End: {
type: 'final',
meta: { type: 'end' },
},
},
};Node Type Handling
Start Node
{
id: 'node-1',
type: 'start',
data: { label: 'Start' },
}
// Becomes: initial state in outputEnd Node
{
id: 'node-2',
type: 'end',
data: { label: 'End' },
}
// Becomes: { type: 'final' } stateTask Node
{
id: 'node-3',
type: 'task',
data: { label: 'Process Order' },
}
// Becomes: { on: { NEXT: 'nextState' }, meta: { type: 'task' } }Condition Node
{
id: 'node-4',
type: 'condition',
data: { label: 'Is Valid?' },
}
// Becomes: { on: { YES: 'trueState', NO: 'falseState' } }Parallel Node
{
id: 'node-5',
type: 'parallel',
data: { label: 'Parallel Tasks' },
}
// Becomes: { type: 'parallel', states: { ... } }Real-World Examples
Approval Workflow
const approvalWorkflow = {
nodes: [
{ id: '1', type: 'start', data: { label: 'Submit' } },
{ id: '2', type: 'task', data: { label: 'Manager Review' } },
{ id: '3', type: 'condition', data: { label: 'Approved?' } },
{ id: '4', type: 'task', data: { label: 'Send Approval' } },
{ id: '5', type: 'task', data: { label: 'Notify Rejection' } },
{ id: '6', type: 'end', data: { label: 'Done' } },
],
edges: [
{ source: '1', target: '2' },
{ source: '2', target: '3' },
{ source: '3', target: '4', label: 'YES' },
{ source: '3', target: '5', label: 'NO' },
{ source: '4', target: '6' },
{ source: '5', target: '6' },
],
};
const stateMachine = workflowEngine.convertToStateMachine(approvalWorkflow);Multi-Step Form
const formWorkflow = {
nodes: [
{ id: 's1', type: 'start', data: { label: 'Step 1' } },
{ id: 's2', type: 'task', data: { label: 'Personal Info' } },
{ id: 's3', type: 'task', data: { label: 'Address' } },
{ id: 's4', type: 'task', data: { label: 'Payment' } },
{ id: 's5', type: 'condition', data: { label: 'Valid?' } },
{ id: 's6', type: 'end', data: { label: 'Complete' } },
],
edges: [
{ source: 's1', target: 's2' },
{ source: 's2', target: 's3' },
{ source: 's3', target: 's4' },
{ source: 's4', target: 's5' },
{ source: 's5', target: 's6', label: 'YES' },
{ source: 's5', target: 's2', label: 'NO' }, // Go back
],
};XState Integration
The output is compatible with XState:
import { createMachine } from 'xstate';
import { interpret } from 'xstate';
const machine = createMachine(stateMachine);
const actor = interpret(machine).start();
actor.send({ type: 'NEXT' }); // Transition to next state
actor.send({ type: 'YES' }); // Transition on conditionFull XState Example
import { createMachine, interpret } from 'xstate';
@Controller('workflows')
export class WorkflowExecutionController {
@Post(':id/execute')
async executeWorkflow(@Param('id') id: string) {
const definition = await this.workflowService.getCompiledDefinition(id);
const machine = createMachine(definition);
const actor = interpret(machine)
.onTransition((state) => {
console.log('Current state:', state.value);
})
.start();
// Execute workflow steps
const result = await this.executeSteps(actor, definition);
return result;
}
private async executeSteps(actor: any, definition: any) {
const states = Object.keys(definition.states);
for (const state of states) {
if (definition.states[state].type === 'final') {
break;
}
await this.performStateAction(state);
actor.send({ type: 'NEXT' });
}
return { completed: true };
}
}API Reference
Service
class WorkflowEngineService {
convertToStateMachine(uiJson: UiJson): StateMachine;
}Types
interface UiJson {
nodes: WorkflowNode[];
edges: WorkflowEdge[];
}
interface WorkflowNode {
id: string;
type: string;
data: Record<string, any>;
position?: { x: number; y: number };
}
interface WorkflowEdge {
id?: string;
source: string;
target: string;
label?: string;
data?: Record<string, any>;
}
interface StateMachine {
id: string;
initial: string;
states: Record<string, any>;
}Troubleshooting
Q: Node label not used?
Ensure nodes have a data.label property:
// ❌ No label
{ id: '1', type: 'start' }
// ✅ With label
{ id: '1', type: 'start', data: { label: 'Start' } }Q: Edge transitions not working?
Check edge labels match expected events:
// For condition nodes, edge labels become transition events
{ source: '3', target: '4', label: 'YES' }
// Triggers: state.on.YESQ: Parallel nodes not handled?
Parallel nodes create nested state configurations:
{
type: 'parallel',
states: {
trackA: { initial: 'a1', states: { a1: {}, a2: {} } },
trackB: { initial: 'b1', states: { b1: {}, b2: {} } },
},
}Related Packages
None - standalone workflow package
License
ISC
