@bpmnkit/core 0.0.23 → 0.0.25

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,11 +1,13 @@
1
- import { layoutProcess } from "../layout/layout-engine.js";
2
1
  import { generateId } from "../types/id-generator.js";
2
+ import { applyAutoLayout } from "./auto-layout.js";
3
3
  import { restConnectorRetries, restConnectorTaskType, restConnectorToIoMappingInputs, restConnectorToTaskHeaders, } from "./rest-connector.js";
4
4
  import { zeebeExtensionsToXmlElements } from "./zeebe-extensions.js";
5
+ // Keep in sync with packages/core/package.json version
6
+ const EXPORTER_VERSION = "0.0.23";
5
7
  // ---------------------------------------------------------------------------
6
8
  // Internal helpers
7
9
  // ---------------------------------------------------------------------------
8
- function buildEventDefinitions(opts, rootErrors, rootMessages) {
10
+ function buildEventDefinitions(opts, rootErrors, rootMessages, rootSignals, rootEscalations) {
9
11
  const defs = [];
10
12
  if (opts.timerDuration || opts.timerDate || opts.timerCycle) {
11
13
  defs.push({
@@ -16,28 +18,63 @@ function buildEventDefinitions(opts, rootErrors, rootMessages) {
16
18
  });
17
19
  }
18
20
  if (opts.errorCode !== undefined || opts.errorRef !== undefined) {
19
- let errorRef = opts.errorRef;
20
- if (!errorRef && opts.errorCode !== undefined && rootErrors) {
21
- const errorId = generateId("Error");
22
- rootErrors.push({ id: errorId, name: opts.errorCode, errorCode: opts.errorCode });
23
- errorRef = errorId;
21
+ const codeOrRef = opts.errorCode ?? opts.errorRef;
22
+ let errorRef;
23
+ if (codeOrRef !== undefined && rootErrors) {
24
+ let existing = rootErrors.find((e) => e.errorCode === codeOrRef || e.name === codeOrRef);
25
+ if (!existing) {
26
+ existing = { id: generateId("Error"), name: codeOrRef, errorCode: codeOrRef };
27
+ rootErrors.push(existing);
28
+ }
29
+ errorRef = existing.id;
30
+ }
31
+ else {
32
+ errorRef = codeOrRef;
24
33
  }
25
34
  defs.push({ type: "error", errorRef });
26
35
  }
27
36
  if (opts.messageName !== undefined) {
28
37
  let messageRef = opts.messageName;
29
38
  if (rootMessages) {
30
- const messageId = generateId("Message");
31
- rootMessages.push({ id: messageId, name: opts.messageName, unknownAttributes: {} });
32
- messageRef = messageId;
39
+ let existing = rootMessages.find((m) => m.name === opts.messageName);
40
+ if (!existing) {
41
+ existing = { id: generateId("Message"), name: opts.messageName, unknownAttributes: {} };
42
+ rootMessages.push(existing);
43
+ }
44
+ messageRef = existing.id;
33
45
  }
34
46
  defs.push({ type: "message", messageRef });
35
47
  }
36
48
  if (opts.signalName !== undefined) {
37
- defs.push({ type: "signal", signalRef: opts.signalName });
49
+ let signalRef = opts.signalName;
50
+ if (rootSignals) {
51
+ let existing = rootSignals.find((s) => s.name === opts.signalName);
52
+ if (!existing) {
53
+ existing = { id: generateId("Signal"), name: opts.signalName };
54
+ rootSignals.push(existing);
55
+ }
56
+ signalRef = existing.id;
57
+ }
58
+ defs.push({ type: "signal", signalRef });
38
59
  }
39
60
  if (opts.escalationCode !== undefined) {
40
- defs.push({ type: "escalation", escalationRef: opts.escalationCode });
61
+ let escalationRef = opts.escalationCode;
62
+ if (rootEscalations) {
63
+ let existing = rootEscalations.find((e) => e.escalationCode === opts.escalationCode);
64
+ if (!existing) {
65
+ existing = {
66
+ id: generateId("Escalation"),
67
+ name: opts.escalationCode,
68
+ escalationCode: opts.escalationCode,
69
+ };
70
+ rootEscalations.push(existing);
71
+ }
72
+ escalationRef = existing.id;
73
+ }
74
+ defs.push({ type: "escalation", escalationRef });
75
+ }
76
+ if (opts.compensation) {
77
+ defs.push({ type: "compensate", activityRef: opts.activityRef });
41
78
  }
42
79
  return defs;
43
80
  }
@@ -155,46 +192,6 @@ function buildAdHocLoopCharacteristics(lc) {
155
192
  ],
156
193
  };
157
194
  }
158
- /** Convert a layout engine result into a BPMN diagram interchange structure. */
159
- function layoutResultToDiagram(processId, layout) {
160
- const shapes = layout.nodes.map((node) => {
161
- const shape = {
162
- id: `${node.id}_di`,
163
- bpmnElement: node.id,
164
- bounds: { ...node.bounds },
165
- unknownAttributes: {},
166
- };
167
- if (node.isExpanded !== undefined) {
168
- shape.isExpanded = node.isExpanded;
169
- }
170
- if (node.labelBounds) {
171
- shape.label = { bounds: { ...node.labelBounds } };
172
- }
173
- return shape;
174
- });
175
- const edges = layout.edges.map((edge) => {
176
- const diEdge = {
177
- id: `${edge.id}_di`,
178
- bpmnElement: edge.id,
179
- waypoints: edge.waypoints.map((wp) => ({ ...wp })),
180
- unknownAttributes: {},
181
- };
182
- if (edge.labelBounds) {
183
- diEdge.label = { bounds: { ...edge.labelBounds } };
184
- }
185
- return diEdge;
186
- });
187
- const plane = {
188
- id: `${processId}_di_plane`,
189
- bpmnElement: processId,
190
- shapes,
191
- edges,
192
- };
193
- return {
194
- id: `${processId}_di`,
195
- plane,
196
- };
197
- }
198
195
  function recomputeIncomingOutgoing(elements, flows) {
199
196
  for (const el of elements) {
200
197
  el.incoming = [];
@@ -236,6 +233,200 @@ function makeConditionExpression(expression) {
236
233
  };
237
234
  }
238
235
  // ---------------------------------------------------------------------------
236
+ // Element factory functions — shared by all three builder classes
237
+ // ---------------------------------------------------------------------------
238
+ function makeServiceTaskEl(id, options) {
239
+ const unknownAttributes = {};
240
+ if (options.modelerTemplate)
241
+ unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
242
+ if (options.modelerTemplateVersion)
243
+ unknownAttributes["zeebe:modelerTemplateVersion"] = options.modelerTemplateVersion;
244
+ if (options.modelerTemplateIcon)
245
+ unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
246
+ const el = makeFlowElement(id, "serviceTask", {
247
+ name: options.name,
248
+ extensionElements: buildServiceTaskExtensions(options),
249
+ });
250
+ el.unknownAttributes = unknownAttributes;
251
+ if (options.isForCompensation)
252
+ el.isForCompensation = true;
253
+ return el;
254
+ }
255
+ function makeScriptTaskEl(id, options) {
256
+ const el = makeFlowElement(id, "scriptTask", {
257
+ name: options.name,
258
+ extensionElements: zeebeExtensionsToXmlElements({
259
+ unknownElements: [
260
+ {
261
+ name: "zeebe:script",
262
+ attributes: { expression: options.expression, resultVariable: options.resultVariable },
263
+ children: [],
264
+ },
265
+ ],
266
+ }),
267
+ });
268
+ if (options.isForCompensation)
269
+ el.isForCompensation = true;
270
+ return el;
271
+ }
272
+ function makeUserTaskEl(id, options) {
273
+ const ext = zeebeExtensionsToXmlElements({
274
+ ...(options?.zeebeUserTask ? { userTask: true } : {}),
275
+ ...(options?.formId ? { formDefinition: { formId: options.formId } } : {}),
276
+ });
277
+ const el = makeFlowElement(id, "userTask", { name: options?.name, extensionElements: ext });
278
+ if (options?.isForCompensation)
279
+ el.isForCompensation = true;
280
+ return el;
281
+ }
282
+ function makeBusinessRuleTaskEl(id, options) {
283
+ const ext = [];
284
+ if (options?.taskType) {
285
+ ext.push(...zeebeExtensionsToXmlElements({ taskDefinition: { type: options.taskType } }));
286
+ }
287
+ if (options?.decisionId) {
288
+ ext.push(...zeebeExtensionsToXmlElements({
289
+ calledDecision: {
290
+ decisionId: options.decisionId,
291
+ resultVariable: options.resultVariable ?? "result",
292
+ },
293
+ }));
294
+ }
295
+ const el = makeFlowElement(id, "businessRuleTask", {
296
+ name: options?.name,
297
+ extensionElements: ext,
298
+ });
299
+ if (options?.isForCompensation)
300
+ el.isForCompensation = true;
301
+ return el;
302
+ }
303
+ function makeCallActivityEl(id, options) {
304
+ const attrs = { processId: options.processId };
305
+ if (options.propagateAllChildVariables !== undefined) {
306
+ attrs.propagateAllChildVariables = String(options.propagateAllChildVariables);
307
+ }
308
+ const el = makeFlowElement(id, "callActivity", {
309
+ name: options.name,
310
+ extensionElements: zeebeExtensionsToXmlElements({
311
+ unknownElements: [{ name: "zeebe:calledElement", attributes: attrs, children: [] }],
312
+ }),
313
+ });
314
+ if (options.isForCompensation)
315
+ el.isForCompensation = true;
316
+ return el;
317
+ }
318
+ function makeExclusiveGatewayEl(id, options) {
319
+ const el = makeFlowElement(id, "exclusiveGateway", options);
320
+ if (options?.defaultFlow && el.type === "exclusiveGateway") {
321
+ ;
322
+ el.default = options.defaultFlow;
323
+ }
324
+ return el;
325
+ }
326
+ function makeInclusiveGatewayEl(id, options) {
327
+ const el = makeFlowElement(id, "inclusiveGateway", options);
328
+ if (options?.defaultFlow && el.type === "inclusiveGateway") {
329
+ ;
330
+ el.default = options.defaultFlow;
331
+ }
332
+ return el;
333
+ }
334
+ // ---------------------------------------------------------------------------
335
+ // Shared graph helpers — used by ProcessBuilder and SubProcessContentBuilder
336
+ // ---------------------------------------------------------------------------
337
+ function insertJoinGateways(elements, flows) {
338
+ const GATEWAY_TYPES = new Set([
339
+ "exclusiveGateway",
340
+ "parallelGateway",
341
+ "inclusiveGateway",
342
+ "eventBasedGateway",
343
+ ]);
344
+ const elementTypes = new Map();
345
+ for (const el of elements)
346
+ elementTypes.set(el.id, el.type);
347
+ const outCount = new Map();
348
+ for (const flow of flows) {
349
+ outCount.set(flow.sourceRef, (outCount.get(flow.sourceRef) ?? 0) + 1);
350
+ }
351
+ const splitGateways = new Set();
352
+ for (const [id, count] of outCount) {
353
+ const type = elementTypes.get(id);
354
+ if (type && GATEWAY_TYPES.has(type) && count >= 2)
355
+ splitGateways.add(id);
356
+ }
357
+ if (splitGateways.size === 0)
358
+ return;
359
+ const incoming = new Map();
360
+ for (const flow of flows) {
361
+ const arr = incoming.get(flow.targetRef);
362
+ if (arr)
363
+ arr.push(flow);
364
+ else
365
+ incoming.set(flow.targetRef, [flow]);
366
+ }
367
+ for (const [targetId, inFlows] of incoming) {
368
+ if (inFlows.length < 2)
369
+ continue;
370
+ const splitToFlows = new Map();
371
+ for (const flow of inFlows) {
372
+ const split = traceBackToSplit(flow.sourceRef, splitGateways, flows);
373
+ if (split) {
374
+ const arr = splitToFlows.get(split);
375
+ if (arr)
376
+ arr.push(flow);
377
+ else
378
+ splitToFlows.set(split, [flow]);
379
+ }
380
+ }
381
+ for (const [splitId, convergingFlows] of splitToFlows) {
382
+ if (convergingFlows.length < 2)
383
+ continue;
384
+ const gwType = elementTypes.get(splitId);
385
+ if (!gwType)
386
+ continue;
387
+ // eventBasedGateway is split-only; converge through an XOR join instead
388
+ const joinType = gwType === "eventBasedGateway" ? "exclusiveGateway" : gwType;
389
+ const targetType = elementTypes.get(targetId);
390
+ if (targetType === joinType)
391
+ continue;
392
+ const joinId = `${splitId}_join`;
393
+ if (elementTypes.has(joinId))
394
+ continue;
395
+ const joinElement = makeFlowElement(joinId, joinType, {});
396
+ elements.push(joinElement);
397
+ elementTypes.set(joinId, joinType);
398
+ for (const flow of convergingFlows)
399
+ flow.targetRef = joinId;
400
+ flows.push({
401
+ id: generateId("Flow"),
402
+ sourceRef: joinId,
403
+ targetRef: targetId,
404
+ extensionElements: [],
405
+ unknownAttributes: {},
406
+ });
407
+ }
408
+ }
409
+ }
410
+ function traceBackToSplit(nodeId, splitGateways, flows) {
411
+ const visited = new Set();
412
+ let current = nodeId;
413
+ while (current) {
414
+ if (visited.has(current))
415
+ return undefined;
416
+ visited.add(current);
417
+ if (splitGateways.has(current))
418
+ return current;
419
+ const inFlows = flows.filter((f) => f.targetRef === current);
420
+ if (inFlows.length !== 1)
421
+ return undefined;
422
+ const prev = inFlows[0];
423
+ if (!prev)
424
+ return undefined;
425
+ current = prev.sourceRef;
426
+ }
427
+ return undefined;
428
+ }
429
+ // ---------------------------------------------------------------------------
239
430
  // Branch builder (used inside gateway branch callbacks)
240
431
  // ---------------------------------------------------------------------------
241
432
  /**
@@ -260,10 +451,23 @@ export class BranchBuilder {
260
451
  /** @internal – true once connectTo() has been called, meaning the branch end is already wired */
261
452
  _connected = false;
262
453
  /** @internal */
263
- constructor(gatewayId, branchName) {
454
+ _textAnnotations = [];
455
+ /** @internal */
456
+ _associations = [];
457
+ _annCounters = new Map();
458
+ rootErrors;
459
+ rootMessages;
460
+ rootSignals;
461
+ rootEscalations;
462
+ /** @internal */
463
+ constructor(gatewayId, branchName, rootErrors = [], rootMessages = [], rootSignals = [], rootEscalations = []) {
264
464
  this.gatewayId = gatewayId;
265
465
  this.branchName = branchName;
266
466
  this.lastNodeId = gatewayId;
467
+ this.rootErrors = rootErrors;
468
+ this.rootMessages = rootMessages;
469
+ this.rootSignals = rootSignals;
470
+ this.rootEscalations = rootEscalations;
267
471
  }
268
472
  /** Set a FEEL condition expression on this branch's outgoing sequence flow. */
269
473
  condition(expression) {
@@ -327,129 +531,97 @@ export class BranchBuilder {
327
531
  get _lastNodeId() {
328
532
  return this.lastNodeId;
329
533
  }
534
+ // ---- Annotations ----
535
+ /** Attach a text annotation to the element at the current cursor position. */
536
+ textAnnotation(text) {
537
+ return this.annotate(this.lastNodeId, text);
538
+ }
539
+ /** Attach a text annotation to an element by explicit ID. */
540
+ annotate(elementId, text) {
541
+ const n = (this._annCounters.get(elementId) ?? 0) + 1;
542
+ this._annCounters.set(elementId, n);
543
+ const annId = `TextAnnotation_${elementId}_${n}`;
544
+ this._textAnnotations.push({ id: annId, text, unknownAttributes: {} });
545
+ this._associations.push({
546
+ id: `Association_${elementId}_${n}`,
547
+ sourceRef: elementId,
548
+ targetRef: annId,
549
+ associationDirection: "None",
550
+ unknownAttributes: {},
551
+ });
552
+ return this;
553
+ }
330
554
  // ---- Flow-node methods (mirror ProcessBuilder) ----
331
555
  serviceTask(id, options) {
332
- const unknownAttributes = {};
333
- if (options.modelerTemplate)
334
- unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
335
- if (options.modelerTemplateVersion)
336
- unknownAttributes["zeebe:modelerTemplateVersion"] = options.modelerTemplateVersion;
337
- if (options.modelerTemplateIcon)
338
- unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
339
- const el = makeFlowElement(id, "serviceTask", {
340
- name: options.name,
341
- extensionElements: buildServiceTaskExtensions(options),
342
- });
343
- el.unknownAttributes = unknownAttributes;
344
- return this.addElement(el);
556
+ return this.addElement(makeServiceTaskEl(id, options));
345
557
  }
346
558
  userTask(id, options) {
347
- const ext = options?.formId
348
- ? zeebeExtensionsToXmlElements({ formDefinition: { formId: options.formId } })
349
- : [];
350
- return this.addElement(makeFlowElement(id, "userTask", {
351
- name: options?.name,
352
- extensionElements: ext,
353
- }));
559
+ return this.addElement(makeUserTaskEl(id, options));
354
560
  }
355
561
  scriptTask(id, options) {
356
- return this.addElement(makeFlowElement(id, "scriptTask", {
357
- name: options.name,
358
- extensionElements: zeebeExtensionsToXmlElements({
359
- unknownElements: [
360
- {
361
- name: "zeebe:script",
362
- attributes: {
363
- expression: options.expression,
364
- resultVariable: options.resultVariable,
365
- },
366
- children: [],
367
- },
368
- ],
369
- }),
370
- }));
562
+ return this.addElement(makeScriptTaskEl(id, options));
371
563
  }
372
564
  sendTask(id, options) {
373
- return this.addElement(makeFlowElement(id, "sendTask", options));
565
+ const el = makeFlowElement(id, "sendTask", options);
566
+ if (options?.isForCompensation)
567
+ el.isForCompensation = true;
568
+ return this.addElement(el);
374
569
  }
375
570
  receiveTask(id, options) {
376
- return this.addElement(makeFlowElement(id, "receiveTask", options));
571
+ const el = makeFlowElement(id, "receiveTask", options);
572
+ if (options?.isForCompensation)
573
+ el.isForCompensation = true;
574
+ return this.addElement(el);
377
575
  }
378
576
  businessRuleTask(id, options) {
379
- const ext = options?.decisionId
380
- ? zeebeExtensionsToXmlElements({
381
- calledDecision: {
382
- decisionId: options.decisionId,
383
- resultVariable: options.resultVariable ?? "result",
384
- },
385
- })
386
- : [];
387
- return this.addElement(makeFlowElement(id, "businessRuleTask", {
388
- name: options?.name,
389
- extensionElements: ext,
390
- }));
577
+ return this.addElement(makeBusinessRuleTaskEl(id, options));
391
578
  }
392
579
  callActivity(id, options) {
393
- const attrs = { processId: options.processId };
394
- if (options.propagateAllChildVariables !== undefined) {
395
- attrs.propagateAllChildVariables = String(options.propagateAllChildVariables);
396
- }
397
- return this.addElement(makeFlowElement(id, "callActivity", {
398
- name: options.name,
399
- extensionElements: zeebeExtensionsToXmlElements({
400
- unknownElements: [
401
- {
402
- name: "zeebe:calledElement",
403
- attributes: attrs,
404
- children: [],
405
- },
406
- ],
407
- }),
408
- }));
580
+ return this.addElement(makeCallActivityEl(id, options));
581
+ }
582
+ /** Add an abstract task with no Zeebe extensions. */
583
+ task(id, options) {
584
+ const el = makeFlowElement(id, "task", options);
585
+ if (options?.isForCompensation)
586
+ el.isForCompensation = true;
587
+ return this.addElement(el);
409
588
  }
410
589
  startEvent(id, options) {
411
590
  const el = makeFlowElement(id ?? generateId("StartEvent"), "startEvent", options);
412
- if (el.type === "startEvent" &&
413
- (options?.timerDuration || options?.timerCycle || options?.timerDate)) {
414
- el.eventDefinitions = buildEventDefinitions(options);
591
+ if (el.type === "startEvent" && options) {
592
+ el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
415
593
  }
416
594
  return this.addElement(el);
417
595
  }
418
596
  endEvent(id, options) {
419
- return this.addElement(makeFlowElement(id ?? generateId("EndEvent"), "endEvent", options));
597
+ const el = makeFlowElement(id ?? generateId("EndEvent"), "endEvent", options);
598
+ if (el.type === "endEvent" && options) {
599
+ el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
600
+ }
601
+ return this.addElement(el);
420
602
  }
421
603
  intermediateThrowEvent(id, options) {
422
604
  const el = makeFlowElement(id ?? generateId("IntermediateThrowEvent"), "intermediateThrowEvent", options);
423
605
  if (el.type === "intermediateThrowEvent" && options) {
424
- el.eventDefinitions = buildEventDefinitions(options);
606
+ el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
425
607
  }
426
608
  return this.addElement(el);
427
609
  }
428
610
  intermediateCatchEvent(id, options) {
429
611
  const el = makeFlowElement(id ?? generateId("IntermediateCatchEvent"), "intermediateCatchEvent", options);
430
612
  if (el.type === "intermediateCatchEvent" && options) {
431
- el.eventDefinitions = buildEventDefinitions(options);
613
+ el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
432
614
  }
433
615
  return this.addElement(el);
434
616
  }
435
617
  exclusiveGateway(id, options) {
436
- const el = makeFlowElement(id, "exclusiveGateway", options);
437
- if (options?.defaultFlow && el.type === "exclusiveGateway") {
438
- ;
439
- el.default = options.defaultFlow;
440
- }
441
- return this.addElement(el);
618
+ return this.addElement(makeExclusiveGatewayEl(id, options));
442
619
  }
443
620
  parallelGateway(id, options) {
444
621
  return this.addElement(makeFlowElement(id, "parallelGateway", options));
445
622
  }
446
623
  inclusiveGateway(id, options) {
447
- const el = makeFlowElement(id, "inclusiveGateway", options);
448
- if (options?.defaultFlow && el.type === "inclusiveGateway") {
449
- ;
450
- el.default = options.defaultFlow;
451
- }
452
- return this.addElement(el);
624
+ return this.addElement(makeInclusiveGatewayEl(id, options));
453
625
  }
454
626
  eventBasedGateway(id, options) {
455
627
  return this.addElement(makeFlowElement(id, "eventBasedGateway", options));
@@ -464,99 +636,199 @@ export class SubProcessContentBuilder {
464
636
  _elements = [];
465
637
  /** @internal */
466
638
  _flows = [];
639
+ /** @internal */
640
+ _textAnnotations = [];
641
+ /** @internal */
642
+ _associations = [];
643
+ _annCounters = new Map();
467
644
  lastNodeId;
645
+ currentGatewayId;
646
+ openBranchEnds = [];
468
647
  addElement(element) {
648
+ if (this._elements.some((n) => n.id === element.id)) {
649
+ throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
650
+ }
469
651
  this._elements.push(element);
470
652
  if (this.lastNodeId) {
471
- const flowId = generateId("Flow");
472
653
  this._flows.push({
473
- id: flowId,
654
+ id: generateId("Flow"),
474
655
  sourceRef: this.lastNodeId,
475
656
  targetRef: element.id,
476
657
  extensionElements: [],
477
658
  unknownAttributes: {},
478
659
  });
479
660
  }
661
+ for (const branchEnd of this.openBranchEnds) {
662
+ this._flows.push({
663
+ id: generateId("Flow"),
664
+ sourceRef: branchEnd,
665
+ targetRef: element.id,
666
+ extensionElements: [],
667
+ unknownAttributes: {},
668
+ });
669
+ }
670
+ this.openBranchEnds = [];
480
671
  this.lastNodeId = element.id;
481
672
  return this;
482
673
  }
674
+ // ---- Events ----
483
675
  startEvent(id, options) {
484
676
  const el = makeFlowElement(id ?? generateId("StartEvent"), "startEvent", options);
485
677
  if (el.type === "startEvent" && options) {
486
678
  el.eventDefinitions = buildEventDefinitions(options);
679
+ if (options.isInterrupting === false)
680
+ el.isInterrupting = false;
487
681
  }
488
682
  return this.addElement(el);
489
683
  }
490
684
  endEvent(id, options) {
491
- return this.addElement(makeFlowElement(id ?? generateId("EndEvent"), "endEvent", options));
492
- }
493
- serviceTask(id, options) {
494
- const unknownAttributes = {};
495
- if (options.modelerTemplate)
496
- unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
497
- if (options.modelerTemplateVersion)
498
- unknownAttributes["zeebe:modelerTemplateVersion"] = options.modelerTemplateVersion;
499
- if (options.modelerTemplateIcon)
500
- unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
501
- const el = makeFlowElement(id, "serviceTask", {
502
- name: options.name,
503
- extensionElements: buildServiceTaskExtensions(options),
504
- });
505
- el.unknownAttributes = unknownAttributes;
685
+ const el = makeFlowElement(id ?? generateId("EndEvent"), "endEvent", options);
686
+ if (el.type === "endEvent" && options)
687
+ el.eventDefinitions = buildEventDefinitions(options);
506
688
  return this.addElement(el);
507
689
  }
508
- userTask(id, options) {
509
- return this.addElement(makeFlowElement(id, "userTask", { name: options?.name }));
510
- }
511
- scriptTask(id, options) {
512
- return this.addElement(makeFlowElement(id, "scriptTask", {
513
- name: options.name,
514
- extensionElements: zeebeExtensionsToXmlElements({
515
- unknownElements: [
516
- {
517
- name: "zeebe:script",
518
- attributes: {
519
- expression: options.expression,
520
- resultVariable: options.resultVariable,
521
- },
522
- children: [],
523
- },
524
- ],
525
- }),
526
- }));
527
- }
528
- callActivity(id, options) {
529
- const attrs = { processId: options.processId };
530
- if (options.propagateAllChildVariables !== undefined) {
531
- attrs.propagateAllChildVariables = String(options.propagateAllChildVariables);
532
- }
533
- return this.addElement(makeFlowElement(id, "callActivity", {
534
- name: options.name,
535
- extensionElements: zeebeExtensionsToXmlElements({
536
- unknownElements: [
537
- {
538
- name: "zeebe:calledElement",
539
- attributes: attrs,
540
- children: [],
541
- },
542
- ],
543
- }),
544
- }));
545
- }
546
690
  intermediateThrowEvent(id, options) {
547
691
  const el = makeFlowElement(id ?? generateId("IntermediateThrowEvent"), "intermediateThrowEvent", options);
548
- if (el.type === "intermediateThrowEvent" && options) {
692
+ if (el.type === "intermediateThrowEvent" && options)
549
693
  el.eventDefinitions = buildEventDefinitions(options);
550
- }
551
694
  return this.addElement(el);
552
695
  }
553
696
  intermediateCatchEvent(id, options) {
554
697
  const el = makeFlowElement(id ?? generateId("IntermediateCatchEvent"), "intermediateCatchEvent", options);
555
- if (el.type === "intermediateCatchEvent" && options) {
698
+ if (el.type === "intermediateCatchEvent" && options)
556
699
  el.eventDefinitions = buildEventDefinitions(options);
557
- }
558
700
  return this.addElement(el);
559
701
  }
702
+ // ---- Tasks ----
703
+ serviceTask(id, options) {
704
+ return this.addElement(makeServiceTaskEl(id, options));
705
+ }
706
+ scriptTask(id, options) {
707
+ return this.addElement(makeScriptTaskEl(id, options));
708
+ }
709
+ userTask(id, options) {
710
+ return this.addElement(makeUserTaskEl(id, options));
711
+ }
712
+ businessRuleTask(id, options) {
713
+ return this.addElement(makeBusinessRuleTaskEl(id, options));
714
+ }
715
+ callActivity(id, options) {
716
+ return this.addElement(makeCallActivityEl(id, options));
717
+ }
718
+ sendTask(id, options) {
719
+ const el = makeFlowElement(id, "sendTask", options);
720
+ if (options?.isForCompensation)
721
+ el.isForCompensation = true;
722
+ return this.addElement(el);
723
+ }
724
+ receiveTask(id, options) {
725
+ const el = makeFlowElement(id, "receiveTask", options);
726
+ if (options?.isForCompensation)
727
+ el.isForCompensation = true;
728
+ return this.addElement(el);
729
+ }
730
+ /** Add an abstract task with no Zeebe extensions. */
731
+ task(id, options) {
732
+ const el = makeFlowElement(id, "task", options);
733
+ if (options?.isForCompensation)
734
+ el.isForCompensation = true;
735
+ return this.addElement(el);
736
+ }
737
+ // ---- Gateways ----
738
+ exclusiveGateway(id, options) {
739
+ this.currentGatewayId = id;
740
+ return this.addElement(makeExclusiveGatewayEl(id, options));
741
+ }
742
+ parallelGateway(id, options) {
743
+ this.currentGatewayId = id;
744
+ return this.addElement(makeFlowElement(id, "parallelGateway", options));
745
+ }
746
+ inclusiveGateway(id, options) {
747
+ this.currentGatewayId = id;
748
+ return this.addElement(makeInclusiveGatewayEl(id, options));
749
+ }
750
+ eventBasedGateway(id, options) {
751
+ this.currentGatewayId = id;
752
+ return this.addElement(makeFlowElement(id, "eventBasedGateway", options));
753
+ }
754
+ // ---- Annotations ----
755
+ /** Attach a text annotation to the element at the current cursor position. */
756
+ textAnnotation(text) {
757
+ if (!this.lastNodeId) {
758
+ throw new Error("textAnnotation() must follow a flow element");
759
+ }
760
+ return this.annotate(this.lastNodeId, text);
761
+ }
762
+ /** Attach a text annotation to an element by explicit ID. */
763
+ annotate(elementId, text) {
764
+ const n = (this._annCounters.get(elementId) ?? 0) + 1;
765
+ this._annCounters.set(elementId, n);
766
+ const annId = `TextAnnotation_${elementId}_${n}`;
767
+ this._textAnnotations.push({ id: annId, text, unknownAttributes: {} });
768
+ this._associations.push({
769
+ id: `Association_${elementId}_${n}`,
770
+ sourceRef: elementId,
771
+ targetRef: annId,
772
+ associationDirection: "None",
773
+ unknownAttributes: {},
774
+ });
775
+ return this;
776
+ }
777
+ // ---- Branching & flow control ----
778
+ branch(name, callback) {
779
+ if (!this.currentGatewayId) {
780
+ throw new Error("branch() must be called after a gateway element");
781
+ }
782
+ const b = new BranchBuilder(this.currentGatewayId, name);
783
+ callback(b);
784
+ for (const el of b._elements) {
785
+ if (this._elements.some((n) => n.id === el.id)) {
786
+ throw new Error(`Duplicate element ID "${el.id}"`);
787
+ }
788
+ this._elements.push(el);
789
+ }
790
+ for (const fl of b._flows)
791
+ this._flows.push(fl);
792
+ for (const ann of b._textAnnotations)
793
+ this._textAnnotations.push(ann);
794
+ for (const assoc of b._associations)
795
+ this._associations.push(assoc);
796
+ if (b._defaultFlowId) {
797
+ const gw = this._elements.find((n) => n.id === this.currentGatewayId);
798
+ if (gw && (gw.type === "exclusiveGateway" || gw.type === "inclusiveGateway")) {
799
+ gw.default = b._defaultFlowId;
800
+ }
801
+ }
802
+ if (!b._connected && b._elements.length > 0) {
803
+ const lastEl = b._elements[b._elements.length - 1];
804
+ if (lastEl && lastEl.type !== "endEvent") {
805
+ this.openBranchEnds.push(b._lastNodeId);
806
+ }
807
+ }
808
+ this.lastNodeId = undefined;
809
+ return this;
810
+ }
811
+ connectTo(targetId) {
812
+ if (this.lastNodeId) {
813
+ this._flows.push({
814
+ id: generateId("Flow"),
815
+ sourceRef: this.lastNodeId,
816
+ targetRef: targetId,
817
+ extensionElements: [],
818
+ unknownAttributes: {},
819
+ });
820
+ }
821
+ this.lastNodeId = undefined;
822
+ return this;
823
+ }
824
+ element(elementId) {
825
+ if (!this._elements.some((n) => n.id === elementId)) {
826
+ throw new Error(`Element "${elementId}" not found in sub-process`);
827
+ }
828
+ this.lastNodeId = elementId;
829
+ this.currentGatewayId = undefined;
830
+ return this;
831
+ }
560
832
  }
561
833
  // ---------------------------------------------------------------------------
562
834
  // Process builder (top-level entry point)
@@ -571,10 +843,18 @@ export class ProcessBuilder {
571
843
  sequenceFlows = [];
572
844
  rootErrors = [];
573
845
  rootMessages = [];
846
+ rootSignals = [];
847
+ rootEscalations = [];
848
+ _textAnnotations = [];
849
+ _associations = [];
850
+ _annCounters = new Map();
574
851
  lastNodeId;
575
852
  currentGatewayId;
576
853
  openBranchEnds = [];
577
854
  _autoLayout = false;
855
+ _executionPlatformVersion = "8.9.0";
856
+ _serviceTaskDefaults = {};
857
+ _savedMainFlowId = undefined;
578
858
  constructor(processId) {
579
859
  this.processId = processId;
580
860
  }
@@ -583,6 +863,17 @@ export class ProcessBuilder {
583
863
  this._autoLayout = true;
584
864
  return this;
585
865
  }
866
+ /** Set the Camunda execution platform version stamped into the BPMN definitions. Defaults to `"8.9.0"`. */
867
+ executionPlatformVersion(version) {
868
+ this._executionPlatformVersion = version;
869
+ return this;
870
+ }
871
+ /** Set process-wide defaults applied to subsequently added elements. */
872
+ defaults(options) {
873
+ if (options.serviceTask)
874
+ this._serviceTaskDefaults = { ...this._serviceTaskDefaults, ...options.serviceTask };
875
+ return this;
876
+ }
586
877
  /** Set the display name for this process. */
587
878
  name(name) {
588
879
  this.processName = name;
@@ -613,7 +904,9 @@ export class ProcessBuilder {
613
904
  extensionElements: extElements,
614
905
  });
615
906
  if (element.type === "startEvent" && options) {
616
- element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages);
907
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
908
+ if (options.isInterrupting === false)
909
+ element.isInterrupting = false;
617
910
  }
618
911
  if (options?.modelerTemplate) {
619
912
  element.unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
@@ -639,10 +932,22 @@ export class ProcessBuilder {
639
932
  this.currentGatewayId = undefined;
640
933
  return this.startEvent(id, options);
641
934
  }
935
+ /**
936
+ * Alias for `addStartEvent()` — begins a new disconnected parallel path.
937
+ *
938
+ * Use this for readability when modeling processes with multiple independent paths.
939
+ */
940
+ disconnectedStartEvent(id, options) {
941
+ return this.addStartEvent(id, options);
942
+ }
642
943
  /** Add an end event. */
643
944
  endEvent(id, options) {
644
945
  const nodeId = id ?? generateId("EndEvent");
645
- this.addFlowElement(makeFlowElement(nodeId, "endEvent", options));
946
+ const element = makeFlowElement(nodeId, "endEvent", options);
947
+ if (element.type === "endEvent" && options) {
948
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
949
+ }
950
+ this.addFlowElement(element);
646
951
  return this;
647
952
  }
648
953
  /** Add an intermediate throw event (none, message, signal, escalation). */
@@ -650,7 +955,7 @@ export class ProcessBuilder {
650
955
  const nodeId = id ?? generateId("IntermediateThrowEvent");
651
956
  const element = makeFlowElement(nodeId, "intermediateThrowEvent", options);
652
957
  if (element.type === "intermediateThrowEvent" && options) {
653
- element.eventDefinitions = buildEventDefinitions(options);
958
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
654
959
  }
655
960
  this.addFlowElement(element);
656
961
  return this;
@@ -660,7 +965,7 @@ export class ProcessBuilder {
660
965
  const nodeId = id ?? generateId("IntermediateCatchEvent");
661
966
  const element = makeFlowElement(nodeId, "intermediateCatchEvent", options);
662
967
  if (element.type === "intermediateCatchEvent" && options) {
663
- element.eventDefinitions = buildEventDefinitions(options);
968
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
664
969
  }
665
970
  this.addFlowElement(element);
666
971
  return this;
@@ -676,32 +981,57 @@ export class ProcessBuilder {
676
981
  if (element.type === "boundaryEvent") {
677
982
  element.attachedToRef = options.attachedTo;
678
983
  element.cancelActivity = options.cancelActivity;
679
- element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages);
984
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
680
985
  }
681
986
  // Boundary events never auto-connect — temporarily clear lastNodeId
682
987
  const prevLast = this.lastNodeId;
683
988
  this.lastNodeId = undefined;
684
989
  this.addFlowElement(element);
990
+ // For compensation boundary events, save the main-flow cursor AFTER addFlowElement
991
+ // so subsequent normal elements don't accidentally clear it before it's consumed.
992
+ if (options.compensation) {
993
+ this._savedMainFlowId = prevLast;
994
+ }
685
995
  // Don't restore prevLast — the builder now chains from the boundary event
686
996
  void prevLast;
687
997
  return this;
688
998
  }
999
+ /**
1000
+ * Attach a boundary event to the preceding task and build its outgoing path,
1001
+ * then restore the builder cursor to the preceding task so the main flow continues.
1002
+ *
1003
+ * @param id - ID for the boundary event element.
1004
+ * @param options - Boundary event options (without `attachedTo` — inferred from cursor).
1005
+ * @param handler - Callback that chains elements from the boundary event.
1006
+ */
1007
+ withBoundary(id, options, handler) {
1008
+ const attachedTo = this.lastNodeId;
1009
+ if (!attachedTo) {
1010
+ throw new Error("withBoundary() must follow a task element. Current builder position has no active element.");
1011
+ }
1012
+ const savedLast = this.lastNodeId;
1013
+ const savedGateway = this.currentGatewayId;
1014
+ const savedOpenEnds = [...this.openBranchEnds];
1015
+ this.openBranchEnds = [];
1016
+ // boundaryEvent() sets lastNodeId to the boundary event id
1017
+ this.boundaryEvent(id, { ...options, attachedTo });
1018
+ // Build the error/timeout path chaining from the boundary event
1019
+ handler(this);
1020
+ // Restore cursor to the original task so the main flow continues
1021
+ this.lastNodeId = savedLast;
1022
+ this.currentGatewayId = savedGateway;
1023
+ this.openBranchEnds = savedOpenEnds;
1024
+ this._savedMainFlowId = undefined;
1025
+ return this;
1026
+ }
689
1027
  // ---- Tasks ----
690
1028
  /** Add a service task with Zeebe task definition and optional IO mappings. */
691
1029
  serviceTask(id, options) {
692
- const unknownAttributes = {};
693
- if (options.modelerTemplate)
694
- unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
695
- if (options.modelerTemplateVersion)
696
- unknownAttributes["zeebe:modelerTemplateVersion"] = options.modelerTemplateVersion;
697
- if (options.modelerTemplateIcon)
698
- unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
699
- const el = makeFlowElement(id, "serviceTask", {
700
- name: options.name,
701
- extensionElements: buildServiceTaskExtensions(options),
702
- });
703
- el.unknownAttributes = unknownAttributes;
704
- this.addFlowElement(el);
1030
+ const merged = {
1031
+ ...options,
1032
+ retries: options.retries ?? this._serviceTaskDefaults.retries,
1033
+ };
1034
+ this.addFlowElement(makeServiceTaskEl(id, merged));
705
1035
  return this;
706
1036
  }
707
1037
  /** Add a REST connector task — syntactic sugar over `serviceTask()`. */
@@ -732,95 +1062,52 @@ export class ProcessBuilder {
732
1062
  }
733
1063
  /** Add a script task with a FEEL expression. */
734
1064
  scriptTask(id, options) {
735
- this.addFlowElement(makeFlowElement(id, "scriptTask", {
736
- name: options.name,
737
- extensionElements: zeebeExtensionsToXmlElements({
738
- unknownElements: [
739
- {
740
- name: "zeebe:script",
741
- attributes: {
742
- expression: options.expression,
743
- resultVariable: options.resultVariable,
744
- },
745
- children: [],
746
- },
747
- ],
748
- }),
749
- }));
1065
+ this.addFlowElement(makeScriptTaskEl(id, options));
750
1066
  return this;
751
1067
  }
752
1068
  /** Add a user task with optional form reference. */
753
1069
  userTask(id, options) {
754
- const extensionElements = options?.formId
755
- ? zeebeExtensionsToXmlElements({ formDefinition: { formId: options.formId } })
756
- : [];
757
- this.addFlowElement(makeFlowElement(id, "userTask", {
758
- name: options?.name,
759
- extensionElements,
760
- }));
1070
+ this.addFlowElement(makeUserTaskEl(id, options));
761
1071
  return this;
762
1072
  }
763
1073
  /** Add a send task (aspirational). */
764
1074
  sendTask(id, options) {
765
- this.addFlowElement(makeFlowElement(id, "sendTask", options));
1075
+ const el = makeFlowElement(id, "sendTask", options);
1076
+ if (options?.isForCompensation)
1077
+ el.isForCompensation = true;
1078
+ this.addFlowElement(el);
766
1079
  return this;
767
1080
  }
768
1081
  /** Add a receive task (aspirational). */
769
1082
  receiveTask(id, options) {
770
- this.addFlowElement(makeFlowElement(id, "receiveTask", options));
1083
+ const el = makeFlowElement(id, "receiveTask", options);
1084
+ if (options?.isForCompensation)
1085
+ el.isForCompensation = true;
1086
+ this.addFlowElement(el);
771
1087
  return this;
772
1088
  }
773
1089
  /** Add a business rule task. */
774
1090
  businessRuleTask(id, options) {
775
- const ext = [];
776
- if (options?.taskType) {
777
- ext.push(...zeebeExtensionsToXmlElements({ taskDefinition: { type: options.taskType } }));
778
- }
779
- if (options?.decisionId) {
780
- ext.push(...zeebeExtensionsToXmlElements({
781
- calledDecision: {
782
- decisionId: options.decisionId,
783
- resultVariable: options.resultVariable ?? "result",
784
- },
785
- }));
786
- }
787
- this.addFlowElement(makeFlowElement(id, "businessRuleTask", {
788
- name: options?.name,
789
- extensionElements: ext,
790
- }));
1091
+ this.addFlowElement(makeBusinessRuleTaskEl(id, options));
791
1092
  return this;
792
1093
  }
793
1094
  /** Add a call activity referencing another process. */
794
1095
  callActivity(id, options) {
795
- const attrs = {
796
- processId: options.processId,
797
- };
798
- if (options.propagateAllChildVariables !== undefined) {
799
- attrs.propagateAllChildVariables = String(options.propagateAllChildVariables);
800
- }
801
- this.addFlowElement(makeFlowElement(id, "callActivity", {
802
- name: options.name,
803
- extensionElements: zeebeExtensionsToXmlElements({
804
- unknownElements: [
805
- {
806
- name: "zeebe:calledElement",
807
- attributes: attrs,
808
- children: [],
809
- },
810
- ],
811
- }),
812
- }));
1096
+ this.addFlowElement(makeCallActivityEl(id, options));
1097
+ return this;
1098
+ }
1099
+ /** Add an abstract task with no Zeebe extensions. */
1100
+ task(id, options) {
1101
+ const el = makeFlowElement(id, "task", options);
1102
+ if (options?.isForCompensation)
1103
+ el.isForCompensation = true;
1104
+ this.addFlowElement(el);
813
1105
  return this;
814
1106
  }
815
1107
  // ---- Gateways ----
816
1108
  /** Add an exclusive gateway (XOR split/join). */
817
1109
  exclusiveGateway(id, options) {
818
- const element = makeFlowElement(id, "exclusiveGateway", options);
819
- if (options?.defaultFlow && element.type === "exclusiveGateway") {
820
- ;
821
- element.default = options.defaultFlow;
822
- }
823
- this.addFlowElement(element);
1110
+ this.addFlowElement(makeExclusiveGatewayEl(id, options));
824
1111
  this.currentGatewayId = id;
825
1112
  return this;
826
1113
  }
@@ -832,12 +1119,7 @@ export class ProcessBuilder {
832
1119
  }
833
1120
  /** Add an inclusive gateway (OR split/join). Aspirational. */
834
1121
  inclusiveGateway(id, options) {
835
- const element = makeFlowElement(id, "inclusiveGateway", options);
836
- if (options?.defaultFlow && element.type === "inclusiveGateway") {
837
- ;
838
- element.default = options.defaultFlow;
839
- }
840
- this.addFlowElement(element);
1122
+ this.addFlowElement(makeInclusiveGatewayEl(id, options));
841
1123
  this.currentGatewayId = id;
842
1124
  return this;
843
1125
  }
@@ -869,7 +1151,7 @@ export class ProcessBuilder {
869
1151
  if (!this.currentGatewayId) {
870
1152
  throw new Error("branch() must be called after a gateway element");
871
1153
  }
872
- const b = new BranchBuilder(this.currentGatewayId, name);
1154
+ const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
873
1155
  callback(b);
874
1156
  for (const el of b._elements) {
875
1157
  if (this.flowElements.some((n) => n.id === el.id)) {
@@ -880,6 +1162,10 @@ export class ProcessBuilder {
880
1162
  for (const fl of b._flows) {
881
1163
  this.sequenceFlows.push(fl);
882
1164
  }
1165
+ for (const ann of b._textAnnotations)
1166
+ this._textAnnotations.push(ann);
1167
+ for (const assoc of b._associations)
1168
+ this._associations.push(assoc);
883
1169
  // If the branch is the default flow, set the gateway's default
884
1170
  if (b._defaultFlowId) {
885
1171
  const gateway = this.flowElements.find((n) => n.id === this.currentGatewayId);
@@ -933,6 +1219,7 @@ export class ProcessBuilder {
933
1219
  adHocSubProcess(id, content, options) {
934
1220
  const sub = new SubProcessContentBuilder();
935
1221
  content(sub);
1222
+ insertJoinGateways(sub._elements, sub._flows);
936
1223
  recomputeIncomingOutgoing(sub._elements, sub._flows);
937
1224
  const zeebeExt = {};
938
1225
  if (options?.taskDefinition) {
@@ -984,6 +1271,8 @@ export class ProcessBuilder {
984
1271
  if (element.type === "adHocSubProcess") {
985
1272
  element.flowElements = sub._elements;
986
1273
  element.sequenceFlows = sub._flows;
1274
+ element.textAnnotations = sub._textAnnotations;
1275
+ element.associations = sub._associations;
987
1276
  if (options?.loopCharacteristics) {
988
1277
  element.loopCharacteristics = buildAdHocLoopCharacteristics(options.loopCharacteristics);
989
1278
  }
@@ -998,11 +1287,14 @@ export class ProcessBuilder {
998
1287
  subProcess(id, content, options) {
999
1288
  const sub = new SubProcessContentBuilder();
1000
1289
  content(sub);
1290
+ insertJoinGateways(sub._elements, sub._flows);
1001
1291
  recomputeIncomingOutgoing(sub._elements, sub._flows);
1002
1292
  const element = makeFlowElement(id, "subProcess", options);
1003
1293
  if (element.type === "subProcess") {
1004
1294
  element.flowElements = sub._elements;
1005
1295
  element.sequenceFlows = sub._flows;
1296
+ element.textAnnotations = sub._textAnnotations;
1297
+ element.associations = sub._associations;
1006
1298
  if (options?.multiInstance) {
1007
1299
  element.loopCharacteristics = buildMultiInstance(options.multiInstance);
1008
1300
  }
@@ -1010,17 +1302,52 @@ export class ProcessBuilder {
1010
1302
  this.addFlowElement(element);
1011
1303
  return this;
1012
1304
  }
1013
- /** Add an event sub-process (aspirational). */
1305
+ /** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
1014
1306
  eventSubProcess(id, content, options) {
1015
1307
  const sub = new SubProcessContentBuilder();
1016
1308
  content(sub);
1309
+ insertJoinGateways(sub._elements, sub._flows);
1017
1310
  recomputeIncomingOutgoing(sub._elements, sub._flows);
1018
- const element = makeFlowElement(id, "eventSubProcess", options);
1019
- if (element.type === "eventSubProcess") {
1311
+ const element = makeFlowElement(id, "subProcess", options);
1312
+ if (element.type === "subProcess") {
1313
+ element.triggeredByEvent = true;
1020
1314
  element.flowElements = sub._elements;
1021
1315
  element.sequenceFlows = sub._flows;
1316
+ element.textAnnotations = sub._textAnnotations;
1317
+ element.associations = sub._associations;
1022
1318
  }
1023
- this.addFlowElement(element);
1319
+ // Event sub-processes have no incoming/outgoing sequence flows and must not
1320
+ // advance the flow cursor — the surrounding process wires around them.
1321
+ // openBranchEnds is intentionally NOT drained here; the next normal
1322
+ // addFlowElement call will drain it and connect branch ends to that element.
1323
+ if (this.flowElements.some((n) => n.id === element.id)) {
1324
+ throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
1325
+ }
1326
+ this._savedMainFlowId = undefined;
1327
+ this.flowElements.push(element);
1328
+ return this;
1329
+ }
1330
+ // ---- Annotations ----
1331
+ /** Attach a text annotation to the element at the current cursor position. */
1332
+ textAnnotation(text) {
1333
+ if (!this.lastNodeId) {
1334
+ throw new Error("textAnnotation() must follow a flow element");
1335
+ }
1336
+ return this.annotate(this.lastNodeId, text);
1337
+ }
1338
+ /** Attach a text annotation to any flow element by explicit ID. */
1339
+ annotate(elementId, text) {
1340
+ const n = (this._annCounters.get(elementId) ?? 0) + 1;
1341
+ this._annCounters.set(elementId, n);
1342
+ const annId = `TextAnnotation_${elementId}_${n}`;
1343
+ this._textAnnotations.push({ id: annId, text, unknownAttributes: {} });
1344
+ this._associations.push({
1345
+ id: `Association_${elementId}_${n}`,
1346
+ sourceRef: elementId,
1347
+ targetRef: annId,
1348
+ associationDirection: "None",
1349
+ unknownAttributes: {},
1350
+ });
1024
1351
  return this;
1025
1352
  }
1026
1353
  // ---- Build ----
@@ -1030,8 +1357,17 @@ export class ProcessBuilder {
1030
1357
  * Resolves all forward-referenced `incoming` / `outgoing` arrays and wraps
1031
1358
  * the process in a {@link BpmnDefinitions} ready for XML serialization.
1032
1359
  */
1033
- build() {
1034
- this.insertJoinGateways();
1360
+ build(options) {
1361
+ const beforeCount = this.flowElements.length;
1362
+ insertJoinGateways(this.flowElements, this.sequenceFlows);
1363
+ if (options?.strict && this.flowElements.length > beforeCount) {
1364
+ const inserted = this.flowElements
1365
+ .slice(beforeCount)
1366
+ .map((e) => e.id)
1367
+ .join(", ");
1368
+ throw new Error(`auto-join gateways were inserted: ${inserted}. Use explicit .connectTo(joinId) to make gateway topology explicit, or remove { strict: true }.`);
1369
+ }
1370
+ this.validate();
1035
1371
  recomputeIncomingOutgoing(this.flowElements, this.sequenceFlows);
1036
1372
  const extensionElements = [];
1037
1373
  if (this._versionTag) {
@@ -1048,15 +1384,15 @@ export class ProcessBuilder {
1048
1384
  extensionElements,
1049
1385
  flowElements: this.flowElements,
1050
1386
  sequenceFlows: this.sequenceFlows,
1051
- textAnnotations: [],
1052
- associations: [],
1387
+ textAnnotations: this._textAnnotations,
1388
+ associations: this._associations,
1053
1389
  unknownAttributes: {},
1054
1390
  };
1055
- return {
1391
+ const defs = {
1056
1392
  id: "Definitions_1",
1057
1393
  targetNamespace: "http://bpmn.io/schema/bpmn",
1058
1394
  exporter: "@bpmnkit/core",
1059
- exporterVersion: "0.0.1",
1395
+ exporterVersion: EXPORTER_VERSION,
1060
1396
  namespaces: {
1061
1397
  bpmn: "http://www.omg.org/spec/BPMN/20100524/MODEL",
1062
1398
  bpmndi: "http://www.omg.org/spec/BPMN/20100524/DI",
@@ -1068,134 +1404,67 @@ export class ProcessBuilder {
1068
1404
  },
1069
1405
  unknownAttributes: {
1070
1406
  "modeler:executionPlatform": "Camunda Cloud",
1071
- "modeler:executionPlatformVersion": "8.6.0",
1407
+ "modeler:executionPlatformVersion": this._executionPlatformVersion,
1072
1408
  },
1073
1409
  errors: this.rootErrors,
1074
- escalations: [],
1410
+ escalations: this.rootEscalations,
1075
1411
  messages: this.rootMessages,
1076
- signals: [],
1412
+ signals: this.rootSignals,
1077
1413
  collaborations: [],
1078
1414
  processes: [process],
1079
- diagrams: this._autoLayout ? [this.buildDiagram(process)] : [],
1415
+ // Seed diagram stub so applyAutoLayout preserves process-specific IDs.
1416
+ diagrams: this._autoLayout
1417
+ ? [
1418
+ {
1419
+ id: `${this.processId}_di`,
1420
+ plane: {
1421
+ id: `${this.processId}_di_plane`,
1422
+ bpmnElement: this.processId,
1423
+ shapes: [],
1424
+ edges: [],
1425
+ },
1426
+ },
1427
+ ]
1428
+ : [],
1080
1429
  };
1430
+ return this._autoLayout ? applyAutoLayout(defs) : defs;
1081
1431
  }
1082
- buildDiagram(process) {
1083
- const layoutResult = layoutProcess(process);
1084
- return layoutResultToDiagram(this.processId, layoutResult);
1085
- }
1086
- // ---- Internal ----
1087
- /**
1088
- * Insert matching join gateways where split-gateway branches converge
1089
- * on a non-gateway target. BPMN best practice: every split has a join.
1090
- */
1091
- insertJoinGateways() {
1092
- const GATEWAY_TYPES = new Set([
1093
- "exclusiveGateway",
1094
- "parallelGateway",
1095
- "inclusiveGateway",
1096
- "eventBasedGateway",
1097
- ]);
1098
- const elementTypes = new Map();
1099
- for (const el of this.flowElements) {
1100
- elementTypes.set(el.id, el.type);
1101
- }
1102
- // Find split gateways (2+ outgoing flows)
1103
- const outCount = new Map();
1432
+ validate() {
1433
+ const elementIds = new Set(this.flowElements.map((el) => el.id));
1104
1434
  for (const flow of this.sequenceFlows) {
1105
- outCount.set(flow.sourceRef, (outCount.get(flow.sourceRef) ?? 0) + 1);
1106
- }
1107
- const splitGateways = new Set();
1108
- for (const [id, count] of outCount) {
1109
- const type = elementTypes.get(id);
1110
- if (type && GATEWAY_TYPES.has(type) && count >= 2) {
1111
- splitGateways.add(id);
1435
+ if (!elementIds.has(flow.targetRef)) {
1436
+ throw new Error(`Sequence flow "${flow.id}" in process "${this.processId}" references unknown ` +
1437
+ `element "${flow.targetRef}". Check connectTo() calls — target must exist.`);
1112
1438
  }
1113
- }
1114
- if (splitGateways.size === 0)
1115
- return;
1116
- // Build incoming flow map
1117
- const incoming = new Map();
1118
- for (const flow of this.sequenceFlows) {
1119
- const arr = incoming.get(flow.targetRef);
1120
- if (arr)
1121
- arr.push(flow);
1122
- else
1123
- incoming.set(flow.targetRef, [flow]);
1124
- }
1125
- // For each target with 2+ incoming flows, check if they trace back
1126
- // to the same split gateway → insert a join gateway if needed
1127
- for (const [targetId, inFlows] of incoming) {
1128
- if (inFlows.length < 2)
1129
- continue;
1130
- // Group incoming flows by originating split gateway
1131
- const splitToFlows = new Map();
1132
- for (const flow of inFlows) {
1133
- const split = this.traceBackToSplit(flow.sourceRef, splitGateways);
1134
- if (split) {
1135
- const arr = splitToFlows.get(split);
1136
- if (arr)
1137
- arr.push(flow);
1138
- else
1139
- splitToFlows.set(split, [flow]);
1140
- }
1141
- }
1142
- for (const [splitId, convergingFlows] of splitToFlows) {
1143
- if (convergingFlows.length < 2)
1144
- continue;
1145
- const gwType = elementTypes.get(splitId);
1146
- if (!gwType)
1147
- continue;
1148
- // Don't insert if target is already a matching gateway type
1149
- const targetType = elementTypes.get(targetId);
1150
- if (targetType === gwType)
1151
- continue;
1152
- const joinId = `${splitId}_join`;
1153
- if (elementTypes.has(joinId))
1154
- continue;
1155
- const joinElement = makeFlowElement(joinId, gwType, {});
1156
- this.flowElements.push(joinElement);
1157
- elementTypes.set(joinId, gwType);
1158
- // Re-route converging flows to the join gateway
1159
- for (const flow of convergingFlows) {
1160
- flow.targetRef = joinId;
1161
- }
1162
- // Add flow from join to original target
1163
- this.sequenceFlows.push({
1164
- id: generateId("Flow"),
1165
- sourceRef: joinId,
1166
- targetRef: targetId,
1167
- extensionElements: [],
1168
- unknownAttributes: {},
1169
- });
1439
+ if (!elementIds.has(flow.sourceRef)) {
1440
+ throw new Error(`Sequence flow "${flow.id}" in process "${this.processId}" references unknown ` +
1441
+ `source element "${flow.sourceRef}".`);
1170
1442
  }
1171
1443
  }
1172
1444
  }
1173
- /** Trace backward from a node to find which split gateway it belongs to. */
1174
- traceBackToSplit(nodeId, splitGateways) {
1175
- const visited = new Set();
1176
- let current = nodeId;
1177
- while (current) {
1178
- if (visited.has(current))
1179
- return undefined;
1180
- visited.add(current);
1181
- if (splitGateways.has(current))
1182
- return current;
1183
- // Follow single incoming flow backward
1184
- const inFlows = this.sequenceFlows.filter((f) => f.targetRef === current);
1185
- if (inFlows.length !== 1)
1186
- return undefined;
1187
- const prev = inFlows[0];
1188
- if (!prev)
1189
- return undefined;
1190
- current = prev.sourceRef;
1191
- }
1192
- return undefined;
1193
- }
1194
1445
  addFlowElement(element) {
1195
1446
  if (this.flowElements.some((n) => n.id === element.id)) {
1196
1447
  throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
1197
1448
  }
1198
1449
  this.flowElements.push(element);
1450
+ // Compensation handlers are outside the normal token flow: link via association
1451
+ // from the preceding compensation boundary event, then restore the main-flow cursor.
1452
+ if (element.isForCompensation) {
1453
+ if (this.lastNodeId) {
1454
+ this._associations.push({
1455
+ id: generateId("Association"),
1456
+ sourceRef: this.lastNodeId,
1457
+ targetRef: element.id,
1458
+ associationDirection: "One",
1459
+ unknownAttributes: {},
1460
+ });
1461
+ }
1462
+ // Restore main-flow cursor (saved by boundaryEvent() when compensation: true)
1463
+ this.lastNodeId = this._savedMainFlowId;
1464
+ this._savedMainFlowId = undefined;
1465
+ // Do NOT connect open branch ends — handler is outside normal flow
1466
+ return;
1467
+ }
1199
1468
  if (this.lastNodeId) {
1200
1469
  const flowId = generateId("Flow");
1201
1470
  this.sequenceFlows.push({
@@ -1218,7 +1487,69 @@ export class ProcessBuilder {
1218
1487
  });
1219
1488
  }
1220
1489
  this.openBranchEnds = [];
1490
+ // Clear any saved compensation cursor — a normal element advancing the cursor
1491
+ // means the compensation boundary/handler pattern has been interrupted.
1492
+ this._savedMainFlowId = undefined;
1221
1493
  this.lastNodeId = element.id;
1222
1494
  }
1223
1495
  }
1496
+ // ---------------------------------------------------------------------------
1497
+ // Diagram builder — multi-process support
1498
+ // ---------------------------------------------------------------------------
1499
+ /**
1500
+ * Builder for a complete BPMN definitions document containing one or more processes.
1501
+ * Use `Bpmn.createDiagram(id?)` to obtain an instance.
1502
+ */
1503
+ export class DiagramBuilder {
1504
+ _id;
1505
+ _processes = [];
1506
+ _errors = [];
1507
+ _messages = [];
1508
+ _executionPlatformVersion = "8.9.0";
1509
+ constructor(id) {
1510
+ this._id = id;
1511
+ }
1512
+ /** Set the Camunda execution platform version stamped into the BPMN definitions. Defaults to `"8.9.0"`. */
1513
+ executionPlatformVersion(version) {
1514
+ this._executionPlatformVersion = version;
1515
+ return this;
1516
+ }
1517
+ process(id, callback) {
1518
+ const builder = new ProcessBuilder(id);
1519
+ callback(builder);
1520
+ const defs = builder.build();
1521
+ this._processes.push(...defs.processes);
1522
+ this._errors.push(...defs.errors);
1523
+ this._messages.push(...defs.messages);
1524
+ return this;
1525
+ }
1526
+ build() {
1527
+ return {
1528
+ id: this._id,
1529
+ targetNamespace: "http://bpmn.io/schema/bpmn",
1530
+ exporter: "@bpmnkit/core",
1531
+ exporterVersion: EXPORTER_VERSION,
1532
+ namespaces: {
1533
+ bpmn: "http://www.omg.org/spec/BPMN/20100524/MODEL",
1534
+ bpmndi: "http://www.omg.org/spec/BPMN/20100524/DI",
1535
+ dc: "http://www.omg.org/spec/DD/20100524/DC",
1536
+ di: "http://www.omg.org/spec/DD/20100524/DI",
1537
+ zeebe: "http://camunda.org/schema/zeebe/1.0",
1538
+ modeler: "http://camunda.org/schema/modeler/1.0",
1539
+ xsi: "http://www.w3.org/2001/XMLSchema-instance",
1540
+ },
1541
+ unknownAttributes: {
1542
+ "modeler:executionPlatform": "Camunda Cloud",
1543
+ "modeler:executionPlatformVersion": this._executionPlatformVersion,
1544
+ },
1545
+ errors: this._errors,
1546
+ escalations: [],
1547
+ messages: this._messages,
1548
+ signals: [],
1549
+ collaborations: [],
1550
+ processes: this._processes,
1551
+ diagrams: [],
1552
+ };
1553
+ }
1554
+ }
1224
1555
  //# sourceMappingURL=bpmn-builder.js.map