@bpmnkit/core 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,14 @@
1
- import type { BpmnAssociation, BpmnDefinitions, BpmnFlowElement, BpmnSequenceFlow, BpmnTextAnnotation } from "./bpmn-model.js";
1
+ import type { BpmnAssociation, BpmnDefinitions, BpmnError, BpmnEscalation, BpmnFlowElement, BpmnMessage, BpmnSequenceFlow, BpmnSignal, BpmnTextAnnotation } from "./bpmn-model.js";
2
2
  import type { RestConnectorConfig } from "./rest-connector.js";
3
3
  /** Options shared by all element methods. */
4
4
  export interface ElementOptions {
5
5
  name?: string;
6
+ isForCompensation?: boolean;
7
+ }
8
+ /** Options for send/receive task elements. */
9
+ export interface MessageTaskOptions extends ElementOptions {
10
+ /** Message name — generates or reuses a root <bpmn:message> and sets messageRef. */
11
+ messageName?: string;
6
12
  }
7
13
  /** Options for creating a start event. */
8
14
  export interface StartEventOptions extends ElementOptions {
@@ -25,6 +31,11 @@ export interface StartEventOptions extends ElementOptions {
25
31
  modelerTemplateVersion?: string;
26
32
  /** Zeebe modeler template icon (data URI). */
27
33
  modelerTemplateIcon?: string;
34
+ /**
35
+ * Non-interrupting flag — only meaningful for start events inside event sub-processes.
36
+ * Pass `false` to emit `isInterrupting="false"`. Omit for the default interrupting behavior.
37
+ */
38
+ isInterrupting?: boolean;
28
39
  }
29
40
  /** Options for creating a service task. */
30
41
  export interface ServiceTaskOptions {
@@ -53,6 +64,8 @@ export interface ServiceTaskOptions {
53
64
  modelerTemplateVersion?: string;
54
65
  /** Zeebe modeler template icon (data URI). */
55
66
  modelerTemplateIcon?: string;
67
+ /** Mark this task as a compensation handler. */
68
+ isForCompensation?: boolean;
56
69
  }
57
70
  /** Options for creating a script task. */
58
71
  export interface ScriptTaskOptions {
@@ -62,6 +75,8 @@ export interface ScriptTaskOptions {
62
75
  expression: string;
63
76
  /** Variable name to store the result. */
64
77
  resultVariable: string;
78
+ /** Mark this task as a compensation handler. */
79
+ isForCompensation?: boolean;
65
80
  }
66
81
  /** Options for creating a user task. */
67
82
  export interface UserTaskOptions {
@@ -71,6 +86,8 @@ export interface UserTaskOptions {
71
86
  formId?: string;
72
87
  /** Emit <zeebe:userTask /> to mark as a Camunda 8 native user task. */
73
88
  zeebeUserTask?: boolean;
89
+ /** Mark this task as a compensation handler. */
90
+ isForCompensation?: boolean;
74
91
  }
75
92
  /** Options for creating a call activity. */
76
93
  export interface CallActivityOptions {
@@ -80,6 +97,8 @@ export interface CallActivityOptions {
80
97
  processId: string;
81
98
  /** Whether to propagate all child variables. */
82
99
  propagateAllChildVariables?: boolean;
100
+ /** Mark this activity as a compensation handler. */
101
+ isForCompensation?: boolean;
83
102
  }
84
103
  /** Options for creating a business rule task. */
85
104
  export interface BusinessRuleTaskOptions {
@@ -91,6 +110,8 @@ export interface BusinessRuleTaskOptions {
91
110
  decisionId?: string;
92
111
  /** Variable to store the result. */
93
112
  resultVariable?: string;
113
+ /** Mark this task as a compensation handler. */
114
+ isForCompensation?: boolean;
94
115
  }
95
116
  /** Options for gateway elements. */
96
117
  export interface GatewayOptions extends ElementOptions {
@@ -118,12 +139,16 @@ export interface IntermediateThrowEventOptions extends ElementOptions {
118
139
  signalName?: string;
119
140
  /** Escalation code — creates an escalation throw event (aspirational). */
120
141
  escalationCode?: string;
142
+ /** Emit a compensateEventDefinition. */
143
+ compensation?: boolean;
144
+ /** Activity to compensate (activityRef attribute on compensateEventDefinition). */
145
+ activityRef?: string;
121
146
  }
122
147
  /** Options for an end event. */
123
148
  export interface EndEventOptions extends ElementOptions {
124
149
  /** Error code — creates an error end event. */
125
150
  errorCode?: string;
126
- /** Error reference ID. */
151
+ /** Error code — creates an error end event. Alias for errorCode. */
127
152
  errorRef?: string;
128
153
  /** Message name — creates a message end event. */
129
154
  messageName?: string;
@@ -140,7 +165,7 @@ export interface BoundaryEventOptions extends ElementOptions {
140
165
  cancelActivity?: boolean;
141
166
  /** Error code — creates an error boundary event. */
142
167
  errorCode?: string;
143
- /** Error reference ID. */
168
+ /** Error code — creates an error boundary event. Alias for errorCode. */
144
169
  errorRef?: string;
145
170
  /** Timer duration — creates a timer boundary event (aspirational). */
146
171
  timerDuration?: string;
@@ -152,6 +177,8 @@ export interface BoundaryEventOptions extends ElementOptions {
152
177
  messageName?: string;
153
178
  /** Signal name — creates a signal boundary event (aspirational). */
154
179
  signalName?: string;
180
+ /** Creates a compensation boundary event. */
181
+ compensation?: boolean;
155
182
  }
156
183
  /** Multi-instance loop configuration. */
157
184
  export interface MultiInstanceOptions {
@@ -230,9 +257,17 @@ export declare class BranchBuilder {
230
257
  readonly _textAnnotations: BpmnTextAnnotation[];
231
258
  /** @internal */
232
259
  readonly _associations: BpmnAssociation[];
260
+ /** @internal – ID of the last gateway added in this branch (for nested branch() support). */
261
+ private currentGatewayId;
262
+ /** @internal – Open ends of nested branches waiting to auto-connect to the next element. */
263
+ private openBranchEnds;
233
264
  private readonly _annCounters;
265
+ private readonly rootErrors;
266
+ private readonly rootMessages;
267
+ private readonly rootSignals;
268
+ private readonly rootEscalations;
234
269
  /** @internal */
235
- constructor(gatewayId: string, branchName: string);
270
+ constructor(gatewayId: string, branchName: string, rootErrors?: BpmnError[], rootMessages?: BpmnMessage[], rootSignals?: BpmnSignal[], rootEscalations?: BpmnEscalation[]);
236
271
  /** Set a FEEL condition expression on this branch's outgoing sequence flow. */
237
272
  condition(expression: string): this;
238
273
  /** Mark this branch as the gateway's default (no-condition) flow. */
@@ -243,8 +278,10 @@ export declare class BranchBuilder {
243
278
  * Supports forward references (element created later) and backward references (loops).
244
279
  */
245
280
  connectTo(targetId: string): this;
246
- /** @internal – ID of the last element added (or the gateway if branch is empty) */
247
- get _lastNodeId(): string;
281
+ /** @internal – ID of the last element added (or undefined if branches are open). */
282
+ get _lastNodeId(): string | undefined;
283
+ /** @internal – Open ends of nested branches that have not yet been connected. */
284
+ get _openBranchEnds(): string[];
248
285
  /** Attach a text annotation to the element at the current cursor position. */
249
286
  textAnnotation(text: string): this;
250
287
  /** Attach a text annotation to an element by explicit ID. */
@@ -252,10 +289,12 @@ export declare class BranchBuilder {
252
289
  serviceTask(id: string, options: ServiceTaskOptions): this;
253
290
  userTask(id: string, options?: UserTaskOptions): this;
254
291
  scriptTask(id: string, options: ScriptTaskOptions): this;
255
- sendTask(id: string, options?: ElementOptions): this;
256
- receiveTask(id: string, options?: ElementOptions): this;
292
+ sendTask(id: string, options?: MessageTaskOptions): this;
293
+ receiveTask(id: string, options?: MessageTaskOptions): this;
257
294
  businessRuleTask(id: string, options?: BusinessRuleTaskOptions): this;
258
295
  callActivity(id: string, options: CallActivityOptions): this;
296
+ /** Add an abstract task with no Zeebe extensions. */
297
+ task(id: string, options?: ElementOptions): this;
259
298
  startEvent(id?: string, options?: StartEventOptions): this;
260
299
  endEvent(id?: string, options?: EndEventOptions): this;
261
300
  intermediateThrowEvent(id?: string, options?: IntermediateThrowEventOptions): this;
@@ -264,6 +303,32 @@ export declare class BranchBuilder {
264
303
  parallelGateway(id: string, options?: ElementOptions): this;
265
304
  inclusiveGateway(id: string, options?: GatewayOptions): this;
266
305
  eventBasedGateway(id: string, options?: ElementOptions): this;
306
+ /**
307
+ * Add a boundary event attached to an existing activity in this branch.
308
+ *
309
+ * The boundary event is NOT connected by a sequence flow — it attaches via
310
+ * `attachedToRef`. The builder cursor advances to the boundary event so
311
+ * subsequent elements chain from it. Use `withBoundary()` if you want the
312
+ * cursor to return to the task afterward.
313
+ */
314
+ boundaryEvent(id: string, options: BoundaryEventOptions): this;
315
+ /**
316
+ * Attach a boundary event to the preceding task and build its outgoing path,
317
+ * then restore the branch cursor to the preceding task so the main branch flow continues.
318
+ *
319
+ * @param id - ID for the boundary event element.
320
+ * @param options - Boundary event options (without `attachedTo` — inferred from cursor).
321
+ * @param handler - Callback that chains elements from the boundary event.
322
+ */
323
+ withBoundary(id: string, options: Omit<BoundaryEventOptions, "attachedTo">, handler: (b: BranchBuilder) => void): this;
324
+ /**
325
+ * Create a named branch from the last gateway added inside this branch.
326
+ *
327
+ * Works identically to the top-level `ProcessBuilder.branch()` — use
328
+ * `.condition(expr)` or `.defaultFlow()` inside the callback, finish with
329
+ * `.connectTo(id)` or an `.endEvent()` to terminate the nested branch.
330
+ */
331
+ branch(name: string, callback: (b: BranchBuilder) => void): this;
267
332
  }
268
333
  /** Builder for the contents of a sub-process or ad-hoc sub-process. */
269
334
  export declare class SubProcessContentBuilder {
@@ -279,6 +344,9 @@ export declare class SubProcessContentBuilder {
279
344
  private lastNodeId;
280
345
  private currentGatewayId;
281
346
  private openBranchEnds;
347
+ private readonly rootMessages;
348
+ /** @internal */
349
+ constructor(rootMessages?: BpmnMessage[]);
282
350
  private addElement;
283
351
  startEvent(id?: string, options?: StartEventOptions): this;
284
352
  endEvent(id?: string, options?: EndEventOptions): this;
@@ -289,8 +357,10 @@ export declare class SubProcessContentBuilder {
289
357
  userTask(id: string, options?: UserTaskOptions): this;
290
358
  businessRuleTask(id: string, options?: BusinessRuleTaskOptions): this;
291
359
  callActivity(id: string, options: CallActivityOptions): this;
292
- sendTask(id: string, options?: ElementOptions): this;
293
- receiveTask(id: string, options?: ElementOptions): this;
360
+ sendTask(id: string, options?: MessageTaskOptions): this;
361
+ receiveTask(id: string, options?: MessageTaskOptions): this;
362
+ /** Add an abstract task with no Zeebe extensions. */
363
+ task(id: string, options?: ElementOptions): this;
294
364
  exclusiveGateway(id: string, options?: GatewayOptions): this;
295
365
  parallelGateway(id: string, options?: ElementOptions): this;
296
366
  inclusiveGateway(id: string, options?: GatewayOptions): this;
@@ -313,6 +383,8 @@ export declare class ProcessBuilder {
313
383
  private readonly sequenceFlows;
314
384
  private readonly rootErrors;
315
385
  private readonly rootMessages;
386
+ private readonly rootSignals;
387
+ private readonly rootEscalations;
316
388
  private readonly _textAnnotations;
317
389
  private readonly _associations;
318
390
  private readonly _annCounters;
@@ -322,6 +394,7 @@ export declare class ProcessBuilder {
322
394
  private _autoLayout;
323
395
  private _executionPlatformVersion;
324
396
  private _serviceTaskDefaults;
397
+ private _savedMainFlowId;
325
398
  constructor(processId: string);
326
399
  /** Enable auto-layout: `build()` will run the layout engine and populate diagram interchange data. */
327
400
  withAutoLayout(): this;
@@ -386,13 +459,15 @@ export declare class ProcessBuilder {
386
459
  /** Add a user task with optional form reference. */
387
460
  userTask(id: string, options?: UserTaskOptions): this;
388
461
  /** Add a send task (aspirational). */
389
- sendTask(id: string, options?: ElementOptions): this;
462
+ sendTask(id: string, options?: MessageTaskOptions): this;
390
463
  /** Add a receive task (aspirational). */
391
- receiveTask(id: string, options?: ElementOptions): this;
464
+ receiveTask(id: string, options?: MessageTaskOptions): this;
392
465
  /** Add a business rule task. */
393
466
  businessRuleTask(id: string, options?: BusinessRuleTaskOptions): this;
394
467
  /** Add a call activity referencing another process. */
395
468
  callActivity(id: string, options: CallActivityOptions): this;
469
+ /** Add an abstract task with no Zeebe extensions. */
470
+ task(id: string, options?: ElementOptions): this;
396
471
  /** Add an exclusive gateway (XOR split/join). */
397
472
  exclusiveGateway(id: string, options?: GatewayOptions): this;
398
473
  /** Add a parallel gateway (AND split/join). */
@@ -432,7 +507,7 @@ export declare class ProcessBuilder {
432
507
  adHocSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: AdHocSubProcessOptions): this;
433
508
  /** Add a sub-process (aspirational). */
434
509
  subProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: SubProcessOptions): this;
435
- /** Add an event sub-process (aspirational). */
510
+ /** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
436
511
  eventSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: ElementOptions): this;
437
512
  /** Attach a text annotation to the element at the current cursor position. */
438
513
  textAnnotation(text: string): this;
@@ -7,7 +7,15 @@ const EXPORTER_VERSION = "0.0.23";
7
7
  // ---------------------------------------------------------------------------
8
8
  // Internal helpers
9
9
  // ---------------------------------------------------------------------------
10
- function buildEventDefinitions(opts, rootErrors, rootMessages) {
10
+ function resolveMessage(messageName, rootMessages) {
11
+ let existing = rootMessages.find((m) => m.name === messageName);
12
+ if (!existing) {
13
+ existing = { id: generateId("Message"), name: messageName, unknownAttributes: {} };
14
+ rootMessages.push(existing);
15
+ }
16
+ return existing.id;
17
+ }
18
+ function buildEventDefinitions(opts, rootErrors, rootMessages, rootSignals, rootEscalations) {
11
19
  const defs = [];
12
20
  if (opts.timerDuration || opts.timerDate || opts.timerCycle) {
13
21
  defs.push({
@@ -18,28 +26,57 @@ function buildEventDefinitions(opts, rootErrors, rootMessages) {
18
26
  });
19
27
  }
20
28
  if (opts.errorCode !== undefined || opts.errorRef !== undefined) {
21
- let errorRef = opts.errorRef;
22
- if (!errorRef && opts.errorCode !== undefined && rootErrors) {
23
- const errorId = generateId("Error");
24
- rootErrors.push({ id: errorId, name: opts.errorCode, errorCode: opts.errorCode });
25
- errorRef = errorId;
29
+ const codeOrRef = opts.errorCode ?? opts.errorRef;
30
+ let errorRef;
31
+ if (codeOrRef !== undefined && rootErrors) {
32
+ let existing = rootErrors.find((e) => e.errorCode === codeOrRef || e.name === codeOrRef);
33
+ if (!existing) {
34
+ existing = { id: generateId("Error"), name: codeOrRef, errorCode: codeOrRef };
35
+ rootErrors.push(existing);
36
+ }
37
+ errorRef = existing.id;
38
+ }
39
+ else {
40
+ errorRef = codeOrRef;
26
41
  }
27
42
  defs.push({ type: "error", errorRef });
28
43
  }
29
44
  if (opts.messageName !== undefined) {
30
- let messageRef = opts.messageName;
31
- if (rootMessages) {
32
- const messageId = generateId("Message");
33
- rootMessages.push({ id: messageId, name: opts.messageName, unknownAttributes: {} });
34
- messageRef = messageId;
35
- }
45
+ const messageRef = rootMessages
46
+ ? resolveMessage(opts.messageName, rootMessages)
47
+ : opts.messageName;
36
48
  defs.push({ type: "message", messageRef });
37
49
  }
38
50
  if (opts.signalName !== undefined) {
39
- defs.push({ type: "signal", signalRef: opts.signalName });
51
+ let signalRef = opts.signalName;
52
+ if (rootSignals) {
53
+ let existing = rootSignals.find((s) => s.name === opts.signalName);
54
+ if (!existing) {
55
+ existing = { id: generateId("Signal"), name: opts.signalName };
56
+ rootSignals.push(existing);
57
+ }
58
+ signalRef = existing.id;
59
+ }
60
+ defs.push({ type: "signal", signalRef });
40
61
  }
41
62
  if (opts.escalationCode !== undefined) {
42
- defs.push({ type: "escalation", escalationRef: opts.escalationCode });
63
+ let escalationRef = opts.escalationCode;
64
+ if (rootEscalations) {
65
+ let existing = rootEscalations.find((e) => e.escalationCode === opts.escalationCode);
66
+ if (!existing) {
67
+ existing = {
68
+ id: generateId("Escalation"),
69
+ name: opts.escalationCode,
70
+ escalationCode: opts.escalationCode,
71
+ };
72
+ rootEscalations.push(existing);
73
+ }
74
+ escalationRef = existing.id;
75
+ }
76
+ defs.push({ type: "escalation", escalationRef });
77
+ }
78
+ if (opts.compensation) {
79
+ defs.push({ type: "compensate", activityRef: opts.activityRef });
43
80
  }
44
81
  return defs;
45
82
  }
@@ -213,10 +250,12 @@ function makeServiceTaskEl(id, options) {
213
250
  extensionElements: buildServiceTaskExtensions(options),
214
251
  });
215
252
  el.unknownAttributes = unknownAttributes;
253
+ if (options.isForCompensation)
254
+ el.isForCompensation = true;
216
255
  return el;
217
256
  }
218
257
  function makeScriptTaskEl(id, options) {
219
- return makeFlowElement(id, "scriptTask", {
258
+ const el = makeFlowElement(id, "scriptTask", {
220
259
  name: options.name,
221
260
  extensionElements: zeebeExtensionsToXmlElements({
222
261
  unknownElements: [
@@ -228,13 +267,19 @@ function makeScriptTaskEl(id, options) {
228
267
  ],
229
268
  }),
230
269
  });
270
+ if (options.isForCompensation)
271
+ el.isForCompensation = true;
272
+ return el;
231
273
  }
232
274
  function makeUserTaskEl(id, options) {
233
275
  const ext = zeebeExtensionsToXmlElements({
234
276
  ...(options?.zeebeUserTask ? { userTask: true } : {}),
235
277
  ...(options?.formId ? { formDefinition: { formId: options.formId } } : {}),
236
278
  });
237
- return makeFlowElement(id, "userTask", { name: options?.name, extensionElements: ext });
279
+ const el = makeFlowElement(id, "userTask", { name: options?.name, extensionElements: ext });
280
+ if (options?.isForCompensation)
281
+ el.isForCompensation = true;
282
+ return el;
238
283
  }
239
284
  function makeBusinessRuleTaskEl(id, options) {
240
285
  const ext = [];
@@ -249,19 +294,28 @@ function makeBusinessRuleTaskEl(id, options) {
249
294
  },
250
295
  }));
251
296
  }
252
- return makeFlowElement(id, "businessRuleTask", { name: options?.name, extensionElements: ext });
297
+ const el = makeFlowElement(id, "businessRuleTask", {
298
+ name: options?.name,
299
+ extensionElements: ext,
300
+ });
301
+ if (options?.isForCompensation)
302
+ el.isForCompensation = true;
303
+ return el;
253
304
  }
254
305
  function makeCallActivityEl(id, options) {
255
306
  const attrs = { processId: options.processId };
256
307
  if (options.propagateAllChildVariables !== undefined) {
257
308
  attrs.propagateAllChildVariables = String(options.propagateAllChildVariables);
258
309
  }
259
- return makeFlowElement(id, "callActivity", {
310
+ const el = makeFlowElement(id, "callActivity", {
260
311
  name: options.name,
261
312
  extensionElements: zeebeExtensionsToXmlElements({
262
313
  unknownElements: [{ name: "zeebe:calledElement", attributes: attrs, children: [] }],
263
314
  }),
264
315
  });
316
+ if (options.isForCompensation)
317
+ el.isForCompensation = true;
318
+ return el;
265
319
  }
266
320
  function makeExclusiveGatewayEl(id, options) {
267
321
  const el = makeFlowElement(id, "exclusiveGateway", options);
@@ -332,15 +386,17 @@ function insertJoinGateways(elements, flows) {
332
386
  const gwType = elementTypes.get(splitId);
333
387
  if (!gwType)
334
388
  continue;
389
+ // eventBasedGateway is split-only; converge through an XOR join instead
390
+ const joinType = gwType === "eventBasedGateway" ? "exclusiveGateway" : gwType;
335
391
  const targetType = elementTypes.get(targetId);
336
- if (targetType === gwType)
392
+ if (targetType === joinType)
337
393
  continue;
338
394
  const joinId = `${splitId}_join`;
339
395
  if (elementTypes.has(joinId))
340
396
  continue;
341
- const joinElement = makeFlowElement(joinId, gwType, {});
397
+ const joinElement = makeFlowElement(joinId, joinType, {});
342
398
  elements.push(joinElement);
343
- elementTypes.set(joinId, gwType);
399
+ elementTypes.set(joinId, joinType);
344
400
  for (const flow of convergingFlows)
345
401
  flow.targetRef = joinId;
346
402
  flows.push({
@@ -400,12 +456,24 @@ export class BranchBuilder {
400
456
  _textAnnotations = [];
401
457
  /** @internal */
402
458
  _associations = [];
459
+ /** @internal – ID of the last gateway added in this branch (for nested branch() support). */
460
+ currentGatewayId;
461
+ /** @internal – Open ends of nested branches waiting to auto-connect to the next element. */
462
+ openBranchEnds = [];
403
463
  _annCounters = new Map();
464
+ rootErrors;
465
+ rootMessages;
466
+ rootSignals;
467
+ rootEscalations;
404
468
  /** @internal */
405
- constructor(gatewayId, branchName) {
469
+ constructor(gatewayId, branchName, rootErrors = [], rootMessages = [], rootSignals = [], rootEscalations = []) {
406
470
  this.gatewayId = gatewayId;
407
471
  this.branchName = branchName;
408
472
  this.lastNodeId = gatewayId;
473
+ this.rootErrors = rootErrors;
474
+ this.rootMessages = rootMessages;
475
+ this.rootSignals = rootSignals;
476
+ this.rootEscalations = rootEscalations;
409
477
  }
410
478
  /** Set a FEEL condition expression on this branch's outgoing sequence flow. */
411
479
  condition(expression) {
@@ -419,22 +487,34 @@ export class BranchBuilder {
419
487
  }
420
488
  addElement(element) {
421
489
  this._elements.push(element);
422
- const flowId = generateId("Flow");
423
- const flow = {
424
- id: flowId,
425
- sourceRef: this.lastNodeId,
426
- targetRef: element.id,
427
- name: this.isFirstElement ? this.branchName : undefined,
428
- conditionExpression: this.isFirstElement && this.pendingCondition
429
- ? makeConditionExpression(this.pendingCondition)
430
- : undefined,
431
- extensionElements: [],
432
- unknownAttributes: {},
433
- };
434
- this._flows.push(flow);
435
- if (this.isFirstElement && this.pendingDefault) {
436
- this._defaultFlowId = flowId;
490
+ if (this.lastNodeId) {
491
+ const flowId = generateId("Flow");
492
+ const flow = {
493
+ id: flowId,
494
+ sourceRef: this.lastNodeId,
495
+ targetRef: element.id,
496
+ name: this.isFirstElement ? this.branchName : undefined,
497
+ conditionExpression: this.isFirstElement && this.pendingCondition
498
+ ? makeConditionExpression(this.pendingCondition)
499
+ : undefined,
500
+ extensionElements: [],
501
+ unknownAttributes: {},
502
+ };
503
+ this._flows.push(flow);
504
+ if (this.isFirstElement && this.pendingDefault) {
505
+ this._defaultFlowId = flowId;
506
+ }
507
+ }
508
+ for (const branchEnd of this.openBranchEnds) {
509
+ this._flows.push({
510
+ id: generateId("Flow"),
511
+ sourceRef: branchEnd,
512
+ targetRef: element.id,
513
+ extensionElements: [],
514
+ unknownAttributes: {},
515
+ });
437
516
  }
517
+ this.openBranchEnds = [];
438
518
  this.isFirstElement = false;
439
519
  this.lastNodeId = element.id;
440
520
  return this;
@@ -447,6 +527,7 @@ export class BranchBuilder {
447
527
  const flowId = generateId("Flow");
448
528
  const flow = {
449
529
  id: flowId,
530
+ // biome-ignore lint/style/noNonNullAssertion: lastNodeId starts as gatewayId and is always defined in pre-branch context
450
531
  sourceRef: this.lastNodeId,
451
532
  targetRef: targetId,
452
533
  name: this.isFirstElement ? this.branchName : undefined,
@@ -465,13 +546,18 @@ export class BranchBuilder {
465
546
  this._connected = true;
466
547
  return this;
467
548
  }
468
- /** @internal – ID of the last element added (or the gateway if branch is empty) */
549
+ /** @internal – ID of the last element added (or undefined if branches are open). */
469
550
  get _lastNodeId() {
470
551
  return this.lastNodeId;
471
552
  }
553
+ /** @internal – Open ends of nested branches that have not yet been connected. */
554
+ get _openBranchEnds() {
555
+ return this.openBranchEnds;
556
+ }
472
557
  // ---- Annotations ----
473
558
  /** Attach a text annotation to the element at the current cursor position. */
474
559
  textAnnotation(text) {
560
+ // biome-ignore lint/style/noNonNullAssertion: lastNodeId starts as gatewayId and is always defined in pre-branch context
475
561
  return this.annotate(this.lastNodeId, text);
476
562
  }
477
563
  /** Attach a text annotation to an element by explicit ID. */
@@ -500,10 +586,20 @@ export class BranchBuilder {
500
586
  return this.addElement(makeScriptTaskEl(id, options));
501
587
  }
502
588
  sendTask(id, options) {
503
- return this.addElement(makeFlowElement(id, "sendTask", options));
589
+ const el = makeFlowElement(id, "sendTask", options);
590
+ if (options?.isForCompensation)
591
+ el.isForCompensation = true;
592
+ if (options?.messageName)
593
+ el.messageRef = resolveMessage(options.messageName, this.rootMessages);
594
+ return this.addElement(el);
504
595
  }
505
596
  receiveTask(id, options) {
506
- return this.addElement(makeFlowElement(id, "receiveTask", options));
597
+ const el = makeFlowElement(id, "receiveTask", options);
598
+ if (options?.isForCompensation)
599
+ el.isForCompensation = true;
600
+ if (options?.messageName)
601
+ el.messageRef = resolveMessage(options.messageName, this.rootMessages);
602
+ return this.addElement(el);
507
603
  }
508
604
  businessRuleTask(id, options) {
509
605
  return this.addElement(makeBusinessRuleTaskEl(id, options));
@@ -511,47 +607,157 @@ export class BranchBuilder {
511
607
  callActivity(id, options) {
512
608
  return this.addElement(makeCallActivityEl(id, options));
513
609
  }
610
+ /** Add an abstract task with no Zeebe extensions. */
611
+ task(id, options) {
612
+ const el = makeFlowElement(id, "task", options);
613
+ if (options?.isForCompensation)
614
+ el.isForCompensation = true;
615
+ return this.addElement(el);
616
+ }
514
617
  startEvent(id, options) {
515
618
  const el = makeFlowElement(id ?? generateId("StartEvent"), "startEvent", options);
516
- if (el.type === "startEvent" &&
517
- (options?.timerDuration || options?.timerCycle || options?.timerDate)) {
518
- el.eventDefinitions = buildEventDefinitions(options);
619
+ if (el.type === "startEvent" && options) {
620
+ el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
519
621
  }
520
622
  return this.addElement(el);
521
623
  }
522
624
  endEvent(id, options) {
523
625
  const el = makeFlowElement(id ?? generateId("EndEvent"), "endEvent", options);
524
626
  if (el.type === "endEvent" && options) {
525
- el.eventDefinitions = buildEventDefinitions(options);
627
+ el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
526
628
  }
527
629
  return this.addElement(el);
528
630
  }
529
631
  intermediateThrowEvent(id, options) {
530
632
  const el = makeFlowElement(id ?? generateId("IntermediateThrowEvent"), "intermediateThrowEvent", options);
531
633
  if (el.type === "intermediateThrowEvent" && options) {
532
- el.eventDefinitions = buildEventDefinitions(options);
634
+ el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
533
635
  }
534
636
  return this.addElement(el);
535
637
  }
536
638
  intermediateCatchEvent(id, options) {
537
639
  const el = makeFlowElement(id ?? generateId("IntermediateCatchEvent"), "intermediateCatchEvent", options);
538
640
  if (el.type === "intermediateCatchEvent" && options) {
539
- el.eventDefinitions = buildEventDefinitions(options);
641
+ el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
540
642
  }
541
643
  return this.addElement(el);
542
644
  }
543
645
  exclusiveGateway(id, options) {
646
+ this.currentGatewayId = id;
544
647
  return this.addElement(makeExclusiveGatewayEl(id, options));
545
648
  }
546
649
  parallelGateway(id, options) {
650
+ this.currentGatewayId = id;
547
651
  return this.addElement(makeFlowElement(id, "parallelGateway", options));
548
652
  }
549
653
  inclusiveGateway(id, options) {
654
+ this.currentGatewayId = id;
550
655
  return this.addElement(makeInclusiveGatewayEl(id, options));
551
656
  }
552
657
  eventBasedGateway(id, options) {
658
+ this.currentGatewayId = id;
553
659
  return this.addElement(makeFlowElement(id, "eventBasedGateway", options));
554
660
  }
661
+ /**
662
+ * Add a boundary event attached to an existing activity in this branch.
663
+ *
664
+ * The boundary event is NOT connected by a sequence flow — it attaches via
665
+ * `attachedToRef`. The builder cursor advances to the boundary event so
666
+ * subsequent elements chain from it. Use `withBoundary()` if you want the
667
+ * cursor to return to the task afterward.
668
+ */
669
+ boundaryEvent(id, options) {
670
+ const element = makeFlowElement(id, "boundaryEvent", options);
671
+ if (element.type === "boundaryEvent") {
672
+ element.attachedToRef = options.attachedTo;
673
+ element.cancelActivity = options.cancelActivity;
674
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
675
+ }
676
+ // Push directly — no sequence flow, boundary events attach via attachedToRef
677
+ this._elements.push(element);
678
+ this.lastNodeId = element.id;
679
+ this.isFirstElement = false;
680
+ return this;
681
+ }
682
+ /**
683
+ * Attach a boundary event to the preceding task and build its outgoing path,
684
+ * then restore the branch cursor to the preceding task so the main branch flow continues.
685
+ *
686
+ * @param id - ID for the boundary event element.
687
+ * @param options - Boundary event options (without `attachedTo` — inferred from cursor).
688
+ * @param handler - Callback that chains elements from the boundary event.
689
+ */
690
+ withBoundary(id, options, handler) {
691
+ const attachedTo = this.lastNodeId;
692
+ if (!attachedTo || attachedTo === this.gatewayId) {
693
+ throw new Error("withBoundary() must follow a task element inside the branch. Current builder position has no active task.");
694
+ }
695
+ const attachedEl = this._elements.find((n) => n.id === attachedTo);
696
+ if (attachedEl?.type === "boundaryEvent") {
697
+ throw new Error("withBoundary() cannot attach to a boundary event. It must follow a task or activity element.");
698
+ }
699
+ const savedLast = this.lastNodeId;
700
+ const savedGateway = this.currentGatewayId;
701
+ const savedConnected = this._connected;
702
+ const savedOpenEnds = [...this.openBranchEnds];
703
+ this.openBranchEnds = [];
704
+ // Create and push the boundary event (no sequence flow)
705
+ this.boundaryEvent(id, { ...options, attachedTo });
706
+ // Build the boundary event's outgoing path
707
+ handler(this);
708
+ // Restore cursor to the task so the branch main flow continues
709
+ this.lastNodeId = savedLast;
710
+ this.currentGatewayId = savedGateway;
711
+ this._connected = savedConnected;
712
+ this.openBranchEnds = savedOpenEnds;
713
+ return this;
714
+ }
715
+ /**
716
+ * Create a named branch from the last gateway added inside this branch.
717
+ *
718
+ * Works identically to the top-level `ProcessBuilder.branch()` — use
719
+ * `.condition(expr)` or `.defaultFlow()` inside the callback, finish with
720
+ * `.connectTo(id)` or an `.endEvent()` to terminate the nested branch.
721
+ */
722
+ branch(name, callback) {
723
+ if (!this.currentGatewayId) {
724
+ throw new Error("branch() must be called after a gateway element");
725
+ }
726
+ const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
727
+ callback(b);
728
+ for (const el of b._elements) {
729
+ if (this._elements.some((n) => n.id === el.id)) {
730
+ throw new Error(`Duplicate element ID "${el.id}"`);
731
+ }
732
+ this._elements.push(el);
733
+ }
734
+ for (const fl of b._flows)
735
+ this._flows.push(fl);
736
+ for (const ann of b._textAnnotations)
737
+ this._textAnnotations.push(ann);
738
+ for (const assoc of b._associations)
739
+ this._associations.push(assoc);
740
+ if (b._defaultFlowId) {
741
+ const gw = this._elements.find((n) => n.id === this.currentGatewayId);
742
+ if (gw && (gw.type === "exclusiveGateway" || gw.type === "inclusiveGateway")) {
743
+ gw.default = b._defaultFlowId;
744
+ }
745
+ }
746
+ if (!b._connected) {
747
+ const allEnds = [
748
+ ...(b._lastNodeId !== undefined ? [b._lastNodeId] : []),
749
+ ...b._openBranchEnds,
750
+ ];
751
+ for (const endId of allEnds) {
752
+ const endEl = this._elements.find((n) => n.id === endId);
753
+ if (endEl && endEl.type !== "endEvent") {
754
+ this.openBranchEnds.push(endId);
755
+ }
756
+ }
757
+ }
758
+ this.lastNodeId = undefined;
759
+ return this;
760
+ }
555
761
  }
556
762
  // ---------------------------------------------------------------------------
557
763
  // Sub-process content builder
@@ -570,6 +776,11 @@ export class SubProcessContentBuilder {
570
776
  lastNodeId;
571
777
  currentGatewayId;
572
778
  openBranchEnds = [];
779
+ rootMessages;
780
+ /** @internal */
781
+ constructor(rootMessages = []) {
782
+ this.rootMessages = rootMessages;
783
+ }
573
784
  addElement(element) {
574
785
  if (this._elements.some((n) => n.id === element.id)) {
575
786
  throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
@@ -600,8 +811,11 @@ export class SubProcessContentBuilder {
600
811
  // ---- Events ----
601
812
  startEvent(id, options) {
602
813
  const el = makeFlowElement(id ?? generateId("StartEvent"), "startEvent", options);
603
- if (el.type === "startEvent" && options)
814
+ if (el.type === "startEvent" && options) {
604
815
  el.eventDefinitions = buildEventDefinitions(options);
816
+ if (options.isInterrupting === false)
817
+ el.isInterrupting = false;
818
+ }
605
819
  return this.addElement(el);
606
820
  }
607
821
  endEvent(id, options) {
@@ -639,10 +853,27 @@ export class SubProcessContentBuilder {
639
853
  return this.addElement(makeCallActivityEl(id, options));
640
854
  }
641
855
  sendTask(id, options) {
642
- return this.addElement(makeFlowElement(id, "sendTask", options));
856
+ const el = makeFlowElement(id, "sendTask", options);
857
+ if (options?.isForCompensation)
858
+ el.isForCompensation = true;
859
+ if (options?.messageName)
860
+ el.messageRef = resolveMessage(options.messageName, this.rootMessages);
861
+ return this.addElement(el);
643
862
  }
644
863
  receiveTask(id, options) {
645
- return this.addElement(makeFlowElement(id, "receiveTask", options));
864
+ const el = makeFlowElement(id, "receiveTask", options);
865
+ if (options?.isForCompensation)
866
+ el.isForCompensation = true;
867
+ if (options?.messageName)
868
+ el.messageRef = resolveMessage(options.messageName, this.rootMessages);
869
+ return this.addElement(el);
870
+ }
871
+ /** Add an abstract task with no Zeebe extensions. */
872
+ task(id, options) {
873
+ const el = makeFlowElement(id, "task", options);
874
+ if (options?.isForCompensation)
875
+ el.isForCompensation = true;
876
+ return this.addElement(el);
646
877
  }
647
878
  // ---- Gateways ----
648
879
  exclusiveGateway(id, options) {
@@ -709,10 +940,16 @@ export class SubProcessContentBuilder {
709
940
  gw.default = b._defaultFlowId;
710
941
  }
711
942
  }
712
- if (!b._connected && b._elements.length > 0) {
713
- const lastEl = b._elements[b._elements.length - 1];
714
- if (lastEl && lastEl.type !== "endEvent") {
715
- this.openBranchEnds.push(b._lastNodeId);
943
+ if (!b._connected) {
944
+ const allEnds = [
945
+ ...(b._lastNodeId !== undefined ? [b._lastNodeId] : []),
946
+ ...b._openBranchEnds,
947
+ ];
948
+ for (const endId of allEnds) {
949
+ const endEl = this._elements.find((n) => n.id === endId);
950
+ if (endEl && endEl.type !== "endEvent") {
951
+ this.openBranchEnds.push(endId);
952
+ }
716
953
  }
717
954
  }
718
955
  this.lastNodeId = undefined;
@@ -753,6 +990,8 @@ export class ProcessBuilder {
753
990
  sequenceFlows = [];
754
991
  rootErrors = [];
755
992
  rootMessages = [];
993
+ rootSignals = [];
994
+ rootEscalations = [];
756
995
  _textAnnotations = [];
757
996
  _associations = [];
758
997
  _annCounters = new Map();
@@ -762,6 +1001,7 @@ export class ProcessBuilder {
762
1001
  _autoLayout = false;
763
1002
  _executionPlatformVersion = "8.9.0";
764
1003
  _serviceTaskDefaults = {};
1004
+ _savedMainFlowId = undefined;
765
1005
  constructor(processId) {
766
1006
  this.processId = processId;
767
1007
  }
@@ -811,7 +1051,9 @@ export class ProcessBuilder {
811
1051
  extensionElements: extElements,
812
1052
  });
813
1053
  if (element.type === "startEvent" && options) {
814
- element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages);
1054
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
1055
+ if (options.isInterrupting === false)
1056
+ element.isInterrupting = false;
815
1057
  }
816
1058
  if (options?.modelerTemplate) {
817
1059
  element.unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
@@ -850,7 +1092,7 @@ export class ProcessBuilder {
850
1092
  const nodeId = id ?? generateId("EndEvent");
851
1093
  const element = makeFlowElement(nodeId, "endEvent", options);
852
1094
  if (element.type === "endEvent" && options) {
853
- element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages);
1095
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
854
1096
  }
855
1097
  this.addFlowElement(element);
856
1098
  return this;
@@ -860,7 +1102,7 @@ export class ProcessBuilder {
860
1102
  const nodeId = id ?? generateId("IntermediateThrowEvent");
861
1103
  const element = makeFlowElement(nodeId, "intermediateThrowEvent", options);
862
1104
  if (element.type === "intermediateThrowEvent" && options) {
863
- element.eventDefinitions = buildEventDefinitions(options);
1105
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
864
1106
  }
865
1107
  this.addFlowElement(element);
866
1108
  return this;
@@ -870,7 +1112,7 @@ export class ProcessBuilder {
870
1112
  const nodeId = id ?? generateId("IntermediateCatchEvent");
871
1113
  const element = makeFlowElement(nodeId, "intermediateCatchEvent", options);
872
1114
  if (element.type === "intermediateCatchEvent" && options) {
873
- element.eventDefinitions = buildEventDefinitions(options);
1115
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
874
1116
  }
875
1117
  this.addFlowElement(element);
876
1118
  return this;
@@ -886,12 +1128,17 @@ export class ProcessBuilder {
886
1128
  if (element.type === "boundaryEvent") {
887
1129
  element.attachedToRef = options.attachedTo;
888
1130
  element.cancelActivity = options.cancelActivity;
889
- element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages);
1131
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
890
1132
  }
891
1133
  // Boundary events never auto-connect — temporarily clear lastNodeId
892
1134
  const prevLast = this.lastNodeId;
893
1135
  this.lastNodeId = undefined;
894
1136
  this.addFlowElement(element);
1137
+ // For compensation boundary events, save the main-flow cursor AFTER addFlowElement
1138
+ // so subsequent normal elements don't accidentally clear it before it's consumed.
1139
+ if (options.compensation) {
1140
+ this._savedMainFlowId = prevLast;
1141
+ }
895
1142
  // Don't restore prevLast — the builder now chains from the boundary event
896
1143
  void prevLast;
897
1144
  return this;
@@ -921,6 +1168,7 @@ export class ProcessBuilder {
921
1168
  this.lastNodeId = savedLast;
922
1169
  this.currentGatewayId = savedGateway;
923
1170
  this.openBranchEnds = savedOpenEnds;
1171
+ this._savedMainFlowId = undefined;
924
1172
  return this;
925
1173
  }
926
1174
  // ---- Tasks ----
@@ -971,12 +1219,22 @@ export class ProcessBuilder {
971
1219
  }
972
1220
  /** Add a send task (aspirational). */
973
1221
  sendTask(id, options) {
974
- this.addFlowElement(makeFlowElement(id, "sendTask", options));
1222
+ const el = makeFlowElement(id, "sendTask", options);
1223
+ if (options?.isForCompensation)
1224
+ el.isForCompensation = true;
1225
+ if (options?.messageName)
1226
+ el.messageRef = resolveMessage(options.messageName, this.rootMessages);
1227
+ this.addFlowElement(el);
975
1228
  return this;
976
1229
  }
977
1230
  /** Add a receive task (aspirational). */
978
1231
  receiveTask(id, options) {
979
- this.addFlowElement(makeFlowElement(id, "receiveTask", options));
1232
+ const el = makeFlowElement(id, "receiveTask", options);
1233
+ if (options?.isForCompensation)
1234
+ el.isForCompensation = true;
1235
+ if (options?.messageName)
1236
+ el.messageRef = resolveMessage(options.messageName, this.rootMessages);
1237
+ this.addFlowElement(el);
980
1238
  return this;
981
1239
  }
982
1240
  /** Add a business rule task. */
@@ -989,6 +1247,14 @@ export class ProcessBuilder {
989
1247
  this.addFlowElement(makeCallActivityEl(id, options));
990
1248
  return this;
991
1249
  }
1250
+ /** Add an abstract task with no Zeebe extensions. */
1251
+ task(id, options) {
1252
+ const el = makeFlowElement(id, "task", options);
1253
+ if (options?.isForCompensation)
1254
+ el.isForCompensation = true;
1255
+ this.addFlowElement(el);
1256
+ return this;
1257
+ }
992
1258
  // ---- Gateways ----
993
1259
  /** Add an exclusive gateway (XOR split/join). */
994
1260
  exclusiveGateway(id, options) {
@@ -1036,7 +1302,7 @@ export class ProcessBuilder {
1036
1302
  if (!this.currentGatewayId) {
1037
1303
  throw new Error("branch() must be called after a gateway element");
1038
1304
  }
1039
- const b = new BranchBuilder(this.currentGatewayId, name);
1305
+ const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
1040
1306
  callback(b);
1041
1307
  for (const el of b._elements) {
1042
1308
  if (this.flowElements.some((n) => n.id === el.id)) {
@@ -1060,10 +1326,16 @@ export class ProcessBuilder {
1060
1326
  }
1061
1327
  // Track the branch's open end so the next element auto-connects from it.
1062
1328
  // Skip branches that terminated at an end event (those are intentional dead-ends).
1063
- if (!b._connected && b._elements.length > 0) {
1064
- const lastEl = b._elements[b._elements.length - 1];
1065
- if (lastEl && lastEl.type !== "endEvent") {
1066
- this.openBranchEnds.push(b._lastNodeId);
1329
+ if (!b._connected) {
1330
+ const allEnds = [
1331
+ ...(b._lastNodeId !== undefined ? [b._lastNodeId] : []),
1332
+ ...b._openBranchEnds,
1333
+ ];
1334
+ for (const endId of allEnds) {
1335
+ const endEl = this.flowElements.find((n) => n.id === endId);
1336
+ if (endEl && endEl.type !== "endEvent") {
1337
+ this.openBranchEnds.push(endId);
1338
+ }
1067
1339
  }
1068
1340
  }
1069
1341
  this.lastNodeId = undefined;
@@ -1102,7 +1374,7 @@ export class ProcessBuilder {
1102
1374
  // ---- Sub-processes ----
1103
1375
  /** Add an ad-hoc sub-process with optional AI agent or multi-instance configuration. */
1104
1376
  adHocSubProcess(id, content, options) {
1105
- const sub = new SubProcessContentBuilder();
1377
+ const sub = new SubProcessContentBuilder(this.rootMessages);
1106
1378
  content(sub);
1107
1379
  insertJoinGateways(sub._elements, sub._flows);
1108
1380
  recomputeIncomingOutgoing(sub._elements, sub._flows);
@@ -1170,7 +1442,7 @@ export class ProcessBuilder {
1170
1442
  }
1171
1443
  /** Add a sub-process (aspirational). */
1172
1444
  subProcess(id, content, options) {
1173
- const sub = new SubProcessContentBuilder();
1445
+ const sub = new SubProcessContentBuilder(this.rootMessages);
1174
1446
  content(sub);
1175
1447
  insertJoinGateways(sub._elements, sub._flows);
1176
1448
  recomputeIncomingOutgoing(sub._elements, sub._flows);
@@ -1187,20 +1459,29 @@ export class ProcessBuilder {
1187
1459
  this.addFlowElement(element);
1188
1460
  return this;
1189
1461
  }
1190
- /** Add an event sub-process (aspirational). */
1462
+ /** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
1191
1463
  eventSubProcess(id, content, options) {
1192
- const sub = new SubProcessContentBuilder();
1464
+ const sub = new SubProcessContentBuilder(this.rootMessages);
1193
1465
  content(sub);
1194
1466
  insertJoinGateways(sub._elements, sub._flows);
1195
1467
  recomputeIncomingOutgoing(sub._elements, sub._flows);
1196
- const element = makeFlowElement(id, "eventSubProcess", options);
1197
- if (element.type === "eventSubProcess") {
1468
+ const element = makeFlowElement(id, "subProcess", options);
1469
+ if (element.type === "subProcess") {
1470
+ element.triggeredByEvent = true;
1198
1471
  element.flowElements = sub._elements;
1199
1472
  element.sequenceFlows = sub._flows;
1200
1473
  element.textAnnotations = sub._textAnnotations;
1201
1474
  element.associations = sub._associations;
1202
1475
  }
1203
- this.addFlowElement(element);
1476
+ // Event sub-processes have no incoming/outgoing sequence flows and must not
1477
+ // advance the flow cursor — the surrounding process wires around them.
1478
+ // openBranchEnds is intentionally NOT drained here; the next normal
1479
+ // addFlowElement call will drain it and connect branch ends to that element.
1480
+ if (this.flowElements.some((n) => n.id === element.id)) {
1481
+ throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
1482
+ }
1483
+ this._savedMainFlowId = undefined;
1484
+ this.flowElements.push(element);
1204
1485
  return this;
1205
1486
  }
1206
1487
  // ---- Annotations ----
@@ -1283,9 +1564,9 @@ export class ProcessBuilder {
1283
1564
  "modeler:executionPlatformVersion": this._executionPlatformVersion,
1284
1565
  },
1285
1566
  errors: this.rootErrors,
1286
- escalations: [],
1567
+ escalations: this.rootEscalations,
1287
1568
  messages: this.rootMessages,
1288
- signals: [],
1569
+ signals: this.rootSignals,
1289
1570
  collaborations: [],
1290
1571
  processes: [process],
1291
1572
  // Seed diagram stub so applyAutoLayout preserves process-specific IDs.
@@ -1323,6 +1604,24 @@ export class ProcessBuilder {
1323
1604
  throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
1324
1605
  }
1325
1606
  this.flowElements.push(element);
1607
+ // Compensation handlers are outside the normal token flow: link via association
1608
+ // from the preceding compensation boundary event, then restore the main-flow cursor.
1609
+ if (element.isForCompensation) {
1610
+ if (this.lastNodeId) {
1611
+ this._associations.push({
1612
+ id: generateId("Association"),
1613
+ sourceRef: this.lastNodeId,
1614
+ targetRef: element.id,
1615
+ associationDirection: "One",
1616
+ unknownAttributes: {},
1617
+ });
1618
+ }
1619
+ // Restore main-flow cursor (saved by boundaryEvent() when compensation: true)
1620
+ this.lastNodeId = this._savedMainFlowId;
1621
+ this._savedMainFlowId = undefined;
1622
+ // Do NOT connect open branch ends — handler is outside normal flow
1623
+ return;
1624
+ }
1326
1625
  if (this.lastNodeId) {
1327
1626
  const flowId = generateId("Flow");
1328
1627
  this.sequenceFlows.push({
@@ -1345,6 +1644,9 @@ export class ProcessBuilder {
1345
1644
  });
1346
1645
  }
1347
1646
  this.openBranchEnds = [];
1647
+ // Clear any saved compensation cursor — a normal element advancing the cursor
1648
+ // means the compensation boundary/handler pattern has been interrupted.
1649
+ this._savedMainFlowId = undefined;
1348
1650
  this.lastNodeId = element.id;
1349
1651
  }
1350
1652
  }
@@ -105,10 +105,12 @@ interface BpmnFlowNodeBase {
105
105
  documentation?: string;
106
106
  extensionElements: XmlElement[];
107
107
  unknownAttributes: Record<string, string>;
108
+ isForCompensation?: boolean;
108
109
  }
109
110
  export interface BpmnStartEvent extends BpmnFlowNodeBase {
110
111
  type: "startEvent";
111
112
  eventDefinitions: BpmnEventDefinition[];
113
+ isInterrupting?: boolean;
112
114
  }
113
115
  export interface BpmnEndEvent extends BpmnFlowNodeBase {
114
116
  type: "endEvent";
@@ -150,10 +152,12 @@ export interface BpmnCallActivity extends BpmnFlowNodeBase {
150
152
  }
151
153
  export interface BpmnSendTask extends BpmnFlowNodeBase {
152
154
  type: "sendTask";
155
+ messageRef?: string;
153
156
  loopCharacteristics?: BpmnMultiInstanceLoopCharacteristics;
154
157
  }
155
158
  export interface BpmnReceiveTask extends BpmnFlowNodeBase {
156
159
  type: "receiveTask";
160
+ messageRef?: string;
157
161
  loopCharacteristics?: BpmnMultiInstanceLoopCharacteristics;
158
162
  }
159
163
  export interface BpmnAdHocSubProcess extends BpmnFlowNodeBase {
@@ -42,6 +42,7 @@ const KNOWN_ATTRS = new Set([
42
42
  "default",
43
43
  "attachedToRef",
44
44
  "cancelActivity",
45
+ "isForCompensation",
45
46
  "sourceRef",
46
47
  "targetRef",
47
48
  "associationDirection",
@@ -275,7 +276,15 @@ function parseFlowElement(element) {
275
276
  unknownAttributes: unknownAttrs(element),
276
277
  };
277
278
  switch (ln) {
278
- case "startEvent":
279
+ case "startEvent": {
280
+ const isInterruptingAttr = attr(element, "isInterrupting");
281
+ return {
282
+ ...base,
283
+ type: "startEvent",
284
+ eventDefinitions: parseEventDefinitions(element),
285
+ ...(isInterruptingAttr === "false" ? { isInterrupting: false } : {}),
286
+ };
287
+ }
279
288
  case "endEvent":
280
289
  case "intermediateCatchEvent":
281
290
  case "intermediateThrowEvent":
@@ -294,12 +303,24 @@ function parseFlowElement(element) {
294
303
  case "serviceTask":
295
304
  case "scriptTask":
296
305
  case "userTask":
297
- case "sendTask":
298
- case "receiveTask":
299
306
  case "businessRuleTask":
300
307
  case "manualTask":
301
308
  case "callActivity":
302
- return { ...base, type: ln, loopCharacteristics: parseLoopCharacteristics(element) };
309
+ return {
310
+ ...base,
311
+ type: ln,
312
+ loopCharacteristics: parseLoopCharacteristics(element),
313
+ isForCompensation: attr(element, "isForCompensation") === "true" ? true : undefined,
314
+ };
315
+ case "sendTask":
316
+ case "receiveTask":
317
+ return {
318
+ ...base,
319
+ type: ln,
320
+ messageRef: attr(element, "messageRef"),
321
+ loopCharacteristics: parseLoopCharacteristics(element),
322
+ isForCompensation: attr(element, "isForCompensation") === "true" ? true : undefined,
323
+ };
303
324
  case "adHocSubProcess":
304
325
  return {
305
326
  ...base,
@@ -157,6 +157,8 @@ function serializeFlowElement(fe, ns) {
157
157
  const attrs = { id: fe.id, ...fe.unknownAttributes };
158
158
  if (fe.name !== undefined)
159
159
  attrs.name = fe.name;
160
+ if (fe.isForCompensation)
161
+ attrs.isForCompensation = "true";
160
162
  const children = [];
161
163
  // Documentation
162
164
  if (fe.documentation !== undefined) {
@@ -169,6 +171,10 @@ function serializeFlowElement(fe, ns) {
169
171
  children.push(...flowRefs(fe.outgoing, "outgoing", bp));
170
172
  switch (fe.type) {
171
173
  case "startEvent":
174
+ if (fe.isInterrupting === false)
175
+ attrs.isInterrupting = "false";
176
+ children.push(...serializeEventDefinitions(fe.eventDefinitions, bp));
177
+ break;
172
178
  case "endEvent":
173
179
  case "intermediateCatchEvent":
174
180
  case "intermediateThrowEvent":
@@ -184,13 +190,17 @@ function serializeFlowElement(fe, ns) {
184
190
  case "serviceTask":
185
191
  case "scriptTask":
186
192
  case "userTask":
187
- case "sendTask":
188
- case "receiveTask":
189
193
  case "businessRuleTask":
190
194
  case "manualTask":
191
195
  case "callActivity":
192
196
  children.push(...serializeLoopCharacteristics(fe.loopCharacteristics, bp));
193
197
  break;
198
+ case "sendTask":
199
+ case "receiveTask":
200
+ if (fe.messageRef)
201
+ attrs.messageRef = fe.messageRef;
202
+ children.push(...serializeLoopCharacteristics(fe.loopCharacteristics, bp));
203
+ break;
194
204
  case "adHocSubProcess":
195
205
  children.push(...serializeLoopCharacteristics(fe.loopCharacteristics, bp));
196
206
  children.push(...serializeProcessContents(fe, ns));
@@ -36,6 +36,7 @@ function repositionBoundaryEvents(flowElements, result) {
36
36
  ps.add(edge.sourceRef);
37
37
  predIds.set(edge.targetRef, ps);
38
38
  }
39
+ const allChainNodes = new Set();
39
40
  for (const [hostId, beIds] of boundaryMap) {
40
41
  const hostNode = nodeById.get(hostId);
41
42
  if (!hostNode)
@@ -72,6 +73,9 @@ function repositionBoundaryEvents(flowElements, result) {
72
73
  queue.push(...(succIds.get(id) ?? []));
73
74
  }
74
75
  }
76
+ // Record all chain members so the forward pass can identify them.
77
+ for (const cid of chainSet)
78
+ allChainNodes.add(cid);
75
79
  // Each boundary event's chain gets its own vertical lane
76
80
  let maxChainH = 0;
77
81
  for (const id of chainOrder) {
@@ -133,6 +137,116 @@ function repositionBoundaryEvents(flowElements, result) {
133
137
  }
134
138
  }
135
139
  }
140
+ // Forward-placement pass: any node not in any chain but whose predecessor
141
+ // has been relocated further right must be pushed rightward.
142
+ // Process in topological order (Kahn's algorithm over the sequenceFlow graph).
143
+ const inDegree = new Map();
144
+ for (const id of nodeById.keys()) {
145
+ inDegree.set(id, (predIds.get(id) ?? new Set()).size);
146
+ }
147
+ const topoQueue = [];
148
+ for (const [id, deg] of inDegree) {
149
+ if (deg === 0)
150
+ topoQueue.push(id);
151
+ }
152
+ const topoOrder = [];
153
+ while (topoQueue.length > 0) {
154
+ const id = topoQueue.shift();
155
+ if (!id)
156
+ break;
157
+ topoOrder.push(id);
158
+ for (const succId of succIds.get(id) ?? []) {
159
+ const newDeg = (inDegree.get(succId) ?? 1) - 1;
160
+ inDegree.set(succId, newDeg);
161
+ if (newDeg === 0)
162
+ topoQueue.push(succId);
163
+ }
164
+ }
165
+ const movedInPass = new Set();
166
+ for (const id of topoOrder) {
167
+ if (allChainNodes.has(id))
168
+ continue;
169
+ const node = nodeById.get(id);
170
+ if (!node)
171
+ continue;
172
+ const preds = predIds.get(id) ?? new Set();
173
+ if (preds.size === 0)
174
+ continue;
175
+ let maxPredRight = 0;
176
+ for (const predId of preds) {
177
+ const pred = nodeById.get(predId);
178
+ if (pred)
179
+ maxPredRight = Math.max(maxPredRight, pred.bounds.x + pred.bounds.width);
180
+ }
181
+ const minX = maxPredRight + CHAIN_GAP;
182
+ if (minX > node.bounds.x) {
183
+ const delta = minX - node.bounds.x;
184
+ node.bounds.x = minX;
185
+ if (node.labelBounds)
186
+ node.labelBounds.x += delta;
187
+ movedInPass.add(id);
188
+ }
189
+ }
190
+ // Spatial bump: if a moved node now overlaps a non-chain node on a parallel
191
+ // path (no predecessor/successor relationship), push it clear and cascade.
192
+ let bumped = true;
193
+ while (bumped) {
194
+ bumped = false;
195
+ for (const movedId of movedInPass) {
196
+ const moved = nodeById.get(movedId);
197
+ if (!moved)
198
+ continue;
199
+ const movedRight = moved.bounds.x + moved.bounds.width;
200
+ for (const [otherId, other] of nodeById) {
201
+ if (otherId === movedId)
202
+ continue;
203
+ if (allChainNodes.has(otherId))
204
+ continue;
205
+ if (movedInPass.has(otherId))
206
+ continue;
207
+ // Check y overlap
208
+ if (other.bounds.y + other.bounds.height <= moved.bounds.y)
209
+ continue;
210
+ if (other.bounds.y >= moved.bounds.y + moved.bounds.height)
211
+ continue;
212
+ // Check x overlap (moved node intrudes into other's space)
213
+ if (other.bounds.x >= movedRight)
214
+ continue;
215
+ if (other.bounds.x + other.bounds.width <= moved.bounds.x)
216
+ continue;
217
+ // Push other right of moved
218
+ const newX = movedRight + CHAIN_GAP;
219
+ if (newX > other.bounds.x) {
220
+ const delta = newX - other.bounds.x;
221
+ other.bounds.x = newX;
222
+ if (other.labelBounds)
223
+ other.labelBounds.x += delta;
224
+ movedInPass.add(otherId);
225
+ bumped = true;
226
+ }
227
+ }
228
+ }
229
+ }
230
+ // Re-route edges where a chain source now points at a moved target,
231
+ // or where the source itself was moved by the forward pass.
232
+ for (const edge of result.edges) {
233
+ const srcMoved = movedInPass.has(edge.sourceRef);
234
+ const tgtMoved = movedInPass.has(edge.targetRef);
235
+ if (!srcMoved && !tgtMoved)
236
+ continue;
237
+ const src = nodeById.get(edge.sourceRef);
238
+ const tgt = nodeById.get(edge.targetRef);
239
+ if (!src || !tgt)
240
+ continue;
241
+ const srcX = Math.round(src.bounds.x + src.bounds.width);
242
+ const srcY = Math.round(src.bounds.y + src.bounds.height / 2);
243
+ const tgtX = Math.round(tgt.bounds.x);
244
+ const tgtY = Math.round(tgt.bounds.y + tgt.bounds.height / 2);
245
+ edge.waypoints = [
246
+ { x: srcX, y: srcY },
247
+ { x: tgtX, y: tgtY },
248
+ ];
249
+ }
136
250
  }
137
251
  /**
138
252
  * Auto-layout a BPMN process using the Sugiyama/layered algorithm.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/core",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",