bpmn-engine

repository·master·Indexed 21 days ago

https://github.com/paed01/bpmn-engine

An 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.

Tokens
17.1K
Snippets
42
Records
54
Agent score
75%

What's inside bpmn-engine

  1. Introduction to bpmn-engine

    master
    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.
  2. Understand Engine activityStatus states

    master

    The activityStatus property describes the current execution state of the engine. It helps you understand if the engine is actively processing, waiting, or idle.

    StatusDescription
    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.
  3. Use Exclusive Gateways with JavaScript condition expressions

    master

    Exclusive 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 a next(err, result) callback. To control the flow, call next(null, boolean) where the boolean determines if the path is taken. Variables are accessed via this.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>
  4. Configure Expression Handlers

    master

    By default, the engine uses the bpmn-elements expression handler. If your BPMN processes require advanced logic with complex operators, you should provide a custom expression handler.

    For advanced requirements, the aircall-expression-parser by Aircall is recommended.

  5. Use an Execution Listener to react to activity events

    master

    An execution listener is an EventEmitter passed to execute() 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 wait state, you can use elementApi.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 });
  6. Upgrade saved state from version < 14

    master

    In 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),
      };
    }
  7. Implement User Tasks via signals

    master

    User tasks are asynchronous points in the process that wait for external input.

    1. The engine emits a 'wait' event on the listener when it reaches a userTask.
    2. Use the elementApi.signal(data) method to provide the required input.
    3. To capture the output of a user task, listen for the 'activity.end' event on the listener and manually map the elementApi.content.output to the engineApi.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;
      }
    });
  8. Save and Recover engine state

    master

    The 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 an options object (e.g., containing a listener) to resume() 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');
    });
  9. Persist engine state using event listeners

    master

    To 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 via api.content.startedAt and api.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
    });
  10. Implement Script Tasks with custom services

    master

    Script tasks execute JavaScript code defined within the BPMN XML.

    • Context: The script has access to this.environment.services and this.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 services option in .execute(), making them available to the script via this.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()),
      },
    });
  11. Debug bpmn-engine using environment variables

    master

    The engine uses the debug library for logging. You can activate debugging by setting the DEBUG environment variable to bpmn-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=''
  12. Execute a BPMN process with Engine.execute()

    master

    To run a BPMN process, instantiate the Engine class with a source (BPMN XML string) and an optional name. You can provide initial variables during instantiation or within the .execute() method. The .execute() method accepts a callback that receives an error and the execution object upon completion. Process variables are accessible via execution.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);
    });