Best Practices

These guidelines are distilled from real-world BPMN implementations. Following them will help you build processes that are easier to understand, operate, and maintain.

10 BPMN Patterns to Embrace

1 Make Error Paths Explicit

Make error paths explicit and visible in the diagram rather than hiding them in worker code. On the hosted service nothing raises a BPMN error today: the job API has no call to throw one (a job can only be completed or failed), and an error end event is read as a plain end, so an error boundary event deploys but never fires. What works today: the worker completes the job with an outcome variable and an exclusive gateway after the task routes on it, one outgoing flow per outcome (declined, invalid, approved). A technical failure (a timeout, a network error) is reported with fail; once the retries are spent the instance becomes an incident you resolve from the dashboard or the API.

<serviceTask id="processPayment" name="Process Payment"> ... </serviceTask>
<sequenceFlow sourceRef="processPayment" targetRef="paymentOutcome" />
<exclusiveGateway id="paymentOutcome" default="toFulfil" />
<sequenceFlow id="toDeclined" sourceRef="paymentOutcome" targetRef="notifyDeclined">
  <conditionExpression>=paymentStatus = "declined"</conditionExpression>
</sequenceFlow>
<sequenceFlow id="toFulfil" sourceRef="paymentOutcome" targetRef="fulfilOrder" />

2 Use Compensation Handlers

For multi-step transactional processes, attach a compensation boundary event to each task that needs rollback, associated with a handler activity marked isForCompensation="true". Compensation is thrown explicitly: route the failure path (for example the gateway branch a declined payment takes) to an intermediate throw or end event carrying a compensateEventDefinition. The handlers then run for the tasks that actually completed, in reverse order (e.g., release reserved inventory when payment fails). A later failure does not trigger compensation by itself.

3 Always Use Explicit End Events

Every process path should end at an explicit End Event. Never leave a path hanging at a task with no outgoing sequence flow. Implicit ends are confusing and can mask bugs where a token gets stuck.

4 Externalize Decisions to DMN

Business rules change frequently. Move complex routing logic (discount tiers, approval thresholds, risk scores) out of BPMN gateway conditions into DMN decision tables. This lets business analysts update rules without modifying the process model.

5 Use Message Correlation Keys

When using message catch events, always define a correlation key based on a stable business identifier (order ID, customer ID). Avoid using internal system keys that may change. The key is declared on the message and names one process variable. This fragment goes inside a document that declares the bpmn and zeebe namespaces:

<bpmn:message id="Message_PaymentReceived" name="PaymentReceived">
  <bpmn:extensionElements>
    <zeebe:subscription correlationKey="=orderId" />
  </bpmn:extensionElements>
</bpmn:message>

The hosted API does not publish messages yet: a message wait is completed by a worker as an ordinary job, and the worker must check the business identifier itself. See the Correlation guide.

6 Make Service Tasks Idempotent

Design your worker handlers so that running them twice with the same inputs produces the same result, keyed on the job key. If a job is retried after a partial failure (e.g., payment was charged but completion failed), the worker should detect the existing state and not charge twice. A completion that already went through answers 404 when repeated, so read the instance to reconcile instead of retrying blindly.

7 Use Parallel Gateways for True Concurrency

When multiple tasks can run simultaneously (e.g., send email AND update CRM), use a Parallel Gateway to split the flow. This is faster than sequential execution and makes the parallelism explicit in the model.

8 Use Call Activities for Reuse

If the same sequence of tasks appears in multiple processes, extract it into a separate process definition and invoke it via a Call Activity. This avoids copy-paste and ensures all callers benefit from improvements.

9 Add Timer Escalation for User Tasks

Human tasks without deadlines create hidden bottlenecks. Attach a non-interrupting timer boundary event to every user task with a reasonable escalation timeout. Route the escalation path to notify a supervisor or escalate to the next approver.

10 Use Meaningful, Consistent Naming

Name every element in your BPMN diagram clearly. Use verb-noun format for tasks ("Validate Order", "Send Notification"), question format for gateways ("Amount Approved?"), and event names for events ("Payment Received"). Consistent naming makes processes readable by business stakeholders.

10 Patterns to Avoid

1 Catch-All Error Handling

Treating every failure the same way hides the distinction between transient failures (retry) and business outcomes (route). Report a transient failure with fail and let the retries run; complete the job with an outcome variable for a business result such as a declined card, and give each outcome its own gateway branch.

2 Hard-Coded Values in BPMN

Avoid embedding magic numbers and strings directly in gateway conditions (amount > 1000, status = "gold"). Put these in DMN decision tables or process variables so they can be changed without redeploying the process.

3 Infinite Loops Without Exit

Loops (sequence flows going back to earlier tasks) must always have a counter or a condition that guarantees eventual termination. A loop without exit creates an instance that never ends and keeps its workers busy for nothing.

4 Too Many Lanes in One Pool

Using more than 4-5 lanes in a single pool makes diagrams unreadable. If you have many organizational units, use collaboration diagrams with multiple pools and message flows between them instead.

5 Mixing BPMN and EIP Concerns

Keep BPMN diagrams focused on business process flow. Message routing, aggregation, and channel management belong in your integration code, for example the Layer 2 EIP models of the qubit-core library used from Go. Embedding routing logic in BPMN gateway conditions makes both harder to understand.

6 Giant Subprocesses

A subprocess that contains 20+ elements is a sign that it should be extracted into a separate process definition. Large embedded subprocesses are hard to test, reuse, and understand independently.

7 Unnamed Elements

Every element in your BPMN diagram should have a meaningful name. Unnamed tasks ("Task_1", "ServiceTask_abc") are impossible to understand from incident reports or audit logs without checking the diagram.

8 Skipping Process Variables for State

Don't manage process state outside of process variables (e.g., in external database records that your workers query). Variables stored on the instance are visible through GET /api/v1/process-instances/{key}, auditable, and survive worker restarts.

9 Ignoring Incidents

A process instance stuck in the INCIDENT state means a customer is waiting or a transaction is incomplete. No webhook event is sent for incidents, so poll GET /api/v1/process-instances?filter.state=INCIDENT (and open_count in GET /api/v1/incidents) and configure on-call alerts on the result for production environments.

10 Over-Engineering Simple Flows with CMMN

CMMN is powerful but complex. Use it only for genuinely adaptive, knowledge-intensive processes where the flow cannot be predefined. A sequential approval workflow with a fixed 3-step path is better modeled as BPMN, not CMMN.