A BPMN 2.0 workflow engine written in Rust — token-based execution, NATS persistence, REST API and desktop UI.
- Overview
- Crates (Modules)
- Workspace Scope
- Supported BPMN Elements
- Architecture
- Quick Start
- REST API
- External Task Client (TypeScript)
- Desktop Application (UI)
- Docker Compose
- Test Metrics
- Roadmap
- License
bpmninja is a BPMN 2.0 engine with the following core features:
- Token-based execution — each path is tracked as an independent token
- 37 BPMN element variants — start/end events, user/service/script tasks, gateways (XOR, AND, OR, complex, event-based), timers, messages, signals, conditional events, boundary events (timer, message, signal, conditional, error, escalation, compensation), link events, call activities, sub-processes
- Full ISO 8601 timers — Duration (
PT30S), AbsoluteDate (2026-04-06T14:30:00Z), Cron (0 9 * * MON-FRI), Repeating Interval (R3/PT10M) - Lock-free concurrency — multi-threaded scaling via
DashMapwait-state queues with atomicremove_iffor race-condition-free task operations - NATS JetStream persistence — KV stores for instances, object store for files, event streaming for history
- Fault-tolerant retry queue — two-stage retry (inline + bounded background worker) for NATS outages; drops + metrics when the queue is full
- Fail-closed durability —
REQUIRE_NATSdefaults to true (also docker-compose) and refuses silent in-memory fallback;/api/readymirrors persistence health - Optional deploy-time model checking — Cargo feature
model-check(off by default) runs rust_bpmn_analyzer (MIT, pinned git revision) for deadlocks, livelocks, unsafe parallelism and dead activities. RuntimeMODEL_CHECKdefaults tooff;warnreturns findings with the deploy,enforcerejects confirmed violations. See Environment variables - Upload limits — multipart instance files capped via
MAX_UPLOAD_BYTES(default 5 MiB → HTTP 413) - Automatic timer scheduler — background task sleeps until the next timer is due (
TIMER_INTERVAL_MSis the maximum idle wait) - Camunda-compatible service tasks — fetch-and-lock pattern with long polling
- Rhai script engine — sandboxed execution listeners (ops / memory budget / timeout)
- Call Activities — call another deployed process definition (
calledElement); variables are propagated in both directions; BPMN error boundaries supported - Suspend / resume — pause and continue instances (timers and tasks blocked)
- Incident management — handle failed service tasks with retry/resolve directly from the UI
- Historical instance archival — completed instances are automatically archived to a separate store with search by definition, business key, date range, status and pagination
- Push-based UI updates — engine fires SSE events (
GET /api/events) on every state change; Tauri desktop app subscribes via a background task and emits Tauri events to React components, replacing polling - Persistent log buffer — rolling 5000-entry log stream captured by a custom
tracinglayer; queryable viaGET /api/logswith level and text filters; automatically persisted to NATS JetStream (ENGINE_LOGSstream, 50 000 entries) when NATS is available, with a local JSON-Lines file as fallback (engine_logs.jsonl, gitignored; setLOG_FILE=offto disable) - Prometheus metrics — all key engine metrics exported at
/metricsviametrics-exporter-prometheus - Multi-arch Docker images — CI builds
linux/amd64+linux/arm64images via GitHub Actions with layer cache - Desktop UI — Tauri app with bpmn-js modeler, live instance/definition tracking with push updates, log stream viewer, history page for archived instances, storage mode badge (NATS / In-Memory), and DataViewer with automatic MIME/base64 detection (cross-platform releases via GitHub Actions CI)
| Crate | Purpose |
|---|---|
engine-core |
Core library — state machine, token registry, gateway routing, condition evaluator, script engine |
bpmn-parser |
Parses BPMN 2.0 XML (quick-xml + serde) into internal ProcessDefinition structs |
persistence-nats |
NATS JetStream-based WorkflowPersistence implementation (KV, Object Store, Streams) |
persistence-memory |
In-Memory WorkflowPersistence implementation for testing and development |
engine-server |
Axum HTTP REST API with type-safe error handling (AppError → HTTP status codes) |
desktop-tauri |
Tauri desktop app (React + bpmn-js) with modeler, instances dashboard, event history and historical instance search |
agent-orchestrator |
Sample worker for external service task processing |
bpmn-ninja-external-task-client |
TypeScript/Node worker client (ESM) — long polling, retry, lock extension, graceful shutdown |
The Rust root workspace (Cargo.toml) currently includes:
engine-corebpmn-parserpersistence-natspersistence-memoryengine-serveragent-orchestrator
Separately managed modules:
desktop-tauri/src-tauri(own Cargo project, built viadesktop-tauri)fuzz(separatecargo-fuzzworkspace)
For full local verification, run the root checks plus module-specific checks from desktop-tauri and fuzz.
| Feature | Description |
|---|---|
| Conditional Flows | Edges with conditions (amount > 100, status == 'approved', ${amount > 100 && approved}). Supports &&/and, ||/or, !/not, ==, !=, >, <, >=, <=, arithmetic + - * / %, parentheses, numbers, single- and double-quoted strings with escapes, true/false/null, and dotted/array paths (order.amount, items[0].id). |
| Execution Listeners | Start/end scripts on nodes (Rhai). Can read and mutate variables. |
| Event Sub-Processes | subProcess triggeredByEvent="true" directly under a process. Message, timer, signal, error (interrupting only) and escalation starts. The scope subscribes when the instance starts and drops subscriptions when it leaves. Interrupting starts cancel the scope; non-interrupting starts run in parallel. |
| File Variables | Upload/download of files as process variables via NATS Object Store. |
| Message Correlation | Matching via messageName + optional businessKey. |
| Signal Correlation | Broadcast via POST /api/signal; wakes every waiting SignalCatchEvent and starts instances with a SignalStartEvent. Broadcast is process-local (single engine node). |
| BPMN Error Handling | ServiceTasks report errors via bpmnError. Routing to the matching BoundaryErrorEvent. |
| Detailed History | Gap-free event log with diffs, snapshots and actors (User, Engine, Timer, ServiceWorker). |
| Instance Archival | Completed instances are archived to a dedicated store. Searchable by definition key, business key, date range, status with pagination. |
| Persistent Wait States | Timers, messages, signals, conditional catches and user/service tasks survive server restarts via NATS KV. |
| Structured JSON Logging | Configurable via tracing-subscriber with the JSON feature and RUST_LOG filter. |
Conditional flow details. A complete ${…} or #{…} wrapper is stripped before evaluation; a wrapper in the middle of an expression or a missing closing brace is a syntax error. &&/|| short-circuit, missing variables compare as false, and a missing value is not the same as null. Invalid conditionExpression and complexGateway activationCondition values are rejected at deploy time with InvalidDefinition (source/target node plus character position), so a broken model no longer falls through to the default flow silently.
For performance and architectural reasons (keep it simple), bpmninja deviates from the strict BPMN 2.0 standard in a few points:
- Service Tasks (External Task Pattern): Instead of synchronously executing code inside the engine,
Service Taskspause execution. They place the task asynchronously into a fetch-and-lock queue (similar to Camunda), from where external workers fetch tasks (topic-based) and report completion via the API. A topic can instead be claimed by an in-processTaskHandlerregistered onWorkflowEngine; that topic is then hidden from external workers and executed by the engine's dispatcher with the same lock, retry and BPMN-error semantics. - Embedded Sub-Processes (Flattening): Embedded sub-processes are resolved directly at parse time and inlined deep into the main graph (flattening). At runtime there are no complex nested instance structures, only direct node sequences. Returns from the sub-process are handled via simulated
SubProcessEndEvents in the same variable scope. - Script Tasks: Script evaluation is not done via JavaScript or Groovy, but natively in Rust via the Rhai engine.
- Multi-Instance (Service Tasks only): Parallel and sequential
multiInstanceLoopCharacteristicsare supported onserviceTaskviacollection+elementVariableorloopCardinality. In both modesloopCounterstarts at 1 and the instance advances once after all items complete. Parallel MI creates all pending tasks up front; sequential MI (isSequential="true") keeps exactly one task open and creates the next one after each completion, preserving collection order and theelementVariablevalue per iteration. Multi-instance on any other task type is rejected at deploy time withInvalidDefinition. - Inclusive Gateway join: Split follows BPMN OR (every outgoing flow whose condition is true). Join waits for taken paths:
expected_countis the matching split's taken branch count (viafind_downstream_join). Unstructured joins without a matching split fall back to AND-styleincoming_flow_count. - Camunda assignee/topic attributes: User tasks read
data-assigneefirst, thencamunda:assignee(quick-xml strips the prefix to@assignee; fallback"unassigned"). Service tasks readdata-topic, thendata-handler, thencamunda:topic(@topic), then the node id. - Task type mapping:
receiveTaskwith amessageRefbecomes a message catch (instantiate="true"is rejected).manualTaskbecomes a user task.businessRuleTaskwith a topic becomes an external service task; without a topic it is rejected (no DMN). A generictaskis rejected — convert it to a concrete type. An emptyscriptTaskis rejected. An intermediate catch without a supported trigger is rejected. - One process per file: Only the first
<process isExecutable="true">is deployed; if none is marked executable, the first<process>is used. Further processes in the same file are ignored.
The engine focuses on a practical and performant core feature set. Elements with execution semantics the engine does not implement are rejected at deploy with InvalidDefinition. The error names every offending element (type, id, source line), not only the first. Visual and data elements are still accepted. The full matrix is docs/knowledge-base/modules/bpmn-parser/element-matrix.md.
- Event scope: Signal events are supported for start, intermediate catch/throw, end and boundary (interrupting/non-interrupting); the broadcast is process-local, so multiple engine nodes do not fan out. Conditional events are supported for intermediate catch and boundary only. A conditional start event is rejected at deploy time with
InvalidDefinitionbecause the engine has no global variable scope to trigger it. An event-based gateway cannot target a conditional catch. Link events are supported: a link throw jumps to the target of its link catch within the same scope; cross-scope links are rejected. Message end events, multiple event definitions on one event, and multiple/parallel-multiple events are rejected. - Cancel events & transactions:
cancelEventDefinitionis rejected at deploy time withInvalidDefinition— cancel events require a BPMN transaction, and transaction sub-processes are not supported. A<transaction>element is rejected with the same reasoning. - Extended sub-processes:
adHocSubProcessandstandardLoopCharacteristicsare rejected. An event sub-process is supported only as a direct child of the process (message, timer, signal, interrupting error, escalation). Nested event sub-processes, conditional starts and sequence flows on the container are rejected. An instantiating receive task and an instantiating or parallel event-based gateway are rejected. A trigger on the start event of an embedded sub-process is rejected, because that start is a pass-through. - Data Objects / Data Stores: Visual data objects and associations (
Data Input/Output Association) are ignored. Data exchange is done exclusively via the JSON variable state (HashMap<String, serde_json::Value>).
Detailed documentation with 8 Mermaid diagrams: docs/architecture.md
flowchart TD
classDef core fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a;
classDef server fill:#bae6fd,stroke:#0284c7,stroke-width:2px,color:#0c4a6e;
classDef persistence fill:#bbf7d0,stroke:#16a34a,stroke-width:2px,color:#14532d;
classDef desktop fill:#fef08a,stroke:#ca8a04,stroke-width:2px,color:#713f12;
classDef agent fill:#fbcfe8,stroke:#db2777,stroke-width:2px,color:#831843;
classDef storage fill:#f0fdf4,stroke:#16a34a,stroke-width:1px,color:#14532d;
subgraph "Clients"
UI["desktop-tauri<br>(Tauri + React + bpmn-js)"]:::desktop
Agent["agent-orchestrator<br>(External Workers)"]:::agent
ExtMsg["External Systems<br>(Messages / Timers)"]:::agent
end
subgraph "Server Layer"
Axum["engine-server<br>(Axum REST API)"]:::server
end
subgraph "Core Engine"
Parser["bpmn-parser<br>(XML → ProcessDefinition)"]:::core
Engine["engine-core<br>(State Machine / Tokens)"]:::core
Trait["WorkflowPersistence<br>(Trait)"]:::core
end
subgraph "Storage"
NatsImpl["persistence-nats"]:::persistence
Nats[("NATS JetStream<br>KV + Object Store")]:::storage
end
UI -- "HTTP REST" --> Axum
Agent -- "fetchAndLock / complete" --> Axum
ExtMsg -- "POST /api/message" --> Axum
Axum --> Parser
Axum --> Engine
Engine -. "uses" .-> Trait
Trait -. "implemented by" .-> NatsImpl
NatsImpl --> Nats
Option A: Devbox (recommended)
# Automatically installs Rust, Node.js and NATS
devbox shellOption B: Manual
- Rust (via
rustup) - Node.js ≥ 18
- Docker & Docker Compose
| Action | Devbox | Shell |
|---|---|---|
| Build | devbox run build |
cargo build --workspace |
| Test | devbox run test |
cargo test --workspace |
| Coverage | devbox run coverage |
cargo llvm-cov --workspace --all-targets --summary-only |
| Lint | devbox run lint |
cargo clippy --workspace -- -D warnings |
| Format | devbox run fmt |
cargo fmt --all --check |
Nx 24.0.1 orchestriert lokal die Projekte rust, desktop, client und api-spec; Cargo bleibt die Build-Engine. In der CI plant Nx nur: nrwl/nx-set-shas plus nx show projects --affected im Job plan ermitteln die betroffenen Projekte, die Build-Jobs rufen anschließend cargo und npm direkt auf. Nx-Cache und Targets sind lokal, in der CI gibt es keinen Nx-Cache. Die Rust-Inputs umfassen zusätzlich deny.toml, .github/workflows/**, package.json, package-lock.json und nx.json, damit Infrastrukturänderungen die Rust-Jobs auslösen. Das test-Target von client nutzt rustSources als Input, weil sein Integrationstest das engine-server-Binary startet. Das Rust-Projekt rust hat cache: false, seine Targets werden also nicht gecacht. Aufruf über npm exec -- nx … (alternativ ./node_modules/.bin/nx); der Aufruf über npx scheitert in der Devbox mit Missing script: nx. Verfügbare Projekte zeigt npm exec -- nx show projects.
npm exec -- nx run-many -t lint test build # alle Projekte, parallel
npm exec -- nx affected -t lint test build # nur betroffene Projekte (Basis: main)
npm exec -- nx run rust:test # einzelnes Target (cargo nextest run --workspace)
npm exec -- nx graph # Projekt-/Task-Graph im Browser# 1. Start NATS
docker compose up -d nats
# 2. Start the engine server
cargo run -p engine-serverThe server runs at http://localhost:8081.
| Variable | Default | Description |
|---|---|---|
NATS_URL |
nats://localhost:4222 |
NATS server URL |
REQUIRE_NATS |
true |
Fail-closed: refuse to start without NATS (no silent in-memory fallback). Set false for ephemeral local runs; /api/ready requires persistence when enabled |
BIND_ADDR |
0.0.0.0 |
Listen address (alias BIND_ADDRESS) |
PORT |
8081 |
HTTP server port |
RUST_LOG |
info |
Log level filter (e.g. debug, warn, engine_server=trace) |
MODEL_CHECK |
off |
Deploy-time check: off, warn (findings in the response and the log), enforce (reject confirmed violations). Requires --features model-check |
MODEL_CHECK_TIMEOUT_MS |
2000 |
How long a deploy waits for that check |
Vollständige Liste (Auth, Upload-/XML-Limits, Logging, Retry-Queue, Rhai-Sandbox, NATS-Retention, Model-Checking, Client-Variablen): docs/env.md.
Model checking is not in the default binary or the published image. Build it explicitly:
cargo run -p engine-server --features model-checkwarn still deploys when the analyzer finds a problem, times out, or panics; the response field model_check explains which. enforce returns HTTP 400 with error and model_check for a confirmed violation. A timeout, a panic, or a second deploy while one analysis is running does not fail the deploy in either mode (the single in-flight analysis runs on a blocking thread and is capped by MODEL_CHECK_TIMEOUT_MS). Setting enforce on a binary built without the feature rejects every deploy, so the switch cannot fail open.
Complete OpenAPI 3.0 specification: docs/openapi.yaml | 🌐 API Portal (Redoc) (requires an active GitHub Pages deploy via /docs)
| Method | Path | Description |
|---|---|---|
POST |
/api/deploy |
Deploy a BPMN definition (max 10 MB). With MODEL_CHECK=warn or enforce the response includes model_check |
GET |
/api/definitions |
List all definitions |
GET |
/api/definitions/:id/xml |
Retrieve the BPMN XML of a definition |
DELETE |
/api/definitions/:id |
Delete a definition (?cascade=true to also delete instances) |
DELETE |
/api/definitions/bpmn/:bpmn_id |
Delete all versions of a BPMN ID |
| Method | Path | Description |
|---|---|---|
POST |
/api/start |
Start an instance (by definition_key) |
POST |
/api/start/latest |
Start an instance of the latest version (by bpmn_process_id) |
POST |
/api/start/timer |
Start a timer start event instance (repeating intervals) |
GET |
/api/instances |
List all instances |
GET |
/api/instances/:id |
Retrieve instance details |
DELETE |
/api/instances/:id |
Delete an instance |
POST |
/api/instances/:id/suspend |
Suspend an instance (timers/tasks paused) |
POST |
/api/instances/:id/resume |
Resume a suspended instance |
PUT |
/api/instances/:id/variables |
Update variables |
POST |
/api/instances/:id/move-token |
Move a token to a different node (modify process instance) |
POST |
/api/instances/:id/migrate |
Migrate an instance to a different definition version (with optional node mapping) |
| Method | Path | Description |
|---|---|---|
GET |
/api/tasks |
List all pending user tasks |
POST |
/api/complete/:id |
Complete a user task |
| Method | Path | Description |
|---|---|---|
GET |
/api/service-tasks |
List all service tasks |
POST |
/api/service-task/fetchAndLock |
Fetch and lock tasks (long polling) |
POST |
/api/service-task/:id/complete |
Complete a task successfully |
POST |
/api/service-task/:id/failure |
Mark a task as failed |
POST |
/api/service-task/:id/retry |
Retry an incident (reset retries) |
POST |
/api/service-task/:id/resolve |
Resolve an incident manually (complete the task without a worker) |
POST |
/api/service-task/:id/extendLock |
Extend the lock |
POST |
/api/service-task/:id/bpmnError |
Report a BPMN error |
| Method | Path | Description |
|---|---|---|
POST |
/api/instances/:id/files/:var |
Upload a file (multipart) |
GET |
/api/instances/:id/files/:var |
Download a file |
DELETE |
/api/instances/:id/files/:var |
Delete a file variable |
| Method | Path | Description |
|---|---|---|
POST |
/api/message |
Correlate a message |
GET |
/api/messages |
List all pending message subscriptions |
POST |
/api/signal |
Broadcast a signal ({ signalName, variables? }) → affected instance IDs |
GET |
/api/signals |
List all pending signal catches |
GET |
/api/timers |
List all active timers |
POST |
/api/timers/process |
Manually process expired timers |
| Method | Path | Description |
|---|---|---|
GET |
/api/instances/:id/history |
Event history of an instance (with filter query params) |
GET |
/api/instances/:id/history/:eid |
A single history event |
GET |
/api/history/instances |
List archived (completed) instances with filters (definition_key, business_key, from, to, state, limit, offset) |
GET |
/api/history/instances/:id |
Load a single archived or active instance by ID |
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Liveness check → always 200 OK while the process is up |
GET |
/api/ready |
Readiness: 503 if required persistence is missing (REQUIRE_NATS) or NATS is disconnected |
GET |
/api/info |
Backend info (type, NATS URL, status) |
GET |
/api/monitoring |
Engine statistics (instances, tasks, storage, errors) |
GET |
/api/monitoring/buckets/:bucket/entries |
List KV bucket entries |
GET |
/api/monitoring/buckets/:bucket/entries/:key |
Load a single KV entry |
GET |
/metrics |
Prometheus metrics (counters, gauges for instances/tasks/errors) |
| Method | Path | Description |
|---|---|---|
GET |
/api/logs |
Query the in-memory rolling log buffer (?level=info&search=…&limit=500) |
GET |
/api/events |
Server-Sent Events stream — fires instance_changed, task_changed, definition_changed on every state mutation |
All errors follow a uniform JSON format:
{ "error": "Human-readable error message" }| HTTP code | Meaning |
|---|---|
400 |
Bad request (bad XML, invalid UUID, missing fields) |
404 |
Resource not found (definition, instance, task, node) |
409 |
Conflict (task not pending, already locked, already completed) |
500 |
Internal server error |
The package @bpmninja/external-task-client is a production-ready worker client for the BPMNinja service task API. It is modelled on the Camunda External Task Client, but talks directly to the BPMNinja REST endpoints under /api/service-task/*.
- Long polling — efficient fetch-and-lock with configurable
asyncResponseTimeout - Multi-topic subscriptions — multiple topics in parallel with individual handlers
- Global retry with exponential backoff — automatic retry on handler errors (
1s → 2s → 4s → …, capped at 30s) - Automatic lock extension — prevents lock expiration for long-running tasks
- Incident creation — after retries are exhausted, an incident is created on the process (
failurewithretries: 0) - BPMN error throwing — triggers boundary error events directly from the worker
- Graceful shutdown — waits for in-flight handlers, cancels fetches via
AbortController - Strict TypeScript —
strict: true, ESM, nativefetch()(Node ≥ 18), no HTTP dependencies - Pino logging — structured logging, disable via
logger: false
import { ExternalTaskClient } from "@bpmninja/external-task-client";
const client = new ExternalTaskClient({
baseUrl: "http://localhost:8081",
workerId: "demo-worker-01",
maxRetries: 3,
autoExtendLock: true,
});
client.subscribe("send-email", async (task, service) => {
const { recipient, subject } = task.variables_snapshot as {
recipient: string;
subject: string;
};
await sendEmail(recipient, subject);
await service.complete({
emailSent: true,
sentAt: new Date().toISOString(),
});
});
// Throw a BPMN error from the worker (Boundary Error Event)
client.subscribe("validate-order", async (task, service) => {
const { amount } = task.variables_snapshot as { amount: number };
if (amount > 10_000) {
await service.bpmnError("ORDER_LIMIT_EXCEEDED");
return;
}
await service.complete({ orderValid: true });
});
client.start();
// Graceful shutdown
process.on("SIGINT", async () => {
await client.stop();
process.exit(0);
});| Method | Description |
|---|---|
complete(variables?) |
Complete the task successfully, optionally with output variables |
failure(msg, details?, retries?) |
Report a failure; retries: 0 creates an incident |
extendLock(ms) |
Extend the lock (ms, internally converted to seconds) |
bpmnError(errorCode) |
Trigger a BPMN error event on the attached boundary |
cd bpmn-ninja-external-task-client
npm install
npm run lint # tsc --noEmit
npm run test # vitest run (85 tests)
npm run example # tsx example/simple-worker.tsThe full example (send-email, validate-order, flaky-task, shutdown) is in bpmn-ninja-external-task-client/example/simple-worker.ts. A complete API reference is available in the package README.
The Tauri app connects to the engine-server via HTTP and receives real-time push updates via Server-Sent Events — no polling required for core state views.
Key UI pages:
- Modeler — bpmn-js canvas with properties panel (execution listeners, default flow conditions, called element configuration)
- Instances — live list of running instances with token highlighting, variable editor, history timeline and token move
- Process Definitions — BPMN diagram with live token overlay, incident list and service task monitoring
- Pending Tasks — user tasks and service task incidents with completion dialogs
- Overview — pending timers, messages and service jobs
- History — searchable archive of completed instances (by definition, business key, date range, status)
- Monitoring — NATS storage info, KV bucket browser, engine statistics, live log stream (filterable by level and text) and storage mode badge (NATS / In-Memory) in both the sidebar and the monitoring page header
Prerequisite: a running backend (NATS + engine server). By default, the app expects the backend at
http://localhost:8081(configurable viaENGINE_API_URL).
Save the following docker-compose.yml locally on your machine and start it via docker compose up -d. The ready-to-use backend image is pulled automatically from the GitHub Container Registry:
services:
nats:
image: nats:alpine
command: ["--js", "--sd", "/data"]
ports:
- "4222:4222"
- "8222:8222"
volumes:
- nats-data:/data
engine-server:
image: ghcr.io/maatini/bpmninja/engine-server:latest
ports:
- "8081:8081"
environment:
- PORT=8081
- NATS_URL=nats://nats:4222
- REQUIRE_NATS=true
depends_on:
- nats
volumes:
nats-data:The ready-to-use apps are available on the GitHub Releases page.
- macOS (.dmg): Open the file and drag the icon into your
Applicationsfolder. Note: since open-source apps are usually not signed, macOS may report that the app is "damaged" and should be moved to the Trash when you try to open it. This is a known Gatekeeper protection behaviour. Runxattr -cr /Applications/bpmninja-desktop.appin the terminal to remove the quarantine flag. After that, the app can be opened normally. - Windows (.exe / .msi): Run the installer by double-clicking it. If a Microsoft SmartScreen warning appears, click "More info" and then "Run anyway".
- Linux (.AppImage / .deb): Install the
.debpackage viasudo dpkg -i package.deb. If you use the.AppImage, you may need to make it executable first withchmod +x app.AppImage.
If you want to run the UI directly from the source repository:
# Devbox
devbox run ui:dev
# Or manually
cd desktop-tauri && npm install && npm run tauri devStarts NATS + engine server as containers:
# Devbox
devbox run engine:docker
# Or manually
docker compose up --buildServices reachable at localhost:8081 (API) and localhost:4222 (NATS).
The compose file includes a cobra-nats service (natsio/nats-box) which provides the NATS CLI inside the Docker network for ad-hoc inspection of streams, KV buckets and subjects without a local installation.
Snapshot from 2026-10-24. The authoritative source is CI (
.github/workflows/ci.yml+.github/workflows/fuzzing.yml).
- Letztes Update (UTC):
2026-10-24T05:27:07.168644+00:00 - Mutation Score: 68.9% (caught: 406, missed: 182, timeout: 1)
- Fuzzing: 9/9 targets (ok)
| Package | Unit | E2E | Total |
|---|---|---|---|
| engine-core | 319 | 5 | 324 |
| bpmn-parser | 91 | — | 91 |
| persistence-nats | 20 | — | 20 |
| persistence-memory | 2 | — | 2 |
| engine-server | 50 | 100 | 150 |
| desktop-tauri | — | 52 | 52 |
| bpmn-ninja-external-task-client | 85 | — | 85 |
| Total | 567 | 157 | 724 |
Authoritative (2026-10-29 for engine-server test counts; 2026-10-24 for the other packages and for coverage below):
cargo test --workspace -- --list(Rust, devbox toolchain 1.99.0),node ./node_modules/@playwright/test/cli.js test --list(desktop-tauri),node ./node_modules/vitest/vitest.mjs list(external task client). Counts are listed test cases. The threee2e_model_checkcases and the tenmodel_checkunit tests are the default feature set;--features model-checkadds the analyzer cases.
Measured with cargo llvm-cov --workspace --all-targets (LLVM source-based coverage, test code excluded).
CI job Rust line coverage runs the same measurement, fails below 75 % lines and uploads lcov.info.
| Crate | Lines | Line coverage |
|---|---|---|
| engine-core | 9,868 | 86.7 % |
| bpmn-parser | 1,336 | 74.8 % |
| engine-server | 2,728 | 78.6 % |
| persistence-nats | 805 | 54.0 % |
| persistence-memory | 101 | 100.0 % |
| agent-orchestrator | 115 | 0.0 % (demo binary, no tests) |
| Total | 14,953 | 81.8 % |
Overall: 82.2 % regions, 75.9 % functions, 81.8 % lines.
| Module | Tests | Coverage |
|---|---|---|
engine::tests::instance_admin |
33 | Audit log, restore from persistence, definition versioning/cascade delete, large file variables, Send/Sync |
engine::tests::timers_messages |
25 | Timers, boundary timer/message, message start/catch, event-based gateway races, failed-timer incidents |
engine::tests::stress_tests |
24 | Throughput, gateway correctness, crash recovery, concurrency, race conditions, memory, infinite loops |
engine::tests::call_errors |
18 | Call activity lifecycle, variable/error propagation, error boundary events, compensation |
engine::tests::gateways |
20 | XOR/OR/AND forks and joins, defaults, downstream join detection, compound conditions |
engine::tests::start_tasks |
12 | User/service tasks, conditional routing, lock/retry, BPMN error handling |
engine::tests::topic_index |
7 | Per-topic lock durations, orphaned entries, restore, incident filtering |
engine::retry_queue::tests |
7 | Display, shutdown/drain budget, skip missing entities, full-queue drop, persistence incidents |
engine::tests::migration |
6 | Instance migration: same node ids, node mapping, rejected for completed/suspended instances |
engine::tests::multi_instance |
10 | Parallel and sequential collection/loop-cardinality multi-instance, snapshots, error cases |
engine::tests::scripting |
6 | Rhai listeners, memory budget, safe failure handling, start/end distinction |
engine::tests::service_incidents |
4 | Incidents on failed service tasks, fetch_and_lock skips incidents, resolve |
engine::tests::sub_process |
4 | Embedded sub-process start-event pass-through, nested sub-process, default-flow completion |
engine::tests::signals |
9 | Signal broadcast to multiple waiters, signal start, throw/end, boundary interrupting/non-interrupting, event-based gateway, restore |
engine::tests::conditional_events |
10 | Conditional catch/boundary on variable changes, true-on-entry, direct variable updates, no busy loop, restore |
engine::tests::link_events |
2 | Link throw jumps to catch target, scope-local resolution |
engine::boundary::tests |
4 | No-events, timer/message/signal/conditional boundary setup |
engine::tests::call_activity_parallel |
3 | Parallel call activities, parent completion, missing target |
engine::tests::inclusive_join |
3 | Inclusive join with partial branches, nested joins |
engine::tests::instance_pagination |
3 | Offset/limit paging, sort by started_at desc, unpaged listing |
engine::tests::timer_due_index |
3 | Due index on delete/restore, future timers not due |
engine::tests::recurring_and_event_timer |
2 | R3 repeating interval, two event timers with equal duration |
engine::tests::xor_merge |
2 | Exclusive merge with unconditional/default flows |
engine::tests::boundary_restore |
1 | Boundary timer persists and restores |
history::tests |
24 | Diff calculation, human-readable text, truncation, snapshots, UTF-8 safety |
domain::tests |
24 | ProcessDefinition builder, gateway validation, signal/conditional element rules, token serialization, timer definitions |
domain::timer::tests |
15 | Remaining time per timer type, recurring detection, infinite limits |
runtime::instance::tests |
14 | Audit log limits, file variables, file references, incident caps |
condition::tests |
22 | Condition parser/evaluator: precedence, short-circuit, wrappers, escapes, paths, validity |
adapter::in_memory::tests |
2 | Storage info, history query filters |
| Integration tests | 5 | BPMN compliance (3), complex gateways (2) |
| Area | Tests | Coverage |
|---|---|---|
| Basic parsing | 10 | Simple BPMN, conditional flows, condition validation (flow and join condition), interleaved bpmn-js output, execution listeners, executable-process selection, Camunda attribute precedence |
| Gateways | 5 | Parallel, exclusive with default, inclusive, event-based, complex |
| Events | 24 | MessageStart, MessageCatch, ErrorEnd, TimerCatch, IntermediateMessageThrow; signal start/catch/throw/end plus missing/unknown signalRef rejects; link throw→catch and link-scope rejects; conditional intermediate catch and conditional rejects (missing condition, invalid condition, conditional start, event-based gateway) |
| Boundary events | 4 | BoundaryTimer, BoundaryError, BoundarySignal, BoundaryConditional |
| ISO 8601 timers | 8 | TimeDate, CronCycle, RepeatingInterval, RepeatingInterval-Compact, formal expression, timer start, duration/definition rejects |
| Task types | 9 | ScriptTask, SendTask (incl. messageRef), ReceiveTask, BusinessRuleTask, manual task, rejects for empty/generic tasks and business rule without topic |
| Sub-processes | 12 | EventSubProcess, RegularSubProcess, start-event pass-through/implicit split, rejects for missing/multiple start events, flattening keeps send/inclusive/signal events and catch, sub-process default-flow resolution and unknown-default rejects, conditional catch in sub-process |
| Advanced | 8 | TerminateEndEvent, CompensationEvents, EscalationEvents, catch without trigger rejected; cancel end/boundary and <transaction> rejected; BPMN without cancel events stays valid |
| Multi-Instance | 9 | Parallel and sequential collection/loop cardinality, rejects for sequential on user tasks, missing/both sources, invalid cardinality |
| Call Activity | 1 | calledElement attribute parsing and mapping to CallActivity element |
| Namespace handling | 1 | Camunda namespace prefixes on execution listeners (camunda:executionListener) |
| Test file | Tests | Coverage |
|---|---|---|
e2e_auth_cors.rs |
19 | Admin/worker API keys, fail-closed on non-loopback bind, escape hatch, CORS allowlist and preflight, auth metrics |
e2e_call_activity.rs |
5 | Call activity spawn, variable propagation, error boundaries, parent/child lifecycle |
e2e_deploy.rs |
5 | Deploy, start, invalid XML → 400, XML retrieval and 404 |
e2e_deploy_persist.rs |
2 | Deploy with/without persistence, save failure → 5xx |
e2e_file_variables.rs |
3 | File upload, task completion with files, multi-file + delete |
e2e_files.rs |
2 | Upload/download/delete lifecycle; oversized upload → 413 |
e2e_gateways.rs |
1 | Parallel gateway over HTTP |
e2e_history.rs |
5 | History generation, querying, business key filter, actor-type filter, pagination |
e2e_instances_page.rs |
2 | X-Total-Count header, full-array response without query parameters |
e2e_lifecycle.rs |
6 | Delete instance, delete definition (409 without cascade), unknown instances, process timers, correlate messages |
e2e_migration.rs |
5 | Instance migration (same node ids, node mapping, unknown target, orphaned token → 422) |
e2e_model_check.rs |
3 | MODEL_CHECK=off omits the report; without feature model-check, warn still deploys (unavailable) and enforce returns 400. Feature build: sound model, deadlock finding, structured rejection |
e2e_monitoring.rs |
7 | Health, ready (dev / REQUIRE_NATS), info, credential redaction, monitoring stats |
e2e_signals.rs |
3 | Signal broadcast to waiters, signal start, GET /api/signals |
e2e_service_tasks.rs |
13 | List, FetchAndLock (limits and lock-duration validation), ExtendLock, Complete, failure/retries, BPMN error, conflicts, incidents |
e2e_start_errors.rs |
8 | Invalid UUID, unknown definition/BPMN ID, timer-start rejection, malformed JSON, 415/413/422 error objects |
e2e_stress.rs |
2 | Concurrent deployments, concurrent starts (multi-thread) |
e2e_suspend_resume.rs |
5 | Suspend/resume lifecycle, token move (204/404) |
e2e_variables.rs |
1 | Variable updates mid-execution |
e2e_versioning.rs |
3 | Version increment, start-latest, instance isolation |
| Test file | Tests | Coverage |
|---|---|---|
app.spec.ts |
52 | Modeler expressions/conditions, gateway configurations, instance lifecycle (suspend/resume), migration dialog, base64 payload monitoring |
| Test file | Tests | Coverage |
|---|---|---|
retry.test.ts |
13 | sleep, calculateBackoff, withRetry (exponential backoff, retry exhaustion) |
TaskService.test.ts |
30 | complete, failure, extendLock, bpmnError (HTTP contract, timeout/retry, error paths) |
ExternalTaskClient.test.ts |
39 | Constructor, lifecycle, poll loop, subscriptions, handler dispatch, retry, lock extension, graceful shutdown |
integration.test.ts |
3 | Real engine-server (via BPMNINJA_SERVER_BIN): deploy, fetch/lock, complete, incidents |
The unit tests use vi.useFakeTimers() and vi.stubGlobal('fetch', …) — no real HTTP calls, no real timers. The integration test is skipped unless BPMNINJA_SERVER_BIN points to a built server binary.
| Area | Files | LoC |
|---|---|---|
| engine-core (lib) | 49 | ~11,500 |
| engine-core (tests) | 3 | ~6,800 |
| bpmn-parser | 4 | ~2,500 |
| persistence-nats | 5 | 1,282 |
| persistence-memory | 2 | ~450 |
| engine-server (lib) | 19 | ~2,400 |
| engine-server (E2E tests) | 20 | ~4,600 |
| desktop-tauri (TypeScript + CSS) | ~55 | ~7,800 |
| desktop-tauri (Rust backend) | 11 | ~900 |
| external-task-client (lib) | 11 | 1,997 |
| external-task-client (tests) | 6 | 1,285 |
| fuzz targets | 9 | 704 |
| Rust workspace | ~112 | ~27,950 |
| Project total | ~177 | ~38,750 |
CI workflow Core Mutation Tests runs cargo-mutants on engine-core and publishes metrics via docs/quality-metrics.json / the Mutation Score badge. The metrics parser reads outcomes.json (summary: CaughtMutant|MissedMutant|…), nested summary counters, and caught.txt/missed.txt fallbacks. Empty mutation artifacts no longer overwrite a previous successful score with 0.0%. A historical full run (2026-04-13) scored 72.4%; the auto-updated QUALITY_METRICS block above reflects the latest successful CI merge. Artifacts are retained 30 days.
The metrics are published weekly (Mondays) on the metrics branch — the badge at the top of this README points there. continue-on-error stays in place for the cargo-mutants run itself; the separate Mutation-Trend prüfen step compares the score against the previous value on the metrics branch and fails below the 65 % threshold or on a trend regression.
To safeguard security- and stability-critical parser and execution components, a parallel fuzzing workflow based on cargo-fuzz (libFuzzer) runs daily in the CI/CD pipeline (and on relevant pull requests).
Currently 9 dedicated fuzz targets are implemented:
fuzz_bpmn_parser: Feeds thequick-xmlparser with arbitrary UTF-8 strings.fuzz_condition: Tests theevaluate_conditionparser with wild expression/variable combinations.fuzz_rhai_script: Checks memory limits and sandboxing when executing manipulated scripts.fuzz_iso8601_duration: Stresses the ISO 8601 parsing logic for timers.fuzz_token_deserialize: Deserializes arbitrary JSON as Token, ProcessInstance, HistoryEntry and FileReference.fuzz_cron_expression: Parses arbitrary strings as cron expressions viacroner.fuzz_deploy_roundtrip: End-to-end pipeline: XML → Parse → Deploy → Start Instance.fuzz_history_diff: Fuzzescalculate_diffandcalculate_diff_from_snapshotwitharbitrary-generated structured input — boundary-length strings (incl. multi-byte UTF-8), large arrays, deeply nested JSON, file-reference-shaped objects, and allInstanceStatevariants.fuzz_server_payloads: Deserializes arbitrary bytes against all 18 REST API DTOs (FetchAndLockRequest,DeployRequest,StartRequest,CorrelateMessageRequest, etc.) to ensure no panics on malformed input.
Sanitizers (AddressSanitizer) are enabled by default and will cause a structured abort and upload of crash artifacts on memory leaks, panics or undefined behaviour.
| Feature | Status |
|---|---|
| Embedded sub-processes (BPMN scopes) | ✅ Implemented |
| Event-based gateway | ✅ Implemented |
Structured JSON logging (tracing-subscriber + JSON) |
✅ Implemented |
| Camunda 7-compatible flow conditions (expression/script) | ✅ Implemented |
Timer start events with repeating intervals (R3/PT30S) |
✅ Implemented |
| Suspend / resume of process instances | ✅ Implemented |
| Extended incident handling (retry / resolve) | ✅ Implemented |
| Historical instances + search | ✅ Implemented |
| Central timer / message / job overview | ✅ Implemented |
| Call Activities (nested process invocation) | ✅ Implemented |
Prometheus metrics endpoint (/metrics) |
✅ Implemented |
Push-based UI updates via SSE (GET /api/events) |
✅ Implemented |
In-memory log stream with filters (GET /api/logs) |
✅ Implemented |
| Multi-arch Docker images (amd64 + arm64) | ✅ Implemented |
| Batch operations on instances | 🔲 Planned |
Process instance migration (POST /api/instances/:id/migrate) |
✅ Implemented |
NATS JetStream log persistence (ENGINE_LOGS stream + file fallback) |
✅ Implemented |
| Token move (modify process instance) | ✅ Implemented |
| Multi-node cluster (NATS-based token locking) | 🔲 Planned |
| OIDC/OAuth2 middleware | 🔲 Planned |
Optional deploy-time model checking (model-check, MODEL_CHECK) |
✅ Implemented |
This project is licensed under either of the following licenses, at your option:
