The @bpmnkit/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 "@bpmnkit/engine";
import { Bpmn } from "@bpmnkit/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
},
});
Time Travel in the Editor
The editor’s play mode (the process-runner plugin) records every engine event of a run, and a timeline scrubber above its tabs lets you move back through them:
- Drag the scrubber to any event. The Variables, FEEL and Errors tabs show the state as it was at that event, and the canvas redraws the tokens: elements holding a token then are marked active, elements already passed through are marked visited.
- Live returns to the latest event, with the tokens as it left them.
- Re-run with these variables starts a new run from the start event, with the variables as they were at the scrubbed event, which is the quickest way to try a different branch with the data a real run produced.
The state is projected from the recorded log, not by re-executing the process, so scrubbing is instant and never calls a worker twice. A run keeps up to 10,000 events.
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 "@bpmnkit/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: "..." }]