{"id":48864843,"url":"https://github.com/maatini/bpmninja","last_synced_at":"2026-04-15T18:02:53.939Z","repository":{"id":344587777,"uuid":"1182285375","full_name":"maatini/bpmninja","owner":"maatini","description":"Lean BPMN 2.0 workflow engine in Rust — token-based execution, NATS JetStream persistence, Axum REST API, Rhai scripting and a Tauri desktop UI with bpmn-js. Camunda-compatible external tasks, ISO 8601 timers, incident management.","archived":false,"fork":false,"pushed_at":"2026-04-13T16:25:24.000Z","size":9363,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-13T18:19:39.406Z","etag":null,"topics":["axum","bpmn","bpmn-js","bpmn2","camunda-alternative","external-task","jetstream","nats","process-engine","react","rhai","rust","tauri","token-based","tokio","typescript","workflow-engine"],"latest_commit_sha":null,"homepage":"https://maatini.github.io/bpmninja/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maatini.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-15T10:01:01.000Z","updated_at":"2026-04-13T16:25:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/maatini/bpmninja","commit_stats":null,"previous_names":["maatini/mini-bpm-engine","maatini/bpmninja"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/maatini/bpmninja","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maatini%2Fbpmninja","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maatini%2Fbpmninja/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maatini%2Fbpmninja/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maatini%2Fbpmninja/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maatini","download_url":"https://codeload.github.com/maatini/bpmninja/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maatini%2Fbpmninja/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31853279,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-15T15:24:51.572Z","status":"ssl_error","status_checked_at":"2026-04-15T15:24:39.138Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["axum","bpmn","bpmn-js","bpmn2","camunda-alternative","external-task","jetstream","nats","process-engine","react","rhai","rust","tauri","token-based","tokio","typescript","workflow-engine"],"created_at":"2026-04-15T18:02:51.407Z","updated_at":"2026-04-15T18:02:53.902Z","avatar_url":"https://github.com/maatini.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# BPMNinja\n\n[![Rust](https://img.shields.io/badge/Rust-stable-brightgreen.svg?style=flat-square)](https://www.rust-lang.org/)\n[![Tests](https://img.shields.io/badge/Tests-291_passing-success?style=flat-square)]()\n[![Mutation Score](https://img.shields.io/badge/Mutation_Score-72.4%25-blue?style=flat-square)]()\n[![License](https://img.shields.io/badge/License-MIT%20OR%20Apache--2.0-blue.svg?style=flat-square)](#license)\n\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"desktop-tauri/public/logo.png\" alt=\"BPMNinja Logo\" width=\"300\" /\u003e\n\u003c/div\u003e\n\n**A BPMN 2.0 workflow engine written in Rust** — token-based execution, NATS persistence, REST API and desktop UI.\n\n---\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Crates (Modules)](#crates-modules)\n- [Supported BPMN Elements](#supported-bpmn-elements)\n- [Architecture](#architecture)\n- [Quick Start](#quick-start)\n- [REST API](#rest-api)\n- [External Task Client (TypeScript)](#external-task-client-typescript)\n- [Desktop Application (UI)](#desktop-application-ui)\n- [Docker Compose](#docker-compose)\n- [Test Metrics](#test-metrics)\n- [Roadmap](#roadmap)\n- [License](#license)\n\n---\n\n## Overview\n\nbpmninja is a BPMN 2.0 engine with the following core features:\n\n- **Token-based execution** — each path is tracked as an independent token\n- **28 BPMN elements** — start/end events, user/service/script tasks, gateways (XOR, AND, OR, complex, event-based), timers, messages, boundary events (timer, message, error, escalation, compensation), call activities, sub-processes\n- **Full ISO 8601 timers** — Duration (`PT30S`), AbsoluteDate (`2026-04-06T14:30:00Z`), Cron (`0 9 * * MON-FRI`), Repeating Interval (`R3/PT10M`)\n- **Lock-free concurrency** — multi-threaded scaling via `DashMap` wait-state queues with atomic `remove_if` for race-condition-free task operations\n- **NATS JetStream persistence** — KV stores for instances, object store for files, event streaming for history\n- **Fault-tolerant retry queue** — two-stage retry system with a background worker to handle NATS outages\n- **Automatic timer scheduler** — background task processes expired timers (configurable via `TIMER_INTERVAL_MS`)\n- **Camunda-compatible service tasks** — fetch-and-lock pattern with long polling\n- **Rhai script engine** — execution listeners for dynamic variable manipulation\n- **Suspend / resume** — pause and continue instances (timers and tasks blocked)\n- **Incident management** — handle failed service tasks with retry/resolve directly from the UI\n- **Historical instance archival** — completed instances are automatically archived to a separate store with search by definition, business key, date range, status and pagination\n- **Desktop UI** — Tauri app with bpmn-js modeler, live instance tracking, history page for archived instances (including cross-platform GitHub Actions CI releases)\n\n---\n\n## Crates (Modules)\n\n| Crate | Purpose |\n|-------|---------|\n| **`engine-core`** | Core library — state machine, token registry, gateway routing, condition evaluator, script engine |\n| **`bpmn-parser`** | Parses BPMN 2.0 XML (`quick-xml` + `serde`) into internal `ProcessDefinition` structs |\n| **`persistence-nats`** | NATS JetStream-based `WorkflowPersistence` implementation (KV, Object Store, Streams) |\n| **`engine-server`** | Axum HTTP REST API with type-safe error handling (`AppError` → HTTP status codes) |\n| **`desktop-tauri`** | Tauri desktop app (React + bpmn-js) with modeler, instances dashboard, event history and historical instance search |\n| **`agent-orchestrator`** | Sample worker for external service task processing |\n| **`bpmn-ninja-external-task-client`** | TypeScript/Node worker client (ESM) — long polling, retry, lock extension, graceful shutdown |\n\n---\n\n## Supported BPMN Elements\n\n### Basic Elements\n\n| BPMN | Element | Description |\n|:---:|---|---|\n| \u003cimg src=\"readme-assets/bpmn-icons/start-event.svg\" width=\"28\"\u003e | **StartEvent** | Simple starting point — process starts immediately. |\n| \u003cimg src=\"readme-assets/bpmn-icons/timer-start-event.svg\" width=\"28\"\u003e | **TimerStartEvent** | Timer-triggered start — supports ISO 8601 Duration (`PT30S`), AbsoluteDate, cron cycle and repeating intervals. |\n| \u003cimg src=\"readme-assets/bpmn-icons/message-start-event.svg\" width=\"28\"\u003e | **MessageStartEvent** | Process is started by an incoming message (via `messageName`). |\n| \u003cimg src=\"readme-assets/bpmn-icons/end-event.svg\" width=\"28\"\u003e | **EndEvent** | End point — process instance is marked as completed. |\n| \u003cimg src=\"readme-assets/bpmn-icons/terminate-end-event.svg\" width=\"28\"\u003e | **TerminateEndEvent** | End point — immediately aborts all active tokens. |\n| \u003cimg src=\"readme-assets/bpmn-icons/error-end-event.svg\" width=\"28\"\u003e | **ErrorEndEvent** | Terminates the process with a BPMN error code (`errorCode`). |\n| \u003cimg src=\"readme-assets/bpmn-icons/user-task.svg\" width=\"34\"\u003e | **UserTask** | Creates a pending task that must be completed externally. |\n| \u003cimg src=\"readme-assets/bpmn-icons/service-task.svg\" width=\"34\"\u003e | **ServiceTask** | External processing via fetch-and-lock pattern (Camunda-compatible). |\n| \u003cimg src=\"readme-assets/bpmn-icons/script-task.svg\" width=\"34\"\u003e | **ScriptTask** | Executes inline scripts via the Rhai engine. |\n| \u003cimg src=\"readme-assets/bpmn-icons/send-task.svg\" width=\"34\"\u003e | **SendTask** | Sends a message via throw event and continues immediately. |\n\n### Gateways\n\n| BPMN | Element | Description |\n|:---:|---|---|\n| \u003cimg src=\"readme-assets/bpmn-icons/exclusive-gateway.svg\" width=\"28\"\u003e | **ExclusiveGateway (XOR)** | Exactly one path is chosen (condition evaluation). Optional default flow. |\n| \u003cimg src=\"readme-assets/bpmn-icons/parallel-gateway.svg\" width=\"28\"\u003e | **ParallelGateway (AND)** | All paths are followed in parallel (token fork). Join waits for all tokens (JoinBarrier). |\n| \u003cimg src=\"readme-assets/bpmn-icons/inclusive-gateway.svg\" width=\"28\"\u003e | **InclusiveGateway (OR)** | All paths with a `true` condition are followed in parallel. Join waits for the expected tokens. |\n| \u003cimg src=\"readme-assets/bpmn-icons/event-based-gateway.svg\" width=\"28\"\u003e | **EventBasedGateway** | Execution pauses until exactly one of the target catch events (timer/message) fires. |\n\n### Intermediate Events\n\n| BPMN | Element | Description |\n|:---:|---|---|\n| \u003cimg src=\"readme-assets/bpmn-icons/timer-catch-event.svg\" width=\"28\"\u003e | **TimerCatchEvent** | Pauses the process until a timer expires. Supports Duration, AbsoluteDate, cron and repeating intervals. Automatically processed by the timer scheduler. |\n| \u003cimg src=\"readme-assets/bpmn-icons/message-catch-event.svg\" width=\"28\"\u003e | **MessageCatchEvent** | Pauses the process until a matching message is correlated via `POST /api/message`. |\n| \u003cimg src=\"readme-assets/bpmn-icons/boundary-timer-event.svg\" width=\"28\"\u003e | **BoundaryTimerEvent** | Timer event attached to a task (interrupting/non-interrupting). Timer is automatically cancelled when the task completes. |\n| \u003cimg src=\"readme-assets/bpmn-icons/boundary-message-event.svg\" width=\"28\"\u003e | **BoundaryMessageEvent** | Message event attached to a task (interrupting/non-interrupting). Waits asynchronously for external messages. |\n| \u003cimg src=\"readme-assets/bpmn-icons/boundary-error-event.svg\" width=\"28\"\u003e | **BoundaryErrorEvent** | Catches BPMN errors (`errorCode`) of a ServiceTask and routes onto an alternative path. |\n\n### Activities \u0026 Sub-Processes\n\n| BPMN | Element | Description |\n|:---:|---|---|\n| \u003cimg src=\"readme-assets/bpmn-icons/call-activity.svg\" width=\"34\"\u003e | **CallActivity** | Calls another process definition (`calledElement`). Variables are propagated. |\n| \u003cimg src=\"readme-assets/bpmn-icons/embedded-subprocess.svg\" width=\"34\"\u003e | **EmbeddedSubProcess** | Embedded sub-process (flattened into the graph). |\n| \u003cimg src=\"readme-assets/bpmn-icons/subprocess-end-event.svg\" width=\"28\"\u003e | **SubProcessEndEvent** | Internal end event of an embedded sub-process (generated during flattening). |\n\n### Additional Concepts\n\n| Feature | Description |\n|---------|-------------|\n| **Conditional Flows** | Edges with conditions (`amount \u003e 100`, `status == 'approved'`). Operators: `==`, `!=`, `\u003e`, `\u003e=`, `\u003c`, `\u003c=`, truthy checks. |\n| **Execution Listeners** | Start/end scripts on nodes (Rhai). Can read and mutate variables. |\n| **Scope Event Listeners** | Timer/message/error event sub-processes at scope level (interrupting/non-interrupting). |\n| **File Variables** | Upload/download of files as process variables via NATS Object Store. |\n| **Message Correlation** | Matching via `messageName` + optional `businessKey`. |\n| **BPMN Error Handling** | ServiceTasks report errors via `bpmnError`. Routing to the matching `BoundaryErrorEvent`. |\n| **Detailed History** | Gap-free event log with diffs, snapshots and actors (`User`, `Engine`, `Timer`, `ServiceWorker`). |\n| **Instance Archival** | Completed instances are archived to a dedicated store. Searchable by definition key, business key, date range, status with pagination. |\n| **Persistent Wait States** | Timers, messages, user/service tasks survive server restarts via NATS KV. |\n| **Structured JSON Logging** | Configurable via `tracing-subscriber` with the JSON feature and `RUST_LOG` filter. |\n\n### Deviations from the BPMN 2.0 Standard\n\nFor performance and architectural reasons (keep it simple), bpmninja deviates from the strict BPMN 2.0 standard in a few points:\n\n- **Service Tasks (External Task Pattern):** Instead of synchronously executing code inside the engine, `Service Tasks` pause 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.\n- **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 `SubProcessEndEvent`s in the same variable scope.\n- **Script Tasks:** Script evaluation is not done via JavaScript or Groovy, but natively in Rust via the **Rhai engine**.\n- **Multi-Instance (Parallel):** Instead of opening encapsulated execution scopes per iteration, engine forking creates simple parallel tokens on the same task object within the global instance variables.\n\n### Currently Unsupported BPMN Elements\n\nThe engine focuses on a practical and performant core feature set. The following BPMN elements are currently **not** supported and will either cause parser errors on deployment or be silently ignored:\n\n- **Other task types:** `BusinessRuleTask` (no DMN support), `ManualTask`, `ReceiveTask`.\n- **Specific intermediate/boundary events:** `SignalEvent`, `CancelEvent`, `LinkEvent`.\n- **Extended sub-processes:** `Transaction Sub-Process`, `Ad-Hoc Sub-Process`.\n- **Specialized gateways:** `Complex Gateway`.\n- **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\u003cString, serde_json::Value\u003e`).\n\n---\n\n## Architecture\n\n\u003e Detailed documentation with 8 Mermaid diagrams: **[docs/architecture.md](docs/architecture.md)**\n\n```mermaid\nflowchart TD\n    classDef core fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a;\n    classDef server fill:#bae6fd,stroke:#0284c7,stroke-width:2px,color:#0c4a6e;\n    classDef persistence fill:#bbf7d0,stroke:#16a34a,stroke-width:2px,color:#14532d;\n    classDef desktop fill:#fef08a,stroke:#ca8a04,stroke-width:2px,color:#713f12;\n    classDef agent fill:#fbcfe8,stroke:#db2777,stroke-width:2px,color:#831843;\n    classDef storage fill:#f0fdf4,stroke:#16a34a,stroke-width:1px,color:#14532d;\n\n    subgraph \"Clients\"\n        UI[\"desktop-tauri\u003cbr\u003e(Tauri + React + bpmn-js)\"]:::desktop\n        Agent[\"agent-orchestrator\u003cbr\u003e(External Workers)\"]:::agent\n        ExtMsg[\"External Systems\u003cbr\u003e(Messages / Timers)\"]:::agent\n    end\n\n    subgraph \"Server Layer\"\n        Axum[\"engine-server\u003cbr\u003e(Axum REST API)\"]:::server\n    end\n\n    subgraph \"Core Engine\"\n        Parser[\"bpmn-parser\u003cbr\u003e(XML → ProcessDefinition)\"]:::core\n        Engine[\"engine-core\u003cbr\u003e(State Machine / Tokens)\"]:::core\n        Trait[\"WorkflowPersistence\u003cbr\u003e(Trait)\"]:::core\n    end\n\n    subgraph \"Storage\"\n        NatsImpl[\"persistence-nats\"]:::persistence\n        Nats[(\"NATS JetStream\u003cbr\u003eKV + Object Store\")]:::storage\n    end\n\n    UI -- \"HTTP REST\" --\u003e Axum\n    Agent -- \"fetchAndLock / complete\" --\u003e Axum\n    ExtMsg -- \"POST /api/message\" --\u003e Axum\n    Axum --\u003e Parser\n    Axum --\u003e Engine\n    Engine -. \"uses\" .-\u003e Trait\n    Trait -. \"implemented by\" .-\u003e NatsImpl\n    NatsImpl --\u003e Nats\n```\n\n---\n\n## Quick Start\n\n### Prerequisites\n\n**Option A: Devbox** (recommended)\n```bash\n# Automatically installs Rust, Node.js and NATS\ndevbox shell\n```\n\n**Option B: Manual**\n- Rust (via `rustup`)\n- Node.js ≥ 18\n- Docker \u0026 Docker Compose\n\n### Build, Test \u0026 Lint\n\n| Action | Devbox | Shell |\n|--------|--------|-------|\n| **Build** | `devbox run build` | `cargo build --workspace` |\n| **Test** | `devbox run test` | `cargo test --workspace` |\n| **Lint** | `devbox run lint` | `cargo clippy --workspace -- -D warnings` |\n| **Format** | `devbox run fmt` | `cargo fmt --all --check` |\n\n### Starting the Engine Server\n\n```bash\n# 1. Start NATS\ndocker compose up -d nats\n\n# 2. Start the engine server\ncargo run -p engine-server\n```\n\nThe server runs at `http://localhost:8081`.\n\n#### Environment Variables\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `NATS_URL` | `nats://localhost:4222` | NATS server URL |\n| `PORT` | `8081` | HTTP server port |\n| `TIMER_INTERVAL_MS` | `1000` | Timer scheduler polling interval (ms) |\n\n---\n\n## REST API\n\n\u003e Complete OpenAPI 3.0 specification: **[docs/openapi.yaml](docs/openapi.yaml)** | 🌐 **[API Portal (Redoc)](https://maatini.github.io/bpmninja/)** *(requires an active GitHub Pages deploy via /docs)*\n\n### Definitions\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `POST` | `/api/deploy` | Deploy a BPMN definition (max 10 MB) |\n| `GET` | `/api/definitions` | List all definitions |\n| `GET` | `/api/definitions/:id/xml` | Retrieve the BPMN XML of a definition |\n| `DELETE` | `/api/definitions/:id` | Delete a definition (`?cascade=true` to also delete instances) |\n| `DELETE` | `/api/definitions/bpmn/:bpmn_id` | Delete all versions of a BPMN ID |\n\n### Instances\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `POST` | `/api/start` | Start an instance (by `definition_key`) |\n| `POST` | `/api/start/latest` | Start an instance of the latest version (by `bpmn_process_id`) |\n| `POST` | `/api/start/timer` | Start a timer start event instance (repeating intervals) |\n| `GET` | `/api/instances` | List all instances |\n| `GET` | `/api/instances/:id` | Retrieve instance details |\n| `DELETE` | `/api/instances/:id` | Delete an instance |\n| `POST` | `/api/instances/:id/suspend` | Suspend an instance (timers/tasks paused) |\n| `POST` | `/api/instances/:id/resume` | Resume a suspended instance |\n| `PUT` | `/api/instances/:id/variables` | Update variables |\n| `POST` | `/api/instances/:id/move-token` | Move a token to a different node (modify process instance) |\n\n### User Tasks\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `/api/tasks` | List all pending user tasks |\n| `POST` | `/api/complete/:id` | Complete a user task |\n\n### Service Tasks (Camunda-compatible)\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `/api/service-tasks` | List all service tasks |\n| `POST` | `/api/service-task/fetchAndLock` | Fetch and lock tasks (long polling) |\n| `POST` | `/api/service-task/:id/complete` | Complete a task successfully |\n| `POST` | `/api/service-task/:id/failure` | Mark a task as failed |\n| `POST` | `/api/service-task/:id/retry` | Retry an incident (reset retries) |\n| `POST` | `/api/service-task/:id/resolve` | Resolve an incident manually (complete the task without a worker) |\n| `POST` | `/api/service-task/:id/extendLock` | Extend the lock |\n| `POST` | `/api/service-task/:id/bpmnError` | Report a BPMN error |\n\n### Files\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `POST` | `/api/instances/:id/files/:var` | Upload a file (multipart) |\n| `GET` | `/api/instances/:id/files/:var` | Download a file |\n| `DELETE` | `/api/instances/:id/files/:var` | Delete a file variable |\n\n### Events \u0026 Messages\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `POST` | `/api/message` | Correlate a message |\n| `GET` | `/api/messages` | List all pending message subscriptions |\n| `GET` | `/api/timers` | List all active timers |\n| `POST` | `/api/timers/process` | Manually process expired timers |\n\n### History \u0026 Archival\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `/api/instances/:id/history` | Event history of an instance (with filter query params) |\n| `GET` | `/api/instances/:id/history/:eid` | A single history event |\n| `GET` | `/api/history/instances` | List archived (completed) instances with filters (`definition_key`, `business_key`, `from`, `to`, `state`, `limit`, `offset`) |\n| `GET` | `/api/history/instances/:id` | Load a single archived or active instance by ID |\n\n### Monitoring \u0026 Health\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `/api/health` | Liveness check → `200 OK` |\n| `GET` | `/api/ready` | Readiness check (checks NATS connection) |\n| `GET` | `/api/info` | Backend info (type, NATS URL, status) |\n| `GET` | `/api/monitoring` | Engine statistics (instances, tasks, storage, errors) |\n| `GET` | `/api/monitoring/buckets/:bucket/entries` | List KV bucket entries |\n| `GET` | `/api/monitoring/buckets/:bucket/entries/:key` | Load a single KV entry |\n\n### Error Handling\n\nAll errors follow a uniform JSON format:\n\n```json\n{ \"error\": \"Human-readable error message\" }\n```\n\n| HTTP code | Meaning |\n|-----------|---------|\n| `400` | Bad request (bad XML, invalid UUID, missing fields) |\n| `404` | Resource not found (definition, instance, task, node) |\n| `409` | Conflict (task not pending, already locked, already completed) |\n| `500` | Internal server error |\n\n---\n\n## External Task Client (TypeScript)\n\nThe package **[`@bpmninja/external-task-client`](bpmn-ninja-external-task-client/README.md)** is a production-ready worker client for the BPMNinja service task API. It is modelled on the [Camunda External Task Client](https://github.com/camunda/camunda-external-task-client-js), but talks directly to the BPMNinja REST endpoints under `/api/service-task/*`.\n\n### Features\n\n- **Long polling** — efficient fetch-and-lock with configurable `asyncResponseTimeout`\n- **Multi-topic subscriptions** — multiple topics in parallel with individual handlers\n- **Global retry with exponential backoff** — automatic retry on handler errors (`1s → 2s → 4s → …`, capped at 30s)\n- **Automatic lock extension** — prevents lock expiration for long-running tasks\n- **Incident creation** — after retries are exhausted, an incident is created on the process (`failure` with `retries: 0`)\n- **BPMN error throwing** — triggers boundary error events directly from the worker\n- **Graceful shutdown** — waits for in-flight handlers, cancels fetches via `AbortController`\n- **Strict TypeScript** — `strict: true`, ESM, native `fetch()` (Node ≥ 18), no HTTP dependencies\n- **Pino logging** — structured logging, disable via `logger: false`\n\n### Quick Start\n\n```typescript\nimport { ExternalTaskClient } from \"@bpmninja/external-task-client\";\n\nconst client = new ExternalTaskClient({\n  baseUrl: \"http://localhost:8081\",\n  workerId: \"demo-worker-01\",\n  maxRetries: 3,\n  autoExtendLock: true,\n});\n\nclient.subscribe(\"send-email\", async (task, service) =\u003e {\n  const { recipient, subject } = task.variables_snapshot as {\n    recipient: string;\n    subject: string;\n  };\n\n  await sendEmail(recipient, subject);\n\n  await service.complete({\n    emailSent: true,\n    sentAt: new Date().toISOString(),\n  });\n});\n\n// Throw a BPMN error from the worker (Boundary Error Event)\nclient.subscribe(\"validate-order\", async (task, service) =\u003e {\n  const { amount } = task.variables_snapshot as { amount: number };\n  if (amount \u003e 10_000) {\n    await service.bpmnError(\"ORDER_LIMIT_EXCEEDED\");\n    return;\n  }\n  await service.complete({ orderValid: true });\n});\n\nclient.start();\n\n// Graceful shutdown\nprocess.on(\"SIGINT\", async () =\u003e {\n  await client.stop();\n  process.exit(0);\n});\n```\n\n### TaskService (per handler)\n\n| Method | Description |\n|--------|-------------|\n| `complete(variables?)` | Complete the task successfully, optionally with output variables |\n| `failure(msg, details?, retries?)` | Report a failure; `retries: 0` creates an incident |\n| `extendLock(ms)` | Extend the lock (ms, internally converted to seconds) |\n| `bpmnError(errorCode)` | Trigger a BPMN error event on the attached boundary |\n\n### Development \u0026 Tests\n\n```bash\ncd bpmn-ninja-external-task-client\nnpm install\nnpm run lint     # tsc --noEmit\nnpm run test     # vitest run  (68 tests)\nnpm run example  # tsx example/simple-worker.ts\n```\n\nThe full example (`send-email`, `validate-order`, `flaky-task`, shutdown) is in [`bpmn-ninja-external-task-client/example/simple-worker.ts`](bpmn-ninja-external-task-client/example/simple-worker.ts). A complete API reference is available in the [package README](bpmn-ninja-external-task-client/README.md).\n\n---\n\n## Desktop Application (UI)\n\nThe Tauri app connects to the `engine-server` via HTTP.\n\n\u003e **Prerequisite**: a running backend (NATS + engine server). By default, the app expects the backend at `http://localhost:8081` (configurable via `ENGINE_API_URL`).\n\n### 1. Run the backend prerequisites\n\nSave 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:\n\n```yaml\nservices:\n  nats:\n    image: nats:alpine\n    command: [\"--js\", \"--sd\", \"/data\"]\n    ports:\n      - \"4222:4222\"\n      - \"8222:8222\"\n    volumes:\n      - nats-data:/data\n\n  engine-server:\n    image: ghcr.io/maatini/bpmninja/engine-server:latest\n    ports:\n      - \"8081:8081\"\n    environment:\n      - PORT=8081\n      - NATS_URL=nats://nats:4222\n    depends_on:\n      - nats\n\nvolumes:\n  nats-data:\n```\n\n### 2. Install the desktop app binary release\n\nThe ready-to-use apps are available on the [GitHub Releases page](https://github.com/maatini/bpmninja/releases).\n\n*   **macOS (.dmg):** Open the file and drag the icon into your `Applications` folder. _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. Run `xattr -cr /Applications/bpmninja-desktop.app` in the terminal to remove the quarantine flag. After that, the app can be opened normally.\n*   **Windows (.exe / .msi):** Run the installer by double-clicking it. If a Microsoft SmartScreen warning appears, click \"More info\" and then \"Run anyway\".\n*   **Linux (.AppImage / .deb):** Install the `.deb` package via `sudo dpkg -i package.deb`. If you use the `.AppImage`, you may need to make it executable first with `chmod +x app.AppImage`.\n\n### 3. Running from source (for developers)\n\nIf you want to run the UI directly from the source repository:\n\n```bash\n# Devbox\ndevbox run ui:dev\n\n# Or manually\ncd desktop-tauri \u0026\u0026 npm install \u0026\u0026 npm run tauri dev\n```\n\n---\n\n## Docker Compose\n\nStarts NATS + engine server as containers:\n\n```bash\n# Devbox\ndevbox run engine:docker\n\n# Or manually\ndocker compose up --build\n```\n\nServices reachable at `localhost:8081` (API) and `localhost:4222` (NATS).\n\n---\n\n## Test Metrics\n\n\u003e Measured via `cargo test --workspace` (Rust) and `npm run test` (TypeScript) on 2026-04-13 — **291 tests, 0 failures**\n\n### Workspace Overview\n\n| Package | Unit | E2E | Total |\n|---------|------|-----|-------|\n| **engine-core** | 214 | 5 | 219 |\n| **bpmn-parser** | 30 | — | 30 |\n| **persistence-nats** | 2 | — | 2 |\n| **engine-server** | 1 | 39 | 40 |\n| **bpmn-ninja-external-task-client** | 68 | — | 68 |\n| **Total** | **247** | **44** | **291** ✅ |\n\n### engine-core Breakdown (219 tests)\n\n| Module | Tests | Coverage |\n|--------|-------|----------|\n| `engine::unit_tests` | 80 | State machine, gateways, user/service tasks, boundary events, call activities, EventBasedGateway, timers, messages, lock ownership, message correlation |\n| `engine::stress_tests` | 24 | Throughput, gateway correctness, crash recovery, concurrency, race conditions, memory, infinite loops |\n| `domain::tests` | 28 | ProcessDefinition builder, token serialization, validation, timer definitions |\n| `history::tests` | 24 | Diff calculation, human-readable text, truncation, snapshots, UTF-8 safety |\n| `condition::tests` | 5 | Condition evaluation, numeric comparisons, truthy checks |\n| `runtime::instance::tests` | 11 | Audit log limits, file variables, file references |\n| `adapter::in_memory::tests` | 8 | Storage info, history query filters, completed instance archival, pagination, date range, business key, state filter |\n| `retry_queue::tests` | 3 | Display, shutdown, skip missing entities |\n| `boundary::tests` | 3 | No-events, timer boundary setup, message boundary setup |\n| Integration tests | 5 | BPMN compliance, complex gateways |\n\n### bpmn-parser Tests (30 tests)\n\n| Area | Tests | Coverage |\n|------|-------|----------|\n| Basic parsing | 6 | Simple BPMN, conditional flows, XOR gateway, timer start, interleaved output, execution listeners |\n| Gateways | 4 | Parallel, inclusive, event-based, complex |\n| Events | 5 | MessageStart, MessageCatch, ErrorEnd, TimerCatch, BoundaryTimer |\n| Boundary events | 2 | BoundaryTimer, BoundaryError |\n| ISO 8601 timers | 5 | TimeDate, CronCycle, RepeatingInterval, RepeatingInterval-Compact, Duration-Reject |\n| Task types | 3 | ScriptTask, SendTask, IntermediateMessageThrow |\n| Sub-processes | 2 | EventSubProcess, RegularSubProcess |\n| Advanced | 3 | TerminateEndEvent, CompensationEvents, EscalationEvents |\n\n### engine-server E2E Tests (40 tests, 12 files)\n\n| Test file | Tests | Coverage |\n|-----------|-------|----------|\n| `e2e_deploy.rs` | 3 | Deploy, start, parallel gateway |\n| `e2e_file_variables.rs` | 3 | File upload, task completion with files, multi-file + delete |\n| `e2e_files.rs` | 1 | Upload/download/delete lifecycle |\n| `e2e_gateways.rs` | 1 | Parallel gateway over HTTP |\n| `e2e_history.rs` | 4 | History generation, querying, completed instances listing, business key filter, pagination |\n| `e2e_lifecycle.rs` | 6 | Delete instance, delete definition, unknown instances, process timers, correlate messages |\n| `e2e_monitoring.rs` | 4 | Health, ready (connected/disconnected), info, monitoring stats |\n| `e2e_service_tasks.rs` | 7 | List service tasks, FetchAndLock, ExtendLock, Complete, Complete with Failure, BPMN error, lock conflict |\n| `e2e_start_errors.rs` | 4 | Invalid UUID, unknown definition, unknown BPMN ID, timer-start rejection |\n| `e2e_stress.rs` | 2 | Concurrent deployments, concurrent starts (multi-thread) |\n| `e2e_variables.rs` | 1 | Variable updates mid-execution |\n| `e2e_versioning.rs` | 3 | Version increment, start-latest, instance isolation |\n\n### bpmn-ninja-external-task-client Tests (68 tests, 3 files)\n\n| Test file | Tests | Coverage |\n|-----------|-------|----------|\n| `retry.test.ts` | 13 | `sleep`, `calculateBackoff`, `withRetry` (exponential backoff, retry exhaustion) |\n| `TaskService.test.ts` | 20 | `complete`, `failure`, `extendLock`, `bpmnError` (HTTP contract \u0026 error paths) |\n| `ExternalTaskClient.test.ts` | 35 | Constructor, lifecycle, poll loop, subscriptions, handler dispatch, retry, lock extension, graceful shutdown |\n\nTests use `vi.useFakeTimers()` and `vi.stubGlobal('fetch', …)` — no real HTTP calls, no real timers.\n\n### Code Statistics\n\n| Area | Files | LoC |\n|------|-------|-----|\n| engine-core (lib) | 45 | 10,626 |\n| engine-core (tests) | 3 | 6,415 |\n| bpmn-parser | 4 | 2,385 |\n| persistence-nats | 5 | 1,282 |\n| engine-server (lib) | 13 | 1,728 |\n| engine-server (E2E tests) | 12 | 2,106 |\n| desktop-tauri (TypeScript + CSS) | 49 | 7,015 |\n| desktop-tauri (Rust backend) | 10 | 798 |\n| external-task-client (lib) | 11 | 1,997 |\n| external-task-client (tests) | 6 | 1,285 |\n| fuzz targets | 9 | 704 |\n| **Rust workspace** | **101** | **~26,044** |\n| **Project total** | **~167** | **~36,341** |\n\n### Mutation Score\nA full workspace run via [`cargo-mutants`](https://mutants.rs) on the `engine-core` crate (CI workflow \"Core Mutation Tests\", 2026-04-13) produced a **mutation score of 72.4%** (359 caught, 137 missed, 552 unviable, 1 timeout out of 1049 mutants tested in ~3h). Top missed areas: `timer_processor.rs` (7 missed), `scripting/runner.rs` (4 missed), `handlers/events.rs` (1 missed). Results are stored as CI artifacts for 30 days.\n\n### Continuous Fuzzing\nTo 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).\n\nCurrently 9 dedicated fuzz targets are implemented:\n* **`fuzz_bpmn_parser`**: Feeds the `quick-xml` parser with arbitrary UTF-8 strings.\n* **`fuzz_condition`**: Tests the `evaluate_condition` parser with wild expression/variable combinations.\n* **`fuzz_rhai_script`**: Checks memory limits and sandboxing when executing manipulated scripts.\n* **`fuzz_iso8601_duration`**: Stresses the ISO 8601 parsing logic for timers.\n* **`fuzz_token_deserialize`**: Deserializes arbitrary JSON as Token, ProcessInstance, HistoryEntry and FileReference.\n* **`fuzz_cron_expression`**: Parses arbitrary strings as cron expressions via `croner`.\n* **`fuzz_deploy_roundtrip`**: End-to-end pipeline: XML → Parse → Deploy → Start Instance.\n* **`fuzz_history_diff`**: Fuzzes `calculate_diff` and `calculate_diff_from_snapshot` with `arbitrary`-generated structured input — boundary-length strings (incl. multi-byte UTF-8), large arrays, deeply nested JSON, file-reference-shaped objects, and all `InstanceState` variants.\n* **`fuzz_server_payloads`**: Deserializes arbitrary bytes against all 18 REST API DTOs (`FetchAndLockRequest`, `DeployRequest`, `StartRequest`, `CorrelateMessageRequest`, etc.) to ensure no panics on malformed input.\n\nSanitizers (AddressSanitizer) are enabled by default and will cause a structured abort and upload of crash artifacts on *memory leaks*, *panics* or *undefined behaviour*.\n\n---\n\n## Roadmap\n\n| Feature | Status |\n|---------|--------|\n| Embedded sub-processes (BPMN scopes) | ✅ Implemented |\n| Event-based gateway | ✅ Implemented |\n| Structured JSON logging (`tracing-subscriber` + JSON) | ✅ Implemented |\n| Camunda 7-compatible flow conditions (expression/script) | ✅ Implemented |\n| Timer start events with repeating intervals (`R3/PT30S`) | ✅ Implemented |\n| Suspend / resume of process instances | ✅ Implemented |\n| Extended incident handling (retry / resolve) | ✅ Implemented |\n| Historical instances + search | ✅ Implemented |\n| Central timer / message / job overview | ✅ Implemented |\n| Batch operations on instances | 🔲 Planned |\n| Process instance migration | 🔲 Planned |\n| Token move (modify process instance) | ✅ Implemented |\n| Multi-node cluster (NATS-based token locking) | 🔲 Planned |\n| OIDC/OAuth2 middleware | 🔲 Planned |\n| Prometheus metrics endpoint | 🔲 Planned |\n\n---\n\n## License\n\nThis project is licensed under either of the following licenses, at your option:\n\n- [MIT License](LICENSE-MIT)\n- [Apache License, Version 2.0](LICENSE-APACHE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaatini%2Fbpmninja","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaatini%2Fbpmninja","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaatini%2Fbpmninja/lists"}