Skip to content

Simulation

The @bpmn-sdk/engine package is a zero-dependency BPMN simulation engine that runs in browsers and Node.js. It’s useful for:

  • Testing process logic before deploying
  • Prototyping without a Camunda cluster
  • CI pipelines — unit-test your process definitions
  • Demos — run live in a browser

Basic Usage

import { Engine } from "@bpmn-sdk/engine";
import { Bpmn } from "@bpmn-sdk/core";
// Build a process
const xml = Bpmn.export(
Bpmn.createProcess("order")
.startEvent("start")
.serviceTask("charge", { taskType: "payment" })
.serviceTask("ship", { taskType: "shipping" })
.endEvent("end")
.build()
);
// Create engine and deploy
const engine = new Engine();
await engine.deploy({ bpmn: xml });
// Register workers
engine.registerJobWorker("payment", async (job) => {
console.log("Charging card...", job.variables);
await job.complete({ transactionId: "txn-123" });
});
engine.registerJobWorker("shipping", async (job) => {
console.log("Shipping order...", job.variables);
await job.complete({ trackingNumber: "1Z999AA1" });
});
// Start an instance
const instance = engine.start("order", {
orderId: "ord-456",
amount: 99.99,
});

Instance State

The ProcessInstance object tracks the running state:

const instance = engine.start("order");
console.log(instance.state); // "running" | "completed" | "cancelled"
console.log(instance.activeElements); // Set<string> — currently active element IDs
// Subscribe to state changes
instance.onChange((newState) => {
console.log("State changed to:", newState);
});

Variables

Variables flow through the process via IO mappings. You can read the current snapshot:

const vars = instance.variables_snapshot;
console.log(vars.orderId); // input variable
console.log(vars.transactionId); // output from a task

Message Correlation

Deliver a message to a waiting intermediateCatchEvent:

// Process has a catch event waiting for "payment-confirmed"
await instance.deliverMessage("payment-confirmed", {
paymentMethod: "card",
confirmedAt: new Date().toISOString(),
});

Step-by-Step Execution

Use the beforeComplete hook to pause execution between elements — useful for debugging and building animated process runners:

const instance = engine.start("order", {}, {
beforeComplete: async (elementId) => {
console.log("About to complete:", elementId);
// Inspect state, update UI, etc.
// Resume by returning from this function
},
});

DMN Decision Evaluation

Deploy decision tables alongside the process:

await engine.deploy({
bpmn: xml,
decisions: [dmnXml], // DMN 1.3 XML strings
});

Testing with Vitest

Write deterministic unit tests for your process logic:

import { describe, it, expect } from "vitest";
import { Engine } from "@bpmn-sdk/engine";
import { buildOrderProcess } from "./processes.js";
describe("order process", () => {
it("completes when payment succeeds", async () => {
const engine = new Engine();
await engine.deploy({ bpmn: buildOrderProcess() });
let completed = false;
engine.registerJobWorker("payment", async (job) => {
await job.complete({ success: true });
});
engine.registerJobWorker("shipping", async (job) => {
await job.complete({ trackingNumber: "TRK-001" });
});
const instance = engine.start("order", { amount: 50 });
await new Promise<void>((resolve) => {
instance.onChange((state) => {
if (state === "completed") { completed = true; resolve(); }
});
});
expect(completed).toBe(true);
expect(instance.variables_snapshot.trackingNumber).toBe("TRK-001");
});
});

Multiple Deployments

The engine supports multiple deployed processes. Use engine.getDeployedProcesses() to list them:

await engine.deploy({ bpmn: processAXml });
await engine.deploy({ bpmn: processBXml });
const processes = engine.getDeployedProcesses();
// [{ id: "process-a", name: "..." }, { id: "process-b", name: "..." }]