import { Engine } from 'bpmn-engine';
const id = Math.floor(Math.random() * 10000);
const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<process id="theProcess2" isExecutable="true">
<startEvent id="theStart" />
<exclusiveGateway id="decision" default="flow2" />
<endEvent id="end1" />
<endEvent id="end2" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="decision" />
<sequenceFlow id="flow2" sourceRef="decision" targetRef="end1" />
<sequenceFlow id="flow3" sourceRef="decision" targetRef="end2">
<conditionExpression>true</conditionExpression>
</sequenceFlow>
</process>
</definitions>`;
const engine = new Engine({
name: 'execution example',
source,
variables: {
id,
},
});
engine.execute((err, execution) => {
console.log('Execution completed with id', execution.environment.variables.id);
});
import { EventEmitter } from 'node:events';
import { Engine } from 'bpmn-engine';
const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<process id="theProcess" isExecutable="true">
<dataObjectReference id="inputFromUserRef" dataObjectRef="inputFromUser" />
<dataObject id="inputFromUser" />
<startEvent id="theStart" />
<userTask id="userTask">
<ioSpecification id="inputSpec">
<dataOutput id="userInput" name="sirname" />
</ioSpecification>
<dataOutputAssociation id="associatedWith" sourceRef="userInput" targetRef="inputFromUserRef" />
</userTask>
<endEvent id="theEnd" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="userTask" />
<sequenceFlow id="flow2" sourceRef="userTask" targetRef="theEnd" />
</process>
</definitions>`;
const engine = new Engine({
name: 'listen example',
source,
});
const listener = new EventEmitter();
listener.once('wait', (task) => {
task.signal({
ioSpecification: {
dataOutputs: [
{
id: 'userInput',
value: 'von Rosen',
},
],
},
});
});
listener.on('flow.take', (flow) => {
console.log(`flow <${flow.id}> was taken`);
});
engine.once('end', (execution) => {
console.log(execution.environment.variables);
console.log(`User sirname is ${execution.environment.output.data.inputFromUser}`);
});
engine.execute(
{
listener,
},
(err) => {
if (err) throw err;
}
);
An exclusive gateway will receive the available process variables as this.environment.variables
.
import { EventEmitter } from 'node:events';
import { Engine } from 'bpmn-engine';
const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<process id="theProcess" isExecutable="true">
<startEvent id="start" />
<exclusiveGateway id="decision" />
<endEvent id="end1" />
<endEvent id="end2" />
<sequenceFlow id="flow1" sourceRef="start" targetRef="decision" />
<sequenceFlow id="flow2" sourceRef="decision" targetRef="end1">
<conditionExpression xsi:type="tFormalExpression" language="JavaScript"><![CDATA[
next(null, this.environment.variables.input <= 50);
]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow3" sourceRef="decision" targetRef="end2">
<conditionExpression xsi:type="tFormalExpression" language="JavaScript"><![CDATA[
next(null, this.environment.variables.input > 50);
]]></conditionExpression>
</sequenceFlow>
</process>
</definitions>`;
const engine = new Engine({
name: 'exclusive gateway example',
source,
});
const listener = new EventEmitter();
listener.on('activity.start', (api) => {
if (api.id === 'end1') throw new Error(`<${api.id}> was not supposed to be taken, check your input`);
if (api.id === 'end2') console.log(`<${api.id}> correct decision was taken`);
});
engine.execute({
listener,
variables: {
input: 51,
},
});
engine.on('end', () => {
console.log('completed');
});
A script task will receive the data available on the process instance. So if bent
or another module is needed it has to be passed when starting the process. The script task also has a callback called next
that has to be called for the task to complete.
The next
callback takes the following arguments:
err
: occasional errorresult
: optional result of the scriptimport { Engine } from 'bpmn-engine';
import bent from 'bent';
const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<process id="theProcess" isExecutable="true">
<startEvent id="theStart" />
<scriptTask id="scriptTask" scriptFormat="Javascript">
<script>
<![CDATA[
const self = this;
const getJson = self.environment.services.get;
const set = self.environment.services.set;
getJson('https://example.com/test').then((result) => {
self.environment.output.statusCode = 200;
set(self, 'statusCode', 200)
next(null, {result});
}).catch((err) => {
set(self, 'statusCode', err.statusCode);
self.environment.output.statusCode = err.statusCode;
next();
});
]]>
</script>
</scriptTask>
<endEvent id="theEnd" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="scriptTask" />
<sequenceFlow id="flow2" sourceRef="scriptTask" targetRef="theEnd" />
</process>
</definitions>`;
const engine = new Engine({
name: 'script task example',
source,
});
engine.execute({
variables: {
scriptTaskCompleted: false,
},
services: {
get: bent('json'),
set,
},
});
engine.on('end', (execution) => {
console.log('Output:', execution.environment.output);
});
function set(activity, name, value) {
activity.logger.debug('set', name, 'to', value);
}
User tasks waits for signal to complete.
import { EventEmitter } from 'node:events';
import { Engine } from 'bpmn-engine';
const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<process id="theProcess" isExecutable="true">
<startEvent id="start" />
<userTask id="task" />
<endEvent id="end" />
<sequenceFlow id="flow1" sourceRef="start" targetRef="task" />
<sequenceFlow id="flow2" sourceRef="task" targetRef="end" />
</process>
</definitions>`;
const engine = new Engine({
name: 'user task example 1',
source,
});
const listener = new EventEmitter();
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;
});
engine.execute(
{
listener,
},
(err, execution) => {
if (err) throw err;
console.log(`User sirname is ${execution.environment.output.task.sirname}`);
}
);
A service task will receive the data available on the process instance. The signature of the service function is:
scope
: activity scopecallback
:
err
: occasional errorresult
: service call resultimport { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { Engine } from 'bpmn-engine';
import bent from 'bent';
const getJson = bent('json');
const camunda = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json');
async function getRequest(scope, callback) {
try {
var result = await getJson(scope.environment.variables.apiPath); // eslint-disable-line no-var
} catch (err) {
return callback(null, err);
}
return callback(null, result);
}
const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camunda="http://camunda.org/schema/1.0/bpmn">
<process id="theProcess" isExecutable="true">
<startEvent id="theStart" />
<serviceTask id="serviceTask" implementation="\${environment.services.getRequest}" camunda:resultVariable="serviceResult" />
<endEvent id="theEnd" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="serviceTask" />
<sequenceFlow id="flow2" sourceRef="serviceTask" targetRef="theEnd" />
</process>
</definitions>`;
const engine = new Engine({
name: 'service task example 1',
source,
moddleOptions: {
camunda,
},
extensions: {
saveToResultVariable(activity) {
if (!activity.behaviour.resultVariable) return;
activity.on('end', ({ environment, content }) => {
environment.output[activity.behaviour.resultVariable] = content.output[0];
});
},
},
});
engine.execute(
{
variables: {
apiPath: 'https://example.com/test',
},
services: {
getRequest,
},
},
(err, execution) => {
if (err) throw err;
console.log('Service task output:', execution.environment.output.serviceResult);
}
);
or as a expression function call:
import { Engine } from 'bpmn-engine';
const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<process id="theProcess" isExecutable="true">
<serviceTask id="serviceTask" name="Get" implementation="\${environment.services.getService()}" />
</process>
</definitions>`;
const engine = new Engine({
name: 'service task example 3',
source,
});
engine.execute({
services: {
getService(defaultScope) {
if (!defaultScope.content.id === 'serviceTask') return;
return (executionContext, callback) => {
callback(null, executionContext.environment.variables.input);
};
},
},
variables: {
input: 1,
},
extensions: {
saveToEnvironmentOutput(activity, { environment }) {
activity.on('end', (api) => {
environment.output[api.id] = api.content.output;
});
},
},
});
engine.once('end', (execution) => {
console.log(execution.name, execution.environment.output);
});
import { EventEmitter } from 'node:events';
import { Engine } from 'bpmn-engine';
const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions id="testProcess" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<process id="theProcess1" isExecutable="true">
<startEvent id="theStart" />
<exclusiveGateway id="decision" default="flow2" />
<endEvent id="end1" />
<endEvent id="end2" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="decision" />
<sequenceFlow id="flow2" sourceRef="decision" targetRef="end1" />
<sequenceFlow id="flow3withExpression" sourceRef="decision" targetRef="end2">
<conditionExpression xsi:type="tFormalExpression">\${environment.services.isBelow(environment.variables.input,2)}</conditionExpression>
</sequenceFlow>
</process>
</definitions>
`;
const engine = new Engine({
name: 'sequence flow example',
source,
});
const listener = new EventEmitter();
listener.on('activity.end', (elementApi) => {
if (elementApi.id === 'end2') throw new Error(`<${elementApi.id}> should not have been taken`);
});
engine.execute({
listener,
services: {
isBelow: (input, test) => {
return input < test;
},
},
variables: {
input: 2,
},
});
engine.once('end', () => {
console.log('WOHO!');
});
import { Engine } from 'bpmn-engine';
import { extension, moddleOptions } from '../test/resources/JsExtension.js';
const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions id="Definitions_1" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:js="http://paed01.github.io/bpmn-engine/schema/2017/08/bpmn">
<process id="Process_with_loop" isExecutable="true">
<serviceTask id="recurring" name="Each item" implementation="\${environment.services.loop}" js:result="sum">
<multiInstanceLoopCharacteristics isSequential="true" js:collection="\${environment.variables.input}" />
</serviceTask>
<boundaryEvent id="errorEvent" attachedToRef="recurring">
<errorEventDefinition />
</boundaryEvent>
</process>
</definitions>`;
const engine = new Engine({
name: 'loop collection',
source,
moddleOptions: {
js: moddleOptions,
},
extensions: {
js: extension,
},
});
let sum = 0;
engine.execute({
services: {
loop: (executionContext, callback) => {
sum += executionContext.content.item;
callback(null, sum);
},
},
variables: {
input: [1, 2, 3, 7],
},
});
engine.once('end', () => {
console.log(sum, 'aught to be 13 blazing fast');
});
Pass an extend function with options.
import { EventEmitter } from 'node:events';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { Engine } from 'bpmn-engine';
const camunda = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json');
const source = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions id="pending" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camunda="http://camunda.org/schema/1.0/bpmn">
<process id="theWaitingGame" isExecutable="true">
<startEvent id="start" />
<parallelGateway id="fork" />
<userTask id="userTask1">
<extensionElements>
<camunda:formData>
<camunda:formField id="surname" label="Surname" type="string" />
<camunda:formField id="givenName" label="Given name" type="string" />
</camunda:formData>
</extensionElements>
</userTask>
<userTask id="userTask2" />
<task id="task" />
<parallelGateway id="join" />
<endEvent id="end" />
<sequenceFlow id="flow1" sourceRef="start" targetRef="fork" />
<sequenceFlow id="flow2" sourceRef="fork" targetRef="userTask1" />
<sequenceFlow id="flow3" sourceRef="fork" targetRef="userTask2" />
<sequenceFlow id="flow4" sourceRef="fork" targetRef="task" />
<sequenceFlow id="flow5" sourceRef="userTask1" targetRef="join" />
<sequenceFlow id="flow6" sourceRef="userTask2" targetRef="join" />
<sequenceFlow id="flow7" sourceRef="task" targetRef="join" />
<sequenceFlow id="flowEnd" sourceRef="join" targetRef="end" />
</process>
</definitions>`;
const engine = new Engine({
name: 'Pending game',
source,
moddleOptions: {
camunda,
},
extensions: {
camunda: camundaExt,
},
});
const listener = new EventEmitter();
listener.on('wait', (elementApi) => {
if (elementApi.content.form) {
console.log(elementApi.content.form);
return elementApi.signal(
elementApi.content.form.fields.reduce((result, field) => {
if (field.label === 'Surname') result[field.id] = 'von Rosen';
if (field.label === 'Given name') result[field.id] = 'Sebastian';
return result;
}, {})
);
}
elementApi.signal();
});
engine.execute({
listener,
});
function camundaExt(activity) {
if (!activity.behaviour.extensionElements) return;
let form;
for (const extn of activity.behaviour.extensionElements.values) {
if (extn.$type === 'camunda:FormData') {
form = {
fields: extn.fields.map((f) => ({ ...f })),
};
}
}
activity.on('enter', () => {
activity.broker.publish('format', 'run.form', { form });
});
}
Add your own extension function that act on service task attributes.
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { Engine } from 'bpmn-engine';
const camunda = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json');
const source = `
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:camunda="http://camunda.org/schema/1.0/bpmn">
<process id="theProcess" isExecutable="true">
<serviceTask id="task1" camunda:expression="\${environment.services.serviceFn}" camunda:resultVariable="result" />
</process>
</definitions>`;
function ServiceExpression(activity) {
const { type: atype, behaviour, environment } = activity;
const expression = behaviour.expression;
const type = `${atype}:expression`;
return {
type,
expression,
execute,
};
function execute(executionMessage, callback) {
const serviceFn = environment.resolveExpression(expression, executionMessage);
serviceFn.call(activity, executionMessage, (err, result) => {
callback(err, result);
});
}
}
const engine = new Engine({
name: 'extend service task',
source,
moddleOptions: {
camunda,
},
services: {
serviceFn(scope, callback) {
callback(null, { data: 1 });
},
},
extensions: {
camundaServiceTask(activity) {
if (activity.behaviour.expression) {
activity.behaviour.Service = ServiceExpression;
}
if (activity.behaviour.resultVariable) {
activity.on('end', (api) => {
activity.environment.output[activity.behaviour.resultVariable] = api.content.output;
});
}
},
},
});
engine.execute((err, instance) => {
if (err) throw err;
console.log(instance.name, instance.environment.output);
});
Publish event when human involvement is required.
import { EventEmitter } from 'node:events';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { Engine } from 'bpmn-engine';
const camunda = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json');
const source = `
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:camunda="http://camunda.org/schema/1.0/bpmn">
<process id="theProcess" isExecutable="true">
<startEvent id="start" />
<sequenceFlow id="flow1" sourceRef="start" targetRef="task" />
<userTask id="task">
<humanPerformer>
<resourceAssignmentExpression>
<formalExpression>\${environment.services.getUser()}</formalExpression>
</resourceAssignmentExpression>
</humanPerformer>
<potentialOwner>
<resourceAssignmentExpression>
<formalExpression>user(pal), group(users)</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
</userTask>
<sequenceFlow id="flow2" sourceRef="task" targetRef="end" />
<endEvent id="end" />
</process>
</definitions>`;
function humanInvolvement(activity) {
if (!activity.behaviour.resources || !activity.behaviour.resources.length) return;
const humanPerformer = activity.behaviour.resources.find((resource) => resource.type === 'bpmn:HumanPerformer');
const potentialOwner = activity.behaviour.resources.find((resource) => resource.type === 'bpmn:PotentialOwner');
activity.on('enter', (api) => {
activity.broker.publish('format', 'run.call.humans', {
humanPerformer: api.resolveExpression(humanPerformer.expression),
potentialOwner: api.resolveExpression(potentialOwner.expression),
});
});
activity.on('wait', (api) => {
api.owner.broker.publish('event', 'activity.call', { ...api.content });
});
}
const listener = new EventEmitter();
const engine = new Engine({
name: 'call humans',
source,
moddleOptions: {
camunda,
},
services: {
getUser() {
return 'pal';
},
},
extensions: {
humanInvolvement,
},
});
listener.on('activity.call', (api) => {
console.log('Make call to', api.content.humanPerformer);
console.log('Owner:', api.content.potentialOwner);
api.signal();
});
engine.execute({ listener }, (err, instance) => {
if (err) throw err;
console.log(instance.name, 'completed');
});
Shake down the possible sequences an activity can have.
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { Engine } from 'bpmn-engine';
import BpmnModdle from 'bpmn-moddle';
import * as elements from 'bpmn-elements';
import Serializer, { TypeResolver } from 'moddle-context-serializer';
const camunda = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json');
const source = `
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<process id="Process_1" isExecutable="true">
<startEvent id="start">
<outgoing>toSayHiTask1</outgoing>
</startEvent>
<task id="sayHiTask" name="say hi">
<incoming>toSayHiTask1</incoming>
<outgoing>toEnd1</outgoing>
<outgoing>toEnd2</outgoing>
</task>
<sequenceFlow id="toSayHiTask1" sourceRef="start" targetRef="sayHiTask" />
<endEvent id="end1">
<incoming>toEnd1</incoming>
</endEvent>
<sequenceFlow id="toEnd1" sourceRef="sayHiTask" targetRef="end1" />
<endEvent id="end2">
<incoming>toEnd2</incoming>
</endEvent>
<sequenceFlow id="toEnd2" sourceRef="sayHiTask" targetRef="end2" />
</process>
</definitions>`;
(async function IIFE() {
const moddleContext = await new BpmnModdle({
camunda,
}).fromXML(source);
const sourceContext = Serializer(moddleContext, TypeResolver(elements));
const engine = new Engine({
sourceContext,
});
const [definition] = await engine.getDefinitions();
const shakenStarts = definition.shake();
console.log('first sequence', shakenStarts.start[0].sequence.reduce(printSequence, ''));
console.log('second sequence', shakenStarts.start[1].sequence.reduce(printSequence, ''));
function printSequence(res, s) {
if (!res) return s.id;
res += ' -> ' + s.id;
return res;
}
})();
One way to persist state is to subscribe to activity and engine events through a listener.
In this example the state of the execution is published on a message broker. Subscribers to the broker will hopefylly know how to persist the state.
import { randomUUID } from 'node:crypto';
import { createRequire } from 'node:module';
import { fileURLToPath } = from 'node:url';
import { Engine } from 'bpmn-engine';
import { EventEmitter } from 'ndoe:events';
import { publish } from './dbbroker.js';
import { getSourceSync, getAllowedServices, getExtensions } from './utils.js';
const camundaModdle = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json')
function ignite(executionId, options = {}) {
const { name, settings } = options;
const listener = new EventEmitter();
listener.on('activity.wait', (_, execution) => {
return publishEvent('bpmn.state.update', {state: execution.getState()});
});
listener.on('activity.end', (_, execution) => {
return publishEvent('bpmn.state.update', {state: execution.getState()});
});
listener.on('activity.timer', (api, execution) => {
return publishEvent('bpmn.state.expires', {
expires: new Date(api.content.startedAt + api.content.timeout),
state: execution.getState(),
});
});
listener.on('activity.timeout', (_, execution) => {
return publishEvent('bpmn.state.expired', {
expired: new Date(),
state: execution.getState(),
});
});
const engine = BpmnEngine({
moddleOptions: {
camunda,
},
...options,
settings: {
...settings,
executionId,
enableDummyService: false,
}
});
engine.once('end', () => {
publishEvent('bpmn.completed');
});
engine.once('error', (err) => {
publishEvent('bpmn.error', {message: err.message, error: err});
});
return { engine, listener };
function publishEvent(routingKey, message) {
publish('events', routingKey, {
name,
executionId,
...message,
});
}
}
const {engine} = ignite(randomUUID(), {
name: 'persisted engine #1',
source: getSourceSync('./mother-of-all.bpmn'),
services: getAllowedServices(),
extensions: getExtensions(),
});
engine.execute();