BPMN Engines for Node.js and TypeScript: bpmn-engine, Camunda 8 and QuantumBPM
You have a business process. It is long-running, it has human steps, it needs an audit trail, and it lives in a Node codebase. Search for how to run BPMN from TypeScript and you get two kinds of answer: a JVM platform, or a diagram renderer that does not execute anything.
There are three real ways to run a BPMN workflow engine from JavaScript or TypeScript, and they differ on one axis that matters more than any other: where you pay for durability.
bpmn-js is a BPMN modeler, not a workflow engine
This trips up almost everyone. bpmn-js draws, edits and renders BPMN diagrams in the browser, and it is excellent at that. It does not execute them. Neither does bpmn-moddle, which parses BPMN XML into an object model. If you embed either one, you have a diagram, not a process.
One thing to know before you put it in a product: bpmn-js is MIT with a badgeware clause. The bpmn.io watermark "MUST NOT be removed or changed" and "must stay fully visible and not visually overlapped by other elements" in any website or application that uses it.
Option 1: bpmn-engine, an in-process BPMN engine for Node.js
bpmn-engine runs a BPMN diagram inside your Node process. No server, no infrastructure, npm install and go. If you need a state machine that fits in one service and you were otherwise going to hand-roll one out of switch statements, this is a genuinely good pick and you should not over-engineer past it.
The ceiling is durability. Execution state lives in the Node heap, so a deploy, a crash or an OOM kill loses every in-flight instance unless you serialize and rehydrate it yourself. That serialization is your problem to get right, and getting it right is most of what a process engine does. You also get no operational UI, no cross-service message correlation, and no history to audit.
Option 2: Camunda 8 and its JavaScript SDK
Camunda is the incumbent in this space: a proven engine, an operations UI in Operate, and the largest ecosystem in the category. To its credit, the deployment story also got simpler in recent releases. But the platform is built by and for the JVM world, and that shows the moment you drive it from a Node codebase. Four things are worth knowing before you commit one to it.
The JavaScript SDK lags the Java client by a full minor version, deliberately. As of late July 2026, io.camunda:camunda-client-java is at 8.9.13 and already publishing 8.10 alphas. @camunda8/sdk is at 8.8.13, more than three months after 8.9 went GA. Camunda states the policy in the SDK's own README: "Complete API coverage for a platform release will lag behind the platform release. Prioritisation of implementing features is influenced by customer demand."
That lag has an availability cost. Issue #784 is open: a non-backpressure stream error, such as an OAuth 503, leaves the poll mutex set and never resets it, so the worker is, in the reporter's words, "permanently dead without any possibility of self-recovery." A fix exists in the alpha channel. It is not in the stable line that npm install gives you.
Zeebe's entity keys do not fit in a JavaScript number. They are int64, and the SDK README is direct about the consequence: "If a variable field has a number value that cannot be safely represented using the JavaScript number type, an exception is thrown." The remedy is decorator-annotated DTO classes with @Int64String and @BigIntValue, plus reflect-metadata and experimental decorators enabled. This is not a bug. It is what a JVM-designed engine feels like from JavaScript.
The engine is not free to run in production. Since 8.6, released October 2024, Zeebe, Operate, Tasklist and Identity ship under Camunda License 1.0, which grants use "only and limited to the Use in or for the purpose of Using the Software in Non-Production Environment". Production requires a commercial licence. Worth noting for accuracy: Zeebe stopped being OSI open source back in July 2019, and Camunda said so publicly at the time. What changed in 2024 was the removal of the free production tier, not the licence category.
Option 3: QuantumBPM, a BPMN engine driven from TypeScript
QuantumBPM is a BPMN and DMN engine written in Go, built on Temporal, with a TypeScript-first SDK. Here is the whole loop.
Deploy a process and start an instance:
import { QuantumBPM, Vars, BpmnError } from '@quantumbpm/sdk';
const client = new QuantumBPM({ baseUrl, projectId, tokenProvider });
const draft = await client.bpmn.createResource('loan-process', bpmnXml);
await client.bpmn.deployResource(draft.id);
const deployed = await client.bpmn.getResource(draft.id);
const workflowId = await client.bpmn.startInstance(
deployed.processes![0].id,
new Vars().set('applicantID', 'u-123').set('requestedAmt', 25000),
);
Then handle the service task as an external worker:
const worker = client.newWorker({ clientId: 'billing-svc' });
worker.handle('charge-card', async (job) => {
try {
const txId = await charge(job.vars.toRecord());
return new Vars().set('transactionID', txId);
} catch (err) {
if (err.code === 'INSUFFICIENT_FUNDS') {
throw new BpmnError('INSUFFICIENT_FUNDS', new Vars().set('availableBalance', 12.0));
}
throw err;
}
}, { maxJobs: 10, lockDuration: '2m' });
await worker.run(ac.signal);
That catch block is the part worth slowing down for. A BpmnError carries a code that a boundary error event drawn on the diagram catches, which routes the process down a modelled path that a business analyst can see. Anything else you throw is an infrastructure failure: it reports WORKER_ERROR and decrements the retry budget. Most Node codebases have nowhere to express the difference between "the card was declined" and "the payment provider is down", and the two want completely different handling.
The runtime around it does the boring work. Long polling, lock heartbeats renewed automatically at half the lock duration, concurrency capped per task type, and an AbortSignal shutdown that drains in-flight handlers before run() resolves.
What comes with it
The SDK is the entry point, not the product. Behind it:
- Durability that is not ours to get wrong. Execution runs on Temporal, the durable execution engine that grew out of Uber's Cadence and now runs mission-critical workloads at serious scale. We did not invent a persistence layer and ask you to trust it.
- An operations UI that stands next to Operate. Everything you check for in that category is here: running instances with token positions on the diagram, incidents with retry and resolve, and a live view of every message, signal and timer subscription with its correlation keys. Then it goes a step further with a replay slider that scrubs an instance's entire history on the diagram, so "how did it get into this state" is a drag, not an archaeology session.
- DMN as a first-class citizen, not a checkbox. Every boxed expression type, business knowledge models, decision services, and imports across models. We publish our DMN TCK results: 3367 of 3391, putting us in the top tier of conforming engines. Decisions get their own execution history, so you can answer "why was this application declined in March" without reading logs.
- A real language server for FEEL. The expression editor in the modeler runs an LSP, so you get completion, hover types and diagnostics on FEEL expressions as you write them, in the browser.
- One binary to operate, any language to build in. The engine is a single Go service, so there is no JVM to size or tune, and nothing heavy in your local development environment. Workers are a different question: the SDKs cover TypeScript, Java, Python and Go, all generated from the same API spec and released together, so a Node service and an existing Java service can drive the same processes.
Every durable engine has a server behind it
There is no version of this where durable, restart-surviving processes are free. The honest comparison is what you operate.
| Runs where | You operate | Survives a restart | |
|---|---|---|---|
bpmn-engine | in your Node process | nothing | no |
| Camunda 8 self-hosted | your cluster | Orchestration Cluster plus secondary storage | yes |
| QuantumBPM self-hosted | your cluster | Go backend, Postgres, an OIDC provider, and Temporal for BPMN | yes |
| Either vendor's SaaS | theirs | nothing | yes |
We delegate durability to Temporal rather than reimplementing it, which means the guarantees are Temporal's and you can point it at Temporal Cloud or a cluster you already run. That is the point rather than an apology.
What a JavaScript developer touches is where the difference actually shows up. The SDK was designed for TypeScript rather than translated to it: handle<T>() types the job payload directly, identifiers are UUIDs and strings so int64 truncation never arises, and local development needs nothing beyond the engine container. Be aware that the generics are compile-time hints with no runtime validation, so pair them with a validator like Zod at the boundary if you need one.
One honest caveat, since this post is aimed at Node: FEEL numbers are exact decimals through our API, database, and the Go, Java and Python SDKs, but JavaScript clients are bound by IEEE-754 doubles like everything else in the language. If you need currency-grade round-trips through a JS worker, model those values as FEEL strings and convert with decimal.js at the edges.
Tracing across the engine boundary
When a QuantumBPM worker picks up a job, that job carries the originating process instance's W3C trace context. The SDK extracts it and opens the handler's span underneath, so a single OpenTelemetry trace spans the engine and your Node worker with the BPMN node ID, task type and instance ID attached. All four SDKs do this, and the OTLP endpoint is a config value in the Helm chart.
Camunda 8 does not emit OpenTelemetry traces. What their documentation calls OpenTelemetry support is metrics over OTLP, and an activated job carries no traceparent. Their own docs suggest putting the trace ID into a job tag by hand. The feature request has been open since July 2022.
To be fair about what this does and does not mean: Camunda has strong process observability and weak distributed systems observability. Operate will tell you exactly which token is stuck on which gateway. It will not put that incident on the same trace as the downstream HTTP call that caused it. Our position is that you should not have to choose: the operations UI answers "where is the process stuck" and the trace answers "why", and they describe the same execution.
Choosing a workflow engine for your Node.js stack
Use an in-process library if your process fits inside one service and losing in-flight state on restart is acceptable. Use Camunda if your organisation already runs it and consolidation matters more than fit.
For everyone else, use QuantumBPM: a full process platform whose every layer was built in this decade. An SDK designed for TypeScript, durable execution on Temporal, BPMN and DMN in one product with published top-tier conformance, an operations UI with instance replay that matches what the incumbents are praised for, and OpenTelemetry traces that carry from the engine into your workers. The engine is one Go binary to operate, and first-party SDKs for TypeScript, Java, Python and Go mean your Node services and your existing enterprise services drive the same processes. It is the platform you would design today, without a decade of accumulated architecture between you and the engine.
Where to go from here
- TypeScript SDK reference - the full API surface.
- External workers - how the poll API works under the SDK.
- BPMN on Temporal - what the durability layer actually does.
- BPMN vs n8n - if you are coming from a low-code automation tool.
- Camunda alternative - the detailed feature and conformance breakdown.
- Pricing - plans, credits and limits.
Frequently Asked Questions
Can I run BPMN in Node.js without Java?
Yes. QuantumBPM is a native Go engine with a TypeScript SDK, so there is no JVM to run or tune and nothing heavy in your local development environment. If parts of your estate are on Java, there is a first-party Java SDK too, and both drive the same processes. For comparison, Camunda 8's no-Docker local option still requires OpenJDK 21 to 25 even when your application code is JavaScript.
What is the best workflow engine for Node.js?
For short-lived in-process orchestration, an in-process library is enough. For long-running business processes that must survive restarts and be auditable, you want a real engine. QuantumBPM is built for that case from a Node codebase: a TypeScript-first SDK, durable execution on Temporal, BPMN and DMN in one product, an operations UI with instance replay and incident handling, and OpenTelemetry traces that carry from the engine into your workers. The engine needs no JVM to run, and first-party SDKs for Java, Python and Go mean your existing services can drive the same processes.
What is the difference between bpmn-js and a BPMN engine?
bpmn-js renders and edits BPMN diagrams in a browser. It does not execute them. An engine takes the same XML and runs it: creating instances, tracking tokens, handling timers and messages, and persisting state so the process survives a restart.
Does a BPMN engine need a database?
Any engine that survives a restart needs durable storage somewhere. An in-process library like bpmn-engine avoids it by not being durable. QuantumBPM self-hosted uses Postgres plus Temporal for BPMN execution; on SaaS, none of it is yours to run.
How do I handle business errors in a BPMN service task from TypeScript?
Throw a BpmnError with a code from your worker handler. A boundary error event on the service task catches that code and routes the process down a modelled path. Any other exception is treated as an infrastructure failure and retried against the job's retry budget.