@bpmnkit/core
v1.6.1
Published
TypeScript-first BPMN 2.0 SDK — parse, build, layout, and optimize diagrams
Maintainers
Readme
Website · Documentation · GitHub · Changelog
Core tier. Semver at 1.0: nothing breaks without a major release. See product tiers.
Overview
@bpmnkit/core is the foundation of the BPMN Kit. It gives you everything to work with BPMN 2.0, DMN 1.3, and Camunda Form definitions in pure TypeScript — no XML wrestling, no runtime dependencies.
Parse → Modify → Validate → ExportFeatures
- BPMN 2.0 — parse, create, and export process diagrams with full Zeebe/Camunda 8 extension support
- Fluent Builder API — construct valid processes programmatically, never touch raw XML
- Sugiyama Layout Engine — auto-position elements with clean orthogonal edge routing
- DMN 1.3 — decision tables, including FEEL expression support
- Camunda Form Definitions — type-safe form schema builder
- Optimizer — built-in rule engine to detect and auto-fix anti-patterns
- Compact Format — 70% smaller token-efficient JSON representation for AI/LLM workflows
- Connector catalog — all Camunda 8 out-of-the-box connector templates, applied deterministically (
@bpmnkit/core/connectors) - Zero Dependencies — runs in browsers, Node.js, Deno, Bun, and edge runtimes
Installation
npm install @bpmnkit/core
pnpm add @bpmnkit/coreQuick Start
Build a process from code
Every element takes (id, options). Branches off a gateway are declared with
.branch(), not by wiring flows by hand — the builder creates the sequence flows.
import { Bpmn } from "@bpmnkit/core"
const defs = Bpmn.createProcess("order-flow")
.name("Order Flow")
.startEvent("start", { name: "Order Received" })
.serviceTask("validate", {
name: "Validate Order",
taskType: "order-validator",
ioMapping: {
inputs: [{ source: "=order", target: "order" }],
outputs: [{ source: "=valid", target: "isValid" }],
},
})
.exclusiveGateway("check", { name: "Order Valid?" })
.branch("valid", (b) =>
b
.condition("=isValid = true")
.serviceTask("fulfill", { name: "Fulfill Order", taskType: "fulfillment-service" })
.endEvent("end", { name: "Order Complete" }),
)
.branch("invalid", (b) =>
b.defaultFlow().endEvent("reject-end", { name: "Order Rejected" }),
)
.withAutoLayout()
.build()
const xml = Bpmn.export(defs)Sub-processes, boundary events and multi-instance
These are the constructs most often reached for and most often guessed at, so the
exact shapes are worth stating. subProcess takes its body as a callback
(second argument) and options third. A boundary event needs the id of the activity
it attaches to; withBoundary supplies it from the preceding activity for you.
import { Bpmn } from "@bpmnkit/core"
const defs = Bpmn.createProcess("fulfilment")
.name("Fulfilment")
.startEvent("start")
// Sub-process: id, content callback, then options.
// multiInstance runs the body once per item in the collection.
.subProcess(
"provision",
(sub) => {
sub
.startEvent("p-start")
.serviceTask("provision-line", { name: "Provision Line", taskType: "provision" })
.endEvent("p-end")
},
{
name: "Provision Each Line",
multiInstance: { collection: "=order.lines", elementVariable: "line", isSequential: false },
},
)
// withBoundary attaches to the activity just added — no id repeated.
.serviceTask("charge", { name: "Charge Payment", taskType: "payment" })
.withBoundary("charge-failed", { errorCode: "PAYMENT_FAILED" }, (b) => {
b.serviceTask("retry-charge", { name: "Retry Charge", taskType: "payment" }).endEvent("gave-up")
})
// Wait for whichever event arrives first.
.eventBasedGateway("await-payment", { name: "Paid?" })
.branch("paid", (b) =>
b
.intermediateCatchEvent("payment-confirmed", {
messageName: "PaymentConfirmed",
correlationKey: "=orderId",
})
.endEvent("done"),
)
.branch("timeout", (b) =>
b.intermediateCatchEvent("payment-timeout", { timerDuration: "P14D" }).endEvent("written-off"),
)
.withAutoLayout()
.build()Attach a boundary event explicitly when it is not the preceding activity — both forms below are accepted, and a boundary event with no host throws rather than exporting BPMN that cannot be read back:
.boundaryEvent("timeout", { attachedTo: "charge", timerDuration: "PT30M" })
.boundaryEvent("timeout", "charge", { timerDuration: "PT30M" })Event definitions are named options, not nested objects — timerDuration,
timerDate, timerCycle, messageName (with correlationKey),
errorCode, signalName, compensation. An option the builder does not
know is dropped silently, so timer: { duration: "P7D" } produces a boundary
event with no timer on it.
Parse and modify existing BPMN
import { Bpmn } from "@bpmnkit/core"
const defs = Bpmn.parse(xml)
const process = defs.processes[0]
// Access flow elements
for (const el of process.flowElements) {
console.log(el.type, el.id, el.name)
}
// Serialize back to XML
const updated = Bpmn.export(defs)Auto-layout a process
import { applyAutoLayout, Bpmn } from "@bpmnkit/core"
// Lays out every process and writes the diagram interchange back onto the model.
const laid = Bpmn.export(applyAutoLayout(Bpmn.parse(xml)))Optimize a diagram
import { Bpmn, optimize } from "@bpmnkit/core"
const defs = Bpmn.parse(xml)
const report = optimize(defs)
console.log(`${report.summary.total} findings`)
for (const finding of report.findings) {
console.log(`[${finding.severity}] ${finding.message}`)
if (finding.applyFix) {
const { description } = finding.applyFix(defs)
console.log("Fixed:", description)
}
}Compact format for AI/LLM workflows
import { Bpmn, compactify, expand } from "@bpmnkit/core"
// Shrink for AI prompt
const defs = Bpmn.parse(xml)
const compact = compactify(defs) // ~70% smaller JSON
const json = JSON.stringify(compact) // send to LLM
// Restore full BPMN from AI response
const restored = expand(JSON.parse(json))
const outXml = Bpmn.export(restored)Rendering while the model is still writing
A model emits a diagram one token at a time, and the outermost } — the one
JSON.parse waits for — is the last character it sends. createCompactStream
reads the elements out of the text as their own literals close, so there is
something to draw long before the document is finished:
import { createCompactStream } from "@bpmnkit/core"
// `base` is the diagram being edited, so a frame shows the whole thing rather
// than the fragment the model is adding to it. Omit it to build from nothing.
const diagramStream = createCompactStream({ base: null })
function onModelChunk(chunk: string): void {
const frame = diagramStream.push(chunk) // null until the frame changes
if (frame) console.log("elements so far:", frame.processes[0]?.flowElements.length)
}Frames are a guess at an unfinished document: push never throws, drops what it
cannot place, and expects the caller to have an authoritative result coming.
API Reference
BPMN
| Export | Description |
|--------|-------------|
| Bpmn.parse(xml) | Parse BPMN XML → BpmnDefinitions |
| Bpmn.export(defs) | Serialize BpmnDefinitions → XML |
| Bpmn.createProcess(id) | Start a ProcessBuilder; set the name with .name(…) |
| Bpmn.makeEmpty(processId?, name?) | Minimal BPMN XML with one start event |
| Bpmn.SAMPLE_XML | 3-node sample diagram string |
Process builder
Every element method is (id, options) and returns the builder. The same methods
exist inside .branch() and inside a sub-process body.
| Method | Description |
|--------|-------------|
| .name(name) / .versionTag(v) | Process-level metadata |
| .startEvent(id?, options?) / .endEvent(id?, options?) | Events; options.name labels them |
| .serviceTask(id, { taskType, ioMapping?, taskHeaders? }) | Zeebe job worker task |
| .userTask(id, opts) / .businessRuleTask(id, { decisionRef }) / .scriptTask(id, opts) | Other task types |
| .receiveTask(id, { message }) / .sendTask(id, opts) / .callActivity(id, opts) | Message and call activities |
| .restConnector(id, { method, url, … }) | Camunda 8 HTTP connector task |
| .exclusiveGateway(id, opts) / .parallelGateway / .inclusiveGateway / .eventBasedGateway | Gateways |
| .branch(name, b => …) | One path off the preceding gateway |
| b.condition(feel) / b.defaultFlow() | Mark a branch's condition, or make it the default |
| .connectTo(id) | Flow to an existing or later element — merges and loops |
| .subProcess(id, content, options?) | Embedded sub-process; content is a callback |
| .transaction(id, content, options?) / .eventSubProcess / .adHocSubProcess | Other containers |
| .withBoundary(id, options, handler) | Boundary event on the preceding activity, plus its path |
| .boundaryEvent(id, options) | Boundary event naming its host in options.attachedTo |
| .intermediateCatchEvent(id, opts) / .intermediateThrowEvent(id, opts) | Intermediate events |
| .withAutoLayout() | Compute coordinates on build() — no x/y by hand |
| .build() | Produce BpmnDefinitions; pass to Bpmn.export |
Event definitions are options on the element: timerDuration, timerDate,
timerCycle, messageName + correlationKey, errorCode, signalName,
compensation. Multi-instance is options.multiInstance =
{ collection, elementVariable, isSequential? }.
Semantics
| Export | Description |
|--------|-------------|
| semanticHash(defs) | SHA-256 of the model, excluding the diagram. Unchanged by layout |
| projectSemantics(defs) | The canonical, presentation-free projection the hash covers |
| diffSemantics(a, b) | What changed between two models, keyed by element id |
Typed code generation
| Export | Description |
|--------|-------------|
| generateProcessTypes(defs \| defs[], options?) | TypeScript source typing job types (variables, output, headers, errors), process ids, messages, signals, error and escalation codes. Deterministic |
| extractProcessContract(defs \| defs[]) | The same contract as data — what casen gen types renders and --check-workers compares |
Editing
| Export | Description |
|--------|-------------|
| applyBpmnOperations(defs, ops) | Apply edit operations to the full model. Strict: unresolved ids throw |
| reconcileCompact(defs, compact) | Apply a compact diagram as changes, keeping what compact cannot carry |
| compactify(defs) | Read-only token-efficient view for LLM prompts. Lossy — not an edit path |
Writing files (@bpmnkit/core/node)
| Export | Description |
|--------|-------------|
| writeBpmn(defs, opts) | Serialize, read back, verify the model survived, then write atomically |
import { writeBpmn } from "@bpmnkit/core/node"
// Refuses rather than overwrite; pass force: true to replace.
const { semanticHash, changes } = await writeBpmn(defs, { output: "flow.bpmn" })Connectors (@bpmnkit/core/connectors)
The Camunda 8 out-of-the-box connector catalog, behind its own subpath so its data is only
bundled by code that imports it. The templates leave out icons, groups, tooltips and
placeholders; @bpmnkit/connectors
adds those back for a property panel.
| Export | Description |
|--------|-------------|
| listConnectors() / searchConnectors(query) | Summaries: task type, direction, required and optional inputs |
| applyConnectorTemplate(id, values) | Builder options for a bundled template, plus problems |
| applyTemplateToElement(defs, id, template, values) | Apply a template to an existing element |
| validateElementTemplate(value) | Structural check of an element template |
| findConnectorCards(query) / formatConnectorCard(card) | One card per operation, with only its inputs — for a model's prompt |
| applyConnectorLines(defs, lines) | Apply the with lines a model wrote in the line format |
| selectConnectors({ text, tasks }) | Pick the cards a model sees for each task of a diagram |
import { applyConnectorTemplate } from "@bpmnkit/core/connectors"
const { serviceTask, problems } = applyConnectorTemplate("io.camunda.connectors.HttpJson.v2", {
url: "https://api.example.com/orders",
})DMN
| Export | Description |
|--------|-------------|
| Dmn.parse(xml) | Parse DMN XML → DmnDefinitions |
| Dmn.export(defs) | Serialize → XML |
| Dmn.createDecisionTable(id, name?) | Start a DecisionTableBuilder |
| Dmn.makeEmpty() | Minimal DMN with one empty decision table |
Form
| Export | Description |
|--------|-------------|
| Form.create() | Start a FormBuilder |
| Form.parse(json) | Parse a form schema |
| Form.export(schema) | Serialize → JSON string |
Layout & Optimization
| Export | Description |
|--------|-------------|
| layoutProcess(process) | Auto-layout all elements; returns LayoutResult |
| optimize(defs) | Run all optimization rules; returns OptimizeReport |
| analyzeCamundaCompat(defs, version?) | What the targeted Camunda 8 version cannot run or requires, as compat/* findings — Camunda Modeler's @camunda/linting rules |
| compactify(defs) | Convert to compact CompactDiagram |
| expand(compact) | Restore full BpmnDefinitions |
| createCompactStream(opts?) | Read a diagram out of a model's token stream, frame by frame |
| parseProcessText(text) | Read a process a model wrote in the line format (PROCESS_TEXT_GUIDE); never throws |
| createProcessTextStream() | Read the line format while it streams, one finished line at a time |
| writeProcessText(defs) | Write an existing diagram in the line format, for a model to read; returns the text and an alias → element id map |
| parseProcessDelta(text) | Read a change script (PROCESS_DELTA_GUIDE): only what changes, by id; never throws |
| generateId(prefix) | Generate a unique short ID |
Process documentation
| Export | Description |
|--------|-------------|
| renderDocumentationHtml(defs, options?) | Self-contained, print-ready HTML: diagram, contents, every element in flow order, linked DMN tables and forms. Print → Save as PDF |
| renderDocumentationMarkdown(defs, options?) | The same content as Markdown, for wikis and Confluence |
| renderDocumentationDocx(defs, options?) | The same content as a Word .docx (bytes), diagram as SVG |
| buildProcessDocumentation(defs, options?) | The structured content, for a renderer of your own |
options takes decisions (DmnDefinitions[]), forms (FormDefinition[]), title, subtitle and diagram. Output is deterministic and all model text is escaped.
Related Packages
| Package | Description |
|---------|-------------|
| @bpmnkit/canvas | Zero-dependency SVG BPMN viewer |
| @bpmnkit/editor | Full-featured interactive BPMN editor |
| @bpmnkit/engine | Lightweight BPMN process simulator for tests and demos |
| @bpmnkit/feel | FEEL expression language parser & evaluator |
| @bpmnkit/plugins | 34 composable canvas plugins |
| @bpmnkit/api | Camunda 8 REST API TypeScript client |
| @bpmnkit/ascii | Render BPMN diagrams as Unicode ASCII art |
| @bpmnkit/markdown | BPMN diagrams in Markdown — remark, markdown-it and README pre-rendering |
| @bpmnkit/docspack | BPMN Kit docs as an offline docspack package for AI agents |
| @bpmnkit/camunda-docspack | Camunda 8 docs as an offline docspack package for AI agents |
| @bpmnkit/ui | Shared design tokens and UI components |
| @bpmnkit/profiles | Shared auth, profile storage, and client factories for CLI & proxy |
| @bpmnkit/operate | Monitoring & operations frontend for Camunda clusters |
| @bpmnkit/connector-gen | Generate connector templates from OpenAPI specs |
| @bpmnkit/connectors | Camunda 8 OOTB connector catalog and deterministic template application |
| @bpmnkit/cli | Camunda 8 command-line interface (casen) |
| @bpmnkit/proxy | Local AI bridge and Camunda API proxy server |
| @bpmnkit/patterns | Domain process patterns for BPMNKit AIKit |
| @bpmnkit/reebe-wasm | WebAssembly BPMN engine for browser simulation |
| @bpmnkit/worker-client | Thin Zeebe REST client for standalone workers |
| @bpmnkit/flow | Code-first durable flows that derive BPMN, job types and workers |
| @bpmnkit/user-tasks | Embeddable user task widget for Camunda 8 |
| @bpmnkit/cli-sdk | Plugin authoring SDK for the casen CLI |
| @bpmnkit/create-casen-plugin | Scaffold a new casen CLI plugin in seconds |
| @bpmnkit/casen-report | HTML reports from Camunda 8 incident and SLA data |
| @bpmnkit/casen-worker-http | Example HTTP worker plugin — completes jobs with live JSONPlaceholder API data |
| @bpmnkit/casen-worker-ai | AI task worker — classify, summarize, extract, and decide using Claude |
