bpmn-engine
repository·master·Indexed 21 days ago
https://github.com/paed01/bpmn-engineAn open-source JavaScript execution engine for BPMN 2.0 workflows. It allows developers to run business process models in JS environments, providing core support for BPMN 2.0 elements and attributes. The engine includes features for state persistence via getState() and recover(), dynamic source addition, and an Execution API to signal active processes, manage activity lifecycle events, and inject custom services into ScriptTasks.
What's inside bpmn-engine
- bpmn-engine is an open-source JavaScript execution engine for BPMN 2.0 workflows. It provides core support for BPMN 2.0 elements and attributes, following the BPMN 2.0 scheme. While it focuses on core BPMN 2.0 support, it is designed to be extensible to understand other schemas and elements.
Understand Engine activityStatus states
masterThe
activityStatusproperty describes the current execution state of the engine. It helps you understand if the engine is actively processing, waiting, or idle.Status Description executingAt least one activity is currently executing (e.g., a service task making an async request). timerAt least one activity is waiting for a timer to complete (usually TimerEventDefinitions).waitAt least one activity is waiting for a signal (e.g., user tasks, intermediate catch events). idleThe engine is not running at all; no activities are running. Use Exclusive Gateways with JavaScript condition expressions
masterExclusive gateways use condition expressions to determine the next path. In
bpmn-engine, these expressions are evaluated in a JavaScript context.Inside a
<conditionExpression>, you have access to anext(err, result)callback. To control the flow, callnext(null, boolean)where the boolean determines if the path is taken. Variables are accessed viathis.environment.variables.<sequenceFlow id="flow2" sourceRef="decision" targetRef="end1"> <conditionExpression xsi:type="tFormalExpression" language="JavaScript"><![CDATA[ next(null, this.environment.variables.input <= 50); ]]></conditionExpression> </sequenceFlow>Configure Expression Handlers
masterBy default, the engine uses the
bpmn-elementsexpression handler. If your BPMN processes require advanced logic with complex operators, you should provide a custom expression handler.For advanced requirements, the
aircall-expression-parserby Aircall is recommended.Use an Execution Listener to react to activity events
masterAn execution listener is an
EventEmitterpassed toexecute()that allows you to intercept BPMN activity lifecycle events. This is useful for injecting data, handling user tasks, or triggering external logic.Common Events:
activity.enter: Fired when an activity is entered.activity.wait: Fired when an activity is waiting for an external signal (e.g., a User Task or Catch Event).activity.start: Fired when an activity starts.
When an activity is in a
waitstate, you can useelementApi.signal(data)to provide the required input and resume the process.import { EventEmitter } from 'node:events'; const listener = new EventEmitter(); listener.on('activity.wait', (elementApi) => { // Provide data to the waiting activity elementApi.signal({ ioSpecification: { dataOutputs: [{ id: 'userInput', value: 2 }] } }); }); engine.execute({ listener });Upgrade saved state from version < 14
masterIn versions prior to v14, the engine output was shared between definitions and processes. From v14 onwards, these are decoupled. If you are attempting to load a saved state created with a version older than v14, you must manually add the process environment to the state to ensure compatibility with newer engine versions.
You can use the following script to polyfill the process environment by copying the definition's environment into each process within the state.
export function upgradeStateToVersion14(state) { const stateVersion = getSemverVersion(state.engineVersion); if (!stateVersion || stateVersion.major >= 14) return state; return polyfillProcessEnvironment(state); } function polyfillProcessEnvironment(state) { if (!state.definitions?.length) return state; const polyfilledState = JSON.parse(JSON.stringify(state)); for (const definition of polyfilledState.definitions) { if (!definition.environment) continue; if (!definition.execution) continue; if (!definition.execution.processes) continue; for (const bp of definition.execution.processes) { addProcessEnvironment(definition.environment, bp); } } return polyfilledState; } function addProcessEnvironment(environment, processState) { processState.environment = JSON.parse(JSON.stringify(environment)); } function getSemverVersion(version) { if (typeof version !== 'string') return; const match = version.match(/^(\d+)\.(\d+)\.(\d+)/); if (!match) return; const [, major, minor, patch] = match; return { major: Number(major), minor: Number(minor), patch: Number(patch), }; }Implement User Tasks via signals
masterUser tasks are asynchronous points in the process that wait for external input.
- The engine emits a
'wait'event on the listener when it reaches auserTask. - Use the
elementApi.signal(data)method to provide the required input. - To capture the output of a user task, listen for the
'activity.end'event on the listener and manually map theelementApi.content.outputto theengineApi.environment.output.
listener.once('wait', (elementApi) => { elementApi.signal({ sirname: 'von Rosen', }); }); listener.on('activity.end', (elementApi, engineApi) => { if (elementApi.content.output) { engineApi.environment.output[elementApi.id] = elementApi.content.output; } });- The engine emits a
Save and Recover engine state
masterThe engine supports persistence by allowing you to capture the current execution state and later recover it to resume a process.
1. Capture State with
getState()getState()is an asynchronous method that returns a serialized object representing the current engine state, including variables, services, and the status of all definitions and processes.2. Prepare for Recovery with
recover(state)To prepare an engine to resume from a saved state, call
recover(state). Warning: This will throw an error if the engine is currently running.3. Resume with
resume([options])Once recovered, call
resume()to continue execution. You can pass anoptionsobject (e.g., containing alistener) toresume()to configure the resumed session. Warning: Attempting to resume a running engine will return an error in the callback or throw an error.// To Save const state = await engine.getState(); // To Resume const engine = new Engine().recover(state); engine.resume({ listener }, (err, execution) => { if (err) throw err; console.log('Resumed and completed'); });Persist engine state using event listeners
masterTo persist the state of a BPMN execution, you can subscribe to specific engine events using a listener. By listening to activity lifecycle events, you can capture the current state of the execution and publish it to an external system (like a database or message broker).
Key events for state persistence include:
activity.wait: Triggered when an activity enters a waiting state.activity.end: Triggered when an activity completes.activity.timer: Triggered when a timer event is encountered; provides expiration timing viaapi.content.startedAtandapi.content.timeout.activity.timeout: Triggered when a timer expires.end: Triggered when the entire engine execution completes.error: Triggered when an error occurs during execution.
import { Engine } from 'bpmn-engine'; import { EventEmitter } from 'node:events'; // ... setup code ... const listener = new EventEmitter(); // Capture state on activity wait listener.on('activity.wait', (_, execution) => { const state = execution.getState(); // Persist state here }); // Capture state on activity completion listener.on('activity.end', (_, execution) => { const state = execution.getState(); // Persist state here }); // Handle timer expiration listener.on('activity.timer', (api, execution) => { const expires = new Date(api.content.startedAt + api.content.timeout); const state = execution.getState(); // Persist expiration info and state }); // Handle engine completion engine.once('end', () => { // Handle completion }); // Handle engine errors engine.once('error', (err) => { // Handle error });Implement Script Tasks with custom services
masterScript tasks execute JavaScript code defined within the BPMN XML.
- Context: The script has access to
this.environment.servicesandthis.environment.variables. - Completion: You must call the
next(err, result)callback to complete the task. - Services: You can inject custom logic (like API clients) into the engine via the
servicesoption in.execute(), making them available to the script viathis.environment.services.
// In BPMN XML /* <scriptTask id="scriptTask" scriptFormat="Javascript"> <script> <![CDATA[ const self = this; const getJson = self.environment.services.get; getJson('https://example.com/test').then((result) => { next(null, {result}); }); ]]> </script> </scriptTask> */ */ // In JavaScript execution engine.execute({ services: { get: (url) => fetch(url).then(r => r.json()), }, });- Context: The script has access to
Debug bpmn-engine using environment variables
masterThe engine uses the
debuglibrary for logging. You can activate debugging by setting theDEBUGenvironment variable tobpmn-engine:*.To achieve more granular debugging, you can filter by specific element types (e.g.,
*scripttask*) or error states (:error:).Linux/macOS: Set
DEBUG=bpmn-engine:*before your execution command.Windows PowerShell:
- To enable:
$env:DEBUG='bpmn-engine:*' - To disable:
$env:DEBUG=''
# Granular debugging for script tasks and errors DEBUG=*scripttask*,*:error:* # Windows PowerShell setup $env:DEBUG='bpmn-engine:* # Turn off debugging in PowerShell $env:DEBUG=''- To enable:
Execute a BPMN process with Engine.execute()
masterTo run a BPMN process, instantiate the
Engineclass with asource(BPMN XML string) and an optionalname. You can provide initialvariablesduring instantiation or within the.execute()method. The.execute()method accepts a callback that receives an error and theexecutionobject upon completion. Process variables are accessible viaexecution.environment.variables.import { Engine } from 'bpmn-engine'; const source = `...BPMN XML...`; const engine = new Engine({ name: 'execution example', source, variables: { id: 123, }, }); engine.execute((err, execution) => { console.log('Execution completed with id', execution.environment.variables.id); });