Building Processes
Service Tasks
Section titled “Service Tasks”A service task represents work done by a system. In Camunda 8 / Zeebe, a job worker picks up the task and completes it:
.serviceTask("charge-card", { name: "Charge Credit Card", taskType: "payment-charge", // worker subscribes to this type taskHeaders: { retries: "3", }, inputMappings: [ { source: "= amount", target: "chargeAmount" }, { source: "= card.token", target: "token" }, ], outputMappings: [ { source: "= transactionId", target: "paymentTransactionId" }, ],})User Tasks
Section titled “User Tasks”A user task waits for a human actor. Optionally attach a Camunda form:
.userTask("review-order", { name: "Review Order", assignee: "= initiator", // FEEL expression candidateGroups: "approvers", formKey: "camunda-forms:bpmn:review-form", dueDate: "= now() + duration(\"P2D\")",})Events
Section titled “Events”Start Events
Section titled “Start Events”// None start.startEvent("start")
// Timer start (runs on a schedule).startEvent("start-daily", { timer: { timeCycle: "R/PT24H" }, // ISO 8601 repeating interval})
// Message start.startEvent("start-on-order", { message: { name: "order-received", correlationKey: "= orderId" },})Intermediate Events
Section titled “Intermediate Events”// Catch a timer (delay).intermediateCatchEvent("wait-1h", { timer: { timeDuration: "PT1H" },})
// Catch a message (wait for external signal).intermediateCatchEvent("wait-for-payment", { message: { name: "payment-confirmed", correlationKey: "= orderId" },})
// Throw a message.intermediateThrowEvent("notify-warehouse", { message: { name: "order-ready" },})End Events
Section titled “End Events”// Normal end.endEvent("end")
// Error end (triggers error boundary event).endEvent("end-error", { error: { code: "PAYMENT_FAILED", message: "Payment processing failed" },})
// Message end.endEvent("end-notify", { message: { name: "process-complete" },})Sub-Processes
Section titled “Sub-Processes”Embed a child process inline. Sub-processes support the full builder API including gateways and branching:
.subProcess("handle-payment", (sub) => sub .startEvent("sub-start") .serviceTask("charge", { taskType: "payment-charge" }) .exclusiveGateway("charge-ok?") .branch("success", (b) => b.condition("= success").serviceTask("receipt", { taskType: "send-receipt" }).endEvent("sub-end") ) .branch("failure", (b) => b.defaultFlow().endEvent("sub-failed", { error: { code: "CHARGE_FAILED" } }) ))Event Sub-Processes
Section titled “Event Sub-Processes”An event sub-process starts when a boundary event fires:
.eventSubProcess("compensation", { triggeredByEvent: true, startEvent: { interrupting: false, error: { code: "PAYMENT_FAILED" }, },}, (sub) => sub .serviceTask("refund", { taskType: "issue-refund" }) .endEvent("refunded"))Boundary Events
Section titled “Boundary Events”.withBoundary() — recommended
Section titled “.withBoundary() — recommended”.withBoundary() attaches a boundary event to the preceding task, lets you build the
error/timeout path inline, then automatically restores the cursor to the original task
so the main flow continues naturally:
.serviceTask("charge", { name: "Charge Card", taskType: "payment-charge" }).withBoundary("on-fail", { errorCode: "PAYMENT_FAILED", cancelActivity: true }, (p) => p .serviceTask("notify", { taskType: "send-email" }) .endEvent("end-failed"),)// cursor is back on "charge" — main flow continues here.serviceTask("fulfill", { name: "Fulfill Order", taskType: "warehouse-pick" }).endEvent("end-ok")Timer boundaries work the same way:
.serviceTask("slow-task", { taskType: "long-job" }).withBoundary("on-timeout", { timerDuration: "PT30S", cancelActivity: false }, (p) => p.serviceTask("escalate", { taskType: "alert" }).endEvent("escalated"),).serviceTask("next-task", { taskType: "continue" }).boundaryEvent() — lower-level
Section titled “.boundaryEvent() — lower-level”Use .boundaryEvent() directly when you need precise cursor control. It moves the
builder cursor to the boundary event itself:
.serviceTask("process-order", { taskType: "order-processor" }).boundaryEvent("timeout", { attachedTo: "process-order", cancelActivity: true, timer: { timeDuration: "PT30M" },}).endEvent("timed-out")Call Activities
Section titled “Call Activities”Invoke a separate (reusable) process definition:
.callActivity("run-subprocess", { name: "Run Fulfillment Sub-Process", calledElement: "fulfillment-process", propagateAllChildVariables: false, inputMappings: [ { source: "= orderId", target: "orderId" }, ], outputMappings: [ { source: "= trackingNumber", target: "trackingNumber" }, ],})Multi-Process Diagrams
Section titled “Multi-Process Diagrams”Bpmn.createDiagram() assembles multiple processes into a single definitions document.
This is useful for caller/callee pairs or any workflow that references another process:
import { Bpmn } from "@bpmnkit/core";
const defs = Bpmn.createDiagram("OrderSystem") .process("order-flow", (p) => p .startEvent("start") .callActivity("run-payment", { processId: "payment-flow" }) .endEvent("end"), ) .process("payment-flow", (p) => p .startEvent("s") .serviceTask("charge", { name: "Charge", taskType: "payment-charge" }) .endEvent("e"), ) .build();
const xml = Bpmn.export(defs); // both processes in one XML fileTask Defaults
Section titled “Task Defaults”.defaults() sets process-wide defaults applied to all subsequent task calls.
Useful when every service task in a process should share the same retry policy:
Bpmn.createProcess("my-flow") .defaults({ serviceTask: { retries: "5" } }) .startEvent("s") .serviceTask("t1", { name: "Task 1", taskType: "worker-a" }) // retries: "5" .serviceTask("t2", { name: "Task 2", taskType: "worker-b" }) // retries: "5" .serviceTask("t3", { name: "Task 3", taskType: "worker-c", retries: "1" }) // override .endEvent("e") .build()Multi-Instance
Section titled “Multi-Instance”Run a task or sub-process once per item in a collection:
.serviceTask("notify-all", { name: "Notify Each Customer", taskType: "send-email", multiInstance: { parallel: true, // false = sequential inputCollection: "= customers", inputElement: "customer", outputCollection: "results", outputElement: "= { sent: true, email: customer.email }", },})