Skip to main content

@c-a-f/workflow

Framework-agnostic workflow and state machine management for CAF. Built on Ploc for reactive state. Define states, transitions, guards, actions, and effects.

Installation

npm install @c-a-f/workflow @c-a-f/core

Features

FeatureDescription
IWorkflowInterface for workflow/state machine implementations.
WorkflowDefinitionid, initialState, states (each with id, label, transitions, onEnter, onExit).
WorkflowState, WorkflowTransitionState and transition definitions; transitions can have target, guard, action.
WorkflowStateSnapshotcurrentState, context, isFinal.
WorkflowManagerExtends Ploc; reactive. subscribe, dispatch(event), canTransition(event), updateContext, reset.
Guard combinatorsand, or, not, always, never, equals, exists, matches. (@c-a-f/workflow/guards)
Action helperslog, updateContext, callService, sequence, parallel, conditional, retry, timeout. (@c-a-f/workflow/actions)
EffectsonStateEnter, onStateExit, onTransition, onFinalState, onStateChange, createEffect, createEffects. (@c-a-f/workflow/effects)

Workflow definition

import { WorkflowManager, WorkflowDefinition } from '@c-a-f/workflow';

const orderWorkflow: WorkflowDefinition = {
id: 'order',
initialState: 'pending',
states: {
pending: {
id: 'pending',
label: 'Pending',
transitions: {
approve: { target: 'approved', guard: (ctx) => ctx.userRole === 'admin' },
cancel: { target: 'cancelled' },
},
},
approved: {
id: 'approved',
label: 'Approved',
transitions: { ship: { target: 'shipped' } },
},
shipped: { id: 'shipped', label: 'Shipped', transitions: {} },
cancelled: { id: 'cancelled', label: 'Cancelled', transitions: {} },
},
};

const workflow = new WorkflowManager(orderWorkflow, { userRole: 'admin' });
workflow.subscribe((snapshot) => console.log('Current state:', snapshot.currentState));

await workflow.dispatch('approve');
await workflow.dispatch('ship');
if (workflow.canTransition('approve')) await workflow.dispatch('approve');

workflow.updateContext({ orderId: '12345' });
await workflow.reset();

Guards

import { and, or, equals, exists } from '@c-a-f/workflow/guards';

// In a transition:
guard: and(
(ctx) => ctx.userRole === 'admin',
or((ctx) => ctx.orderAmount > 1000, (ctx) => ctx.isVip === true)
),
guard: equals('canCancel', true),
guard: exists('paymentConfirmed'),

Actions

import { log, updateContext, callService, sequence, parallel, conditional, retry } from '@c-a-f/workflow/actions';

// In a transition or onEnter/onExit:
action: sequence(
log('Approving order...'),
updateContext({ status: 'approved', approvedAt: new Date() }),
callService(async (ctx) => await orderService.approve(ctx.orderId))
),
action: parallel(
callService(async (ctx) => await shippingService.createShipment(ctx.orderId)),
callService(async (ctx) => await notificationService.send(ctx.orderId))
),
onEnter: retry(callService(async (ctx) => await deliveryService.schedule(ctx.orderId)), 3, 1000),

Effects

import { createEffect, onStateEnter, onStateExit, onTransition, onFinalState } from '@c-a-f/workflow/effects';

createEffect(workflow, onStateEnter('approved', async (snapshot) => {
await notificationService.sendApprovalNotification(snapshot.context.orderId);
}));

createEffect(workflow, onStateExit('pending', async (snapshot) => {
console.log('Order is no longer pending');
}));

createEffect(workflow, onTransition(async (from, to, snapshot) => {
await auditService.logTransition(snapshot.context.orderId, from, to);
}));

createEffect(workflow, onFinalState(async (snapshot) => {
await analyticsService.trackCompletion(snapshot.context.orderId);
}));

Exports

  • Main: IWorkflow, WorkflowDefinition, WorkflowState, WorkflowTransition, WorkflowStateSnapshot, WorkflowManager, WorkflowStateId, WorkflowEventId, WorkflowContext, WorkflowGuard, WorkflowAction
  • /guards: and, or, not, always, never, equals, exists, matches
  • /actions: log, updateContext, callService, sequence, parallel, conditional, retry, timeout
  • /effects: onStateEnter, onStateExit, onTransition, onFinalState, onStateChange, createEffect, createEffects

Dependencies

  • @c-a-f/core — Core (Ploc)