@uipath/maestro-builder-sdk 6.11.5 → 6.12.0

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.
Files changed (56) hide show
  1. package/dist/api-index.md +83 -56
  2. package/dist/api-members.md +362 -261
  3. package/dist/bpmn/bpmn-expr-check.js +8 -1
  4. package/dist/bpmn/bpmn-sdk.d.ts +1026 -27
  5. package/dist/bpmn/bpmn-sdk.js +1328 -83
  6. package/dist/bpmn/check-cli.js +19 -4
  7. package/dist/bpmn/check.d.ts +1 -1
  8. package/dist/bpmn/check.js +529 -13
  9. package/dist/bpmn/connector.d.ts +2 -2
  10. package/dist/bpmn/connector.js +74 -37
  11. package/dist/bpmn/data-service.d.ts +124 -0
  12. package/dist/bpmn/data-service.js +98 -0
  13. package/dist/bpmn/decompile-cli.js +70 -8
  14. package/dist/bpmn/decompile.d.ts +71 -0
  15. package/dist/bpmn/decompile.js +845 -31
  16. package/dist/bpmn/graph.d.ts +69 -0
  17. package/dist/bpmn/graph.js +98 -0
  18. package/dist/bpmn/intsvc.js +2 -2
  19. package/dist/bpmn/known-methods.d.ts +15 -0
  20. package/dist/bpmn/known-methods.js +271 -0
  21. package/dist/bpmn/lift-verify.d.ts +40 -0
  22. package/dist/bpmn/lift-verify.js +133 -0
  23. package/dist/bpmn/serialize.d.ts +65 -1
  24. package/dist/bpmn/serialize.js +169 -22
  25. package/dist/bpmn/typed-node.d.ts +37 -6
  26. package/dist/bpmn/typed-node.js +81 -19
  27. package/dist/check-cli.js +2 -1
  28. package/dist/check.d.ts +13 -0
  29. package/dist/check.js +11 -0
  30. package/dist/cli/commands/bpmn.js +7 -1
  31. package/dist/cli-library.d.ts +7 -0
  32. package/dist/cli-library.js +19 -0
  33. package/dist/compile-cli.js +8 -5
  34. package/dist/core/byoa.d.ts +69 -0
  35. package/dist/core/byoa.js +98 -0
  36. package/dist/core/library.d.ts +23 -0
  37. package/dist/core/library.js +47 -1
  38. package/dist/core/lookup-store.d.ts +12 -0
  39. package/dist/core/lookup-store.js +25 -0
  40. package/dist/index.d.ts +5 -2
  41. package/dist/index.js +3 -0
  42. package/dist/registry/generators/descriptors.d.ts +1 -1
  43. package/dist/registry/generators/descriptors.js +20 -2
  44. package/dist/registry/generators/markdown/page.js +3 -1
  45. package/dist/registry/generators/markdown/sections/event-facts.d.ts +2 -0
  46. package/dist/registry/generators/markdown/sections/event-facts.js +45 -0
  47. package/dist/registry/library-build/events.d.ts +32 -3
  48. package/dist/registry/library-build/events.js +48 -19
  49. package/dist/registry/prepare/byoa-ledger.d.ts +12 -0
  50. package/dist/registry/prepare/byoa-ledger.js +26 -0
  51. package/dist/registry/prepare/event.d.ts +5 -1
  52. package/dist/registry/prepare/event.js +22 -8
  53. package/dist/registry/prepare/messages.d.ts +2 -0
  54. package/dist/registry/prepare/messages.js +4 -0
  55. package/dist/serialize.js +24 -1
  56. package/package.json +2 -2
@@ -2,11 +2,105 @@ import { connector as makeConnector } from '../core/actions.js';
2
2
  import { checkBpmnExpressions, BpmnBuildError } from './bpmn-expr-check.js';
3
3
  import { BPMN_SCHEMA_VERSIONS } from './format-profile.js';
4
4
  import { schemaVersionRefusal } from '../schema-version.js';
5
- import { bindingRequiredFields, REGISTRY_GAPS, typedNodeOutputVar } from './typed-node.js';
5
+ import { bindingRequiredFields, REGISTRY_GAPS, registryType, typedNodeOutputVar } from './typed-node.js';
6
+ import { DATA_SERVICE_ACTIVITY_TYPE, DATA_SERVICE_CONNECTOR_KEY, dataServiceWire } from './data-service.js';
6
7
  export { BpmnBuildError } from './bpmn-expr-check.js';
7
8
  function normTimer(t) {
8
9
  return typeof t === 'string' ? { duration: t } : t;
9
10
  }
11
+ /** Bookkeeping the nesting sugar shares across every scope of one process. */
12
+ class SugarRegistry {
13
+ /** Flow ids the builder wrote by implication rather than from an explicit `.sequenceFlow()`. */
14
+ impliedFlowIds = new Set();
15
+ /**
16
+ * Ids to give implied edges, keyed `source\u0000target` — see {@link ScopeBuilder.flowIds}.
17
+ *
18
+ * An implied edge has no call of its own, so there is nowhere to name it and its id
19
+ * is derived. This is the one place an author (in practice, `decompile`) can say what
20
+ * a derived id should be instead.
21
+ */
22
+ pinnedFlowIds = new Map();
23
+ /** Variables an `errorVar` asked for — declared at build unless the author declared the id. */
24
+ autoVars = [];
25
+ }
26
+ const GATEWAY_NODE_KINDS = new Set(['exclusiveGateway', 'parallelGateway', 'inclusiveGateway', 'eventBasedGateway']);
27
+ /**
28
+ * Whether the node serializes to a start event — nothing may flow INTO it.
29
+ *
30
+ * Exported because `graph.ts` needs the SAME answer: `check --graph` decides what is
31
+ * `[NO INCOMING]` and what is merely an entry point, and a second copy of this test
32
+ * drifted — it missed the `typed` arm, so a registry type whose element IS a start
33
+ * event was reported unreachable.
34
+ *
35
+ * @param n - The node.
36
+ * @returns True when the node is an entry point of its own.
37
+ * @internal
38
+ */
39
+ export function isStartLike(n) {
40
+ if (n.kind === 'startEvent')
41
+ return true;
42
+ if (n.kind === 'connectorEvent')
43
+ return n.subscribe === 'trigger';
44
+ if (n.kind !== 'typed')
45
+ return false;
46
+ try {
47
+ return registryType(n.type, n.spec).element === 'bpmn:StartEvent';
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ /** Whether a pending exit may be wired into this node when it is placed. */
54
+ function isContinuationTarget(n) {
55
+ if (isStartLike(n) || n.kind === 'boundaryEvent')
56
+ return false;
57
+ if (n.kind === 'subProcess' && n.triggeredByEvent)
58
+ return false;
59
+ return true;
60
+ }
61
+ /**
62
+ * Whether, in a `sequence` scope, the path continues out of this node to the next
63
+ * one placed. An end event ends it; a gateway's fan-out is never implied; a boundary
64
+ * event and an event sub-process start their own paths.
65
+ */
66
+ function opensPath(n) {
67
+ if (n.kind === 'endEvent' || n.kind === 'boundaryEvent' || GATEWAY_NODE_KINDS.has(n.kind))
68
+ return false;
69
+ if (n.kind === 'subProcess' && n.triggeredByEvent)
70
+ return false;
71
+ return true;
72
+ }
73
+ /** Whether the node is something an event-based gateway can wait on. */
74
+ function waitsForEvent(n) {
75
+ if (n.kind === 'intermediateCatchEvent')
76
+ return true;
77
+ if (n.kind === 'connectorEvent')
78
+ return n.subscribe === 'event';
79
+ if (n.kind !== 'typed')
80
+ return false;
81
+ try {
82
+ const element = registryType(n.type, n.spec).element;
83
+ return element === 'bpmn:IntermediateCatchEvent' || element === 'bpmn:ReceiveTask';
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ /**
90
+ * The product's error-capture row: an output reading `=Error` into a variable.
91
+ *
92
+ * Which extension carries it depends on the element. A boundary event takes a
93
+ * `uipath:activity`, as every error boundary in the real exports does; a start event
94
+ * cannot (`uip maestro bpmn validate`: *"uipath:Activity cannot be attached to
95
+ * bpmn:startEvent"*), so an event sub-process's error start carries the row on a
96
+ * `uipath:mapping` instead — the same element a start event's input mapping uses.
97
+ */
98
+ function errorCapturePayload(varId, on) {
99
+ return {
100
+ type: on === 'boundaryEvent' ? 'uipath:Activity' : 'BPMN.Variables',
101
+ outputRows: [{ name: 'Error', type: 'jsonSchema', var: varId, source: '=Error' }],
102
+ };
103
+ }
10
104
  // ─── Definitions registry (shared across the top process + sub-processes) ─────
11
105
  /**
12
106
  * Collects `uipath:binding` declarations, de-duped by id.
@@ -95,13 +189,34 @@ function uniqueId(prefix, index) {
95
189
  * element id (`Error_1`) — could never match a real failure, so the handler was
96
190
  * unreachable. Passing `{ name, code }` still sets them independently.
97
191
  */
98
- function toErrorDef(defs, error) {
192
+ function toErrorDef(defs, error, where) {
99
193
  // `true` is catch-any: no `bpmn:error` is declared and no `errorRef` is written.
100
194
  if (error === true)
101
195
  return { type: 'error' };
102
196
  const spec = typeof error === 'string' ? { name: error } : error;
197
+ // A BLANK name declares a `bpmn:error` with nothing to match on, which the platform
198
+ // rejects (ERROR_BOUNDARY_EVENT_EMPTY_ERROR_REF on a boundary,
199
+ // ERROR_END_EVENT_MISSING_EXCEPTION on an end event) after a whole toolchain cycle.
200
+ // `error: true` is the way to say "any error"; a name is the way to say which.
201
+ if (spec.name.trim() === '') {
202
+ throw new Error(`${where}: the error name is empty, so the declared \`bpmn:error\` has nothing to match on. ` +
203
+ 'Pass `error: true` to catch any error, or a name — `error: "PaymentFailed"`, or ' +
204
+ '`error: { name: "PaymentFailed", code: "PAYMENT_FAILED" }`.');
205
+ }
103
206
  return { type: 'error', errorRef: defs.errorRef(spec.name, spec.code ?? spec.name) };
104
207
  }
208
+ /** The error a boundary event or event sub-process catches: its ref, or catch-all. */
209
+ function errorCaught(n) {
210
+ const event = 'event' in n ? n.event : undefined;
211
+ if (event?.type !== 'error')
212
+ return undefined;
213
+ return { catchAll: event.errorRef === undefined, ref: event.errorRef };
214
+ }
215
+ /** The single start event of a built sub-process scope, when it has exactly one. */
216
+ function loneStart(nodes) {
217
+ const starts = nodes.filter((n) => n.kind === 'startEvent');
218
+ return starts.length === 1 ? starts[0] : undefined;
219
+ }
105
220
  // ─── Graph scope (shared by the top process and sub-processes) ────────────────
106
221
  /**
107
222
  * The set of node/flow/variable methods common to the process root and every
@@ -110,11 +225,11 @@ function toErrorDef(defs, error) {
110
225
  */
111
226
  class ScopeBuilder {
112
227
  /** @internal */
113
- _nodes = [];
228
+ _nodes;
114
229
  /** @internal */
115
- _flows = [];
230
+ _flows;
116
231
  /** @internal */
117
- _vars = [];
232
+ _vars;
118
233
  // An explicit field, not a constructor parameter property: an inline comment
119
234
  // on a parameter property never reaches the emitted `.d.ts` property, so the
120
235
  // `@internal` tag would be lost there.
@@ -122,18 +237,428 @@ class ScopeBuilder {
122
237
  _defs;
123
238
  /** @internal */
124
239
  _bindingRegistry;
240
+ /** @internal */
241
+ _storage;
242
+ /** @internal */
243
+ _sugar;
244
+ /** @internal How THIS scope wires siblings — an arm's is always `sequence`. */
245
+ _mode;
246
+ /** @internal The mode of the real scope this builder writes into, which a nested `.subProcess()` inherits. */
247
+ _scopeMode;
248
+ /** @internal Whether this builder is an arm or handler path rather than a real scope. */
249
+ _isArm = false;
250
+ /** @internal The id of a start event the builder synthesized for this scope (an event sub-process body). */
251
+ _synthesizedStart;
252
+ /** @internal Elements placed in a `sequence` scope with nothing leading into them, checked at close. */
253
+ _unattached = [];
254
+ /** @internal Edges waiting for the element that continues them. */
255
+ _open = [];
256
+ /** @internal Whether anything has been placed yet, so an unreachable element can be told from the first. */
257
+ _started = false;
258
+ /** @internal What left this path for good (`.goto()`), after which nothing may follow. */
259
+ _closedBy;
125
260
  /**
126
261
  * @param defs - Definitions-level message/error declarations, shared with every scope.
127
262
  * @param bindings - `uipath:binding` declarations, shared so a typed node can
128
263
  * declare one from inside a sub-process.
264
+ * @param storage - The arrays to write into; an arm shares its enclosing scope's,
265
+ * a real scope gets its own.
266
+ * @param mode - How siblings no explicit flow covers are wired — see `WiringMode`.
267
+ * @param sugar - Process-wide bookkeeping for the nesting sugar.
129
268
  * @returns The scope builder.
130
269
  */
131
- constructor(defs, bindings) {
270
+ constructor(defs, bindings, storage, mode = 'explicit', sugar = new SugarRegistry()) {
132
271
  this._defs = defs;
133
272
  this._bindingRegistry = bindings;
273
+ this._storage = storage ?? { nodes: [], flows: [], vars: [], mustClose: [] };
274
+ this._nodes = this._storage.nodes;
275
+ this._flows = this._storage.flows;
276
+ this._vars = this._storage.vars;
277
+ this._mode = mode;
278
+ this._scopeMode = mode;
279
+ this._sugar = sugar;
280
+ }
281
+ /**
282
+ * Choose how this scope wires the elements no explicit `.sequenceFlow()` covers.
283
+ *
284
+ * @remarks
285
+ * `'explicit'` is the default: siblings are not wired to each other, only the exits
286
+ * of a nested construct continue to the next element. `'sequence'` wires consecutive
287
+ * siblings in order, so a linear process needs no `.sequenceFlow()` at all.
288
+ *
289
+ * Three rules keep `'sequence'` local and reviewable. **Explicit wins**: an element
290
+ * that already has an explicit outgoing flow is not also wired to the next one, so a
291
+ * hand-written edge never gains a silent twin. **A bare gateway implies nothing
292
+ * outward**: it is wired into, but its outgoing flows are yours to write (or use
293
+ * `.choose()` / `.fork()` / `.race()`, which imply theirs). **Nothing dangles
294
+ * silently**: an element nothing leads into, or a path that does not end, is refused
295
+ * at `.build()` rather than emitted. `bpmn check --graph` prints the wiring that
296
+ * resulted, implied edges marked, so a change can be reviewed against what was
297
+ * inferred rather than what was typed.
298
+ *
299
+ * Must come before the scope's first element. A `.subProcess()` body inherits its
300
+ * enclosing scope's mode and may set its own; an arm or handler body is always
301
+ * `sequence` and refuses the call.
302
+ *
303
+ * @example
304
+ * **A linear process with no sequence flows written**
305
+ * ```ts
306
+ * bpmn('intake')
307
+ * .flowMode('sequence')
308
+ * .var('note', 'string', { default: '' })
309
+ * .startEvent('start')
310
+ * .task('assign', { set: { note: 'hello' } })
311
+ * .scriptTask('log', { script: 'return note;', inputs: { note: '=vars.note' } })
312
+ * .endEvent('done')
313
+ * .build();
314
+ * ```
315
+ *
316
+ * @param mode - `'explicit'` or `'sequence'`.
317
+ * @returns This builder, so calls chain.
318
+ * @throws If called inside an arm or handler body, or after an element has been placed.
319
+ */
320
+ flowMode(mode) {
321
+ if (this._isArm) {
322
+ throw new Error(`.flowMode('${mode}') is not available inside an arm or handler body — those always chain in order.`);
323
+ }
324
+ if (this._nodes.length > 0) {
325
+ throw new Error(`.flowMode('${mode}') must come before the scope's first element; "${this._nodes[0].id}" is already placed.`);
326
+ }
327
+ this._mode = mode;
328
+ this._scopeMode = mode;
329
+ return this;
134
330
  }
135
331
  push(node) {
332
+ this.place(node);
333
+ return this;
334
+ }
335
+ /**
336
+ * Place an activity and run its body, in which boundary events are declared.
337
+ *
338
+ * @param node - The activity node.
339
+ * @param body - Its boundary handlers, if any.
340
+ * @returns This builder, so calls chain.
341
+ */
342
+ pushActivity(node, body) {
343
+ this.place(node);
344
+ if (body)
345
+ body(new ActivityBuilder(this, node.id));
346
+ return this;
347
+ }
348
+ /**
349
+ * Place a node: wire every pending exit into it, record it, and — in a
350
+ * `sequence` scope — leave it as the open end the next element continues from.
351
+ *
352
+ * @param node - The node to place.
353
+ * @param viaSugar - Set when the builder itself is placing a gateway it synthesized,
354
+ * which a `sequence` scope otherwise refuses.
355
+ * @internal
356
+ */
357
+ place(node, viaSugar = false) {
358
+ if (this._closedBy !== undefined) {
359
+ throw new Error(`"${node.id}" follows ${this._closedBy}, which already left this path — nothing can come after it.`);
360
+ }
361
+ if (this._mode === 'sequence') {
362
+ if (isStartLike(node)) {
363
+ if (this._isArm) {
364
+ throw new Error(`startEvent "${node.id}": a body has no start event of its own — it begins where the enclosing element hands over to it.`);
365
+ }
366
+ if (this._synthesizedStart !== undefined) {
367
+ throw new Error(`startEvent "${node.id}": an event sub-process has exactly one start event, and this one's is ` +
368
+ `synthesized as "${this._synthesizedStart}" — write what follows it instead.`);
369
+ }
370
+ // Explicit wins here as everywhere: a plain exit whose element already has an
371
+ // explicit outgoing flow was wired by hand and is not open — the same test
372
+ // `continueTo` applies before implying an edge and `closeScope` applies at the
373
+ // end. Only an exit nothing continues, or one carrying a condition or default
374
+ // (a branch with nowhere to go), blocks a new chain.
375
+ const dangling = this._open.filter((e) => e.condition !== undefined || e.defaultOf !== undefined || !this.hasExplicitOutgoing(e.source));
376
+ if (dangling.length > 0) {
377
+ throw new Error(`startEvent "${node.id}" begins a new chain while "${dangling.map((e) => e.source).join('", "')}" is still ` +
378
+ `open — end the current path with an end event before starting another.`);
379
+ }
380
+ }
381
+ // Inside an arm a bare gateway is refused outright: its fan-out cannot be implied
382
+ // and nothing after it could be reached. In a real scope it is allowed — it is
383
+ // wired INTO, and its outgoing flows are the author's to write — and whatever
384
+ // follows it is checked at close for an explicit incoming edge.
385
+ if (GATEWAY_NODE_KINDS.has(node.kind) && !viaSugar && this._isArm) {
386
+ throw new Error(`${node.kind} "${node.id}": inside a body, branch with .choose() / .fork() / .race() — ` +
387
+ `a bare gateway's outgoing flows cannot be implied.`);
388
+ }
389
+ }
390
+ const target = isContinuationTarget(node);
391
+ if (target) {
392
+ if (this._mode === 'sequence' && this._started && this._open.length === 0) {
393
+ if (this._isArm) {
394
+ throw new Error(`"${node.id}" is unreachable: the path ended before it (an end event, or every arm of a branch), so nothing leads to it.`);
395
+ }
396
+ // Placed with nothing implied into it. Legal only if an explicit flow leads
397
+ // here — which may be written later — so it is checked when the scope closes.
398
+ const previous = this._nodes[this._nodes.length - 1];
399
+ this._unattached.push({ id: node.id, after: previous ? previous.id : '' });
400
+ }
401
+ this.continueTo(node.id);
402
+ }
403
+ this._nodes.push(node);
404
+ this._started = true;
405
+ if (this._mode === 'sequence' && opensPath(node))
406
+ this._open = [{ source: node.id }];
407
+ else if (target)
408
+ this._open = [];
409
+ }
410
+ /** Wire every pending exit into `target`, except one the author already wired by hand. */
411
+ continueTo(target) {
412
+ for (const exit of this._open) {
413
+ // Explicit wins: a plain exit whose element already has an explicit outgoing
414
+ // flow was wired by hand, and an implied edge beside it would be a second,
415
+ // silent fan-out. An exit that CARRIES something — a condition, a default —
416
+ // is an edge in its own right and is always written.
417
+ if (exit.condition === undefined && exit.defaultOf === undefined && this.hasExplicitOutgoing(exit.source))
418
+ continue;
419
+ this.writeImpliedFlow(exit, target);
420
+ }
421
+ this._open = [];
422
+ }
423
+ hasExplicitOutgoing(source) {
424
+ return this._flows.some((f) => f.source === source && !this._sugar.impliedFlowIds.has(f.id));
425
+ }
426
+ /**
427
+ * Write the flow a pending exit stands for, now that its target is known.
428
+ *
429
+ * @param exit - The exit.
430
+ * @param target - The element it flows into.
431
+ * @internal
432
+ */
433
+ writeImpliedFlow(exit, target) {
434
+ // An id the author pinned for this pair wins over the derived one. Nothing else
435
+ // changes: the edge is still implied, still counted as implied, and a gateway
436
+ // `default` still ends up naming whichever id the edge got.
437
+ let id = this._sugar.pinnedFlowIds.get(`${exit.source}\u0000${target}`) ?? `Flow_${exit.source}_${target}`;
438
+ const existing = this._flows.find((f) => f.id === id);
439
+ if (existing && existing.source === exit.source && existing.target === target) {
440
+ // The same edge, already there — an author's explicit restatement, or two
441
+ // exits converging on one target from one source. One edge, not two.
442
+ if (exit.defaultOf)
443
+ exit.defaultOf.default = id;
444
+ return;
445
+ }
446
+ if (existing) {
447
+ // A DIFFERENT edge already owns that id. `Flow_<source>_<target>` is ambiguous
448
+ // the moment an element id contains an underscore: `a_b → c` and `a → b_c` both
449
+ // spell `Flow_a_b_c`. Left alone the second edge ships with a duplicate id and
450
+ // `check` reports DUPLICATE_DOCUMENT_ID for a flow the author never named and,
451
+ // because the edge is implied, has no way to rename. So the id is suffixed until
452
+ // it is free. An author who wants a specific id still writes `.sequenceFlow()`.
453
+ let n = 2;
454
+ while (this._flows.some((f) => f.id === `${id}_${n}`))
455
+ n++;
456
+ id = `${id}_${n}`;
457
+ }
458
+ this._flows.push({ id, source: exit.source, target, name: exit.name, condition: exit.condition, implied: true });
459
+ this._sugar.impliedFlowIds.add(id);
460
+ if (exit.defaultOf)
461
+ exit.defaultOf.default = id;
462
+ }
463
+ /**
464
+ * Check the implied wiring of a real scope once its author's function has
465
+ * returned: every path a handler had to end has ended, and every exit a construct
466
+ * left open reached something.
467
+ *
468
+ * @param what - The scope, for the message.
469
+ * @internal
470
+ */
471
+ closeScope(what) {
472
+ // The duplicate-error refusals again, over the FINISHED node list.
473
+ //
474
+ // `_attach` and `.eventSubProcess()` catch the collision at the call site, which is
475
+ // where the best message lives, but only for the sugar: `.boundaryEvent()` and a
476
+ // hand-rolled `.subProcess(…, { triggeredByEvent: true })` never go through them, so
477
+ // two explicitly-written handlers on one activity, or two hand-rolled nets in one
478
+ // scope, reached `validate`. Order mattered too — the call-site scan sees earlier
479
+ // nodes only. Repeating the test here costs one pass and covers every spelling and
480
+ // both orders.
481
+ const seenOnActivity = new Map();
482
+ const seenNets = [];
483
+ for (const n of this._nodes) {
484
+ const here = n.kind === 'boundaryEvent'
485
+ ? { key: n.attachedTo, list: seenOnActivity.get(n.attachedTo) ?? [] }
486
+ : n.kind === 'subProcess' && n.triggeredByEvent === true
487
+ ? { key: undefined, list: seenNets }
488
+ : undefined;
489
+ if (!here)
490
+ continue;
491
+ const start = n.kind === 'subProcess' ? loneStart(n.nodes) : undefined;
492
+ const caught = n.kind === 'boundaryEvent' ? errorCaught(n) : start === undefined ? undefined : errorCaught(start);
493
+ if (caught === undefined)
494
+ continue;
495
+ const clash = here.list.find((o) => o.catchAll === caught.catchAll && o.ref === caught.ref);
496
+ if (clash !== undefined) {
497
+ const where = here.key !== undefined ? `on "${here.key}"` : 'in this scope';
498
+ throw new Error(`"${n.id}" catches the same error as "${clash.id}" ${where} ` +
499
+ `(${caught.catchAll ? 'both catch ANY error' : `both catch "${caught.ref}"`}), so one of the two can ` +
500
+ 'never run. Catch a specific error in each, or keep the one catch-all.');
501
+ }
502
+ here.list.push({ id: n.id, catchAll: caught.catchAll, ref: caught.ref });
503
+ if (here.key !== undefined)
504
+ seenOnActivity.set(here.key, here.list);
505
+ }
506
+ for (const { id, after } of this._unattached) {
507
+ if (!this._flows.some((f) => f.target === id)) {
508
+ throw new Error(`"${id}" has no incoming flow. In a sequence scope it was placed after "${after}", which does not ` +
509
+ `continue to it (a bare gateway's outgoing flows are never implied; an end event ends the path). ` +
510
+ `Wire it with .sequenceFlow(…, "${id}"), or place it after an element that continues.`);
511
+ }
512
+ }
513
+ for (const { id, reason } of this._storage.mustClose) {
514
+ if (!this._flows.some((f) => f.source === id)) {
515
+ throw new Error(`${reason} ends at "${id}", which has no outgoing flow. A non-interrupting handler runs beside a token ` +
516
+ `that will reach the next statement anyway, so its path cannot rejoin there: end it with an end event, ` +
517
+ `leave it with .goto(), or wire it with .sequenceFlow("${id}", …).`);
518
+ }
519
+ }
520
+ this._storage.mustClose.length = 0;
521
+ for (const exit of this._open) {
522
+ const who = exit.origin ?? what;
523
+ if (exit.condition !== undefined || exit.defaultOf !== undefined) {
524
+ throw new Error(`${who} has nowhere to go: nothing follows the branch in ${what}. Add the element that comes ` +
525
+ `next, or end every arm inside its own body.`);
526
+ }
527
+ if (!this._flows.some((f) => f.source === exit.source)) {
528
+ throw new Error(`${who} ends at "${exit.source}", which has no outgoing flow, and nothing follows it in ${what} ` +
529
+ `to continue it. Add the element that comes next, end the path with an end event, or wire it with ` +
530
+ `.sequenceFlow("${exit.source}", …).`);
531
+ }
532
+ }
533
+ this._open = [];
534
+ }
535
+ /**
536
+ * The definitions registry, for the activity builder to intern a message or error.
537
+ *
538
+ * @returns The registry shared by every scope of this process.
539
+ * @internal
540
+ */
541
+ _defsOf() {
542
+ return this._defs;
543
+ }
544
+ /**
545
+ * This scope's variable declarations, for an `errorVar` to be declared into.
546
+ *
547
+ * @returns The array the scope declares variables into.
548
+ * @internal
549
+ */
550
+ _varsOf() {
551
+ return this._vars;
552
+ }
553
+ /**
554
+ * Attach a boundary event to an activity of this scope and run its handler path.
555
+ *
556
+ * @param activityId - The activity the boundary attaches to.
557
+ * @param suffix - `onError` / `onTimer` / `onMessage`, for the derived id.
558
+ * @param spec - The event definition and the options it came with.
559
+ * @param body - The handler path.
560
+ * @internal
561
+ */
562
+ _attach(activityId, suffix, spec, body) {
563
+ const { opts } = spec;
564
+ const id = opts.id ?? `${activityId}_${suffix}`;
565
+ if (this._nodes.some((n) => n.id === id)) {
566
+ throw new Error(`"${activityId}" already has a boundary event "${id}"; a second .${suffix}() on one activity needs its own \`id\`.`);
567
+ }
568
+ if (opts.errorVar !== undefined && opts.payload !== undefined) {
569
+ throw new Error(`.${suffix}() on "${activityId}": pass \`errorVar\` or \`payload\`, not both — both write the event's output row.`);
570
+ }
571
+ // Two handlers on one activity catching the SAME error: whichever the runtime picks,
572
+ // the other is dead, and the platform rejects the pair outright
573
+ // (MULTIPLE_CATCH_ALL_BOUNDARY_EVENTS_ON_TASK for two catch-alls,
574
+ // DUPLICATE_ERROR_BOUNDARY_EVENT_ON_TASK for two of the same ref). Both handlers are
575
+ // in hand here, so this is a refusal rather than a diagnostic a rung later.
576
+ const caught = errorCaught({ kind: 'boundaryEvent', id, attachedTo: activityId, event: spec.event });
577
+ if (caught !== undefined) {
578
+ const clash = this._nodes.find((n) => {
579
+ if (n.kind !== 'boundaryEvent' || n.attachedTo !== activityId)
580
+ return false;
581
+ const other = errorCaught(n);
582
+ return other !== undefined && other.catchAll === caught.catchAll && other.ref === caught.ref;
583
+ });
584
+ if (clash !== undefined) {
585
+ throw new Error(`.${suffix}() on "${activityId}" catches the same error as "${clash.id}" ` +
586
+ `(${caught.catchAll ? 'both catch ANY error' : `both catch "${caught.ref}"`}), so one of the two can never ` +
587
+ 'run. Catch a specific error in each handler, or keep the one catch-all.');
588
+ }
589
+ }
590
+ const node = {
591
+ kind: 'boundaryEvent',
592
+ id,
593
+ name: opts.name,
594
+ attachedTo: activityId,
595
+ cancelActivity: spec.interrupting,
596
+ event: spec.event,
597
+ payload: opts.payload ?? (opts.errorVar !== undefined ? errorCapturePayload(opts.errorVar, 'boundaryEvent') : undefined),
598
+ };
599
+ // Straight into the scope, not through `place`: a boundary event is not a
600
+ // continuation of anything, and must not consume the exits waiting for one.
136
601
  this._nodes.push(node);
602
+ if (opts.errorVar !== undefined)
603
+ this._sugar.autoVars.push({ id: opts.errorVar, vars: this._vars, elementId: id });
604
+ const arm = new PathBuilder(this._defs, this._bindingRegistry, this._storage, this._sugar, [{ source: id, origin: `the handler "${id}" on "${activityId}"` }], this._scopeMode);
605
+ body?.(arm);
606
+ const tails = arm._finish();
607
+ if (spec.interrupting) {
608
+ // The activity was cancelled, so the handler's path IS the continuation: it
609
+ // rejoins at the next statement, as the activity's own path would have.
610
+ this._open.push(...tails);
611
+ }
612
+ else {
613
+ // The activity is still running and will reach the next statement itself;
614
+ // the handler's path must end on its own — checked when the scope closes,
615
+ // so an explicit flow written later still counts.
616
+ for (const t of tails)
617
+ this._storage.mustClose.push({ id: t.source, reason: `The non-interrupting handler "${id}" on "${activityId}"` });
618
+ }
619
+ }
620
+ /**
621
+ * The shared lowering of `choose` / `fork` / `race`: a splitting gateway, one
622
+ * arm per branch, and a join only when more than one arm falls through.
623
+ *
624
+ * @param split - The gateway that opens the construct.
625
+ * @param joinKind - The gateway kind that closes it, when one is needed.
626
+ * @param arms - Each arm's entry edge and body.
627
+ * @param what - The construct, for messages.
628
+ * @param mustWait - `race`: each arm has to begin with something that waits.
629
+ */
630
+ branch(split, joinKind, arms, what, mustWait = false, join = {}) {
631
+ this.place(split, true);
632
+ const exits = [];
633
+ arms.forEach((arm, i) => {
634
+ const ab = new PathBuilder(this._defs, this._bindingRegistry, this._storage, this._sugar, [arm.entry], this._scopeMode);
635
+ arm.body?.(ab);
636
+ if (mustWait) {
637
+ const first = ab._first;
638
+ if (!first || !waitsForEvent(first)) {
639
+ throw new Error(`${what}: arm ${i + 1} must begin with what it waits for — an intermediateCatchEvent (message or timer), ` +
640
+ `a receiveMessage, or a waitForEvent — but begins with ${first ? `${first.kind} "${first.id}"` : 'nothing'}.`);
641
+ }
642
+ }
643
+ exits.push(...ab._finish());
644
+ });
645
+ const empties = exits.filter((e) => e.source === split.id);
646
+ if (empties.length > 1) {
647
+ throw new Error(`${what} has ${empties.length} arms with no body; at most one arm may be empty.`);
648
+ }
649
+ if (exits.length >= 2) {
650
+ // Only when more than one arm actually reaches the continuation: a single
651
+ // fall-through needs no join, and emitting one would be the SUPERFLUOUS_GATEWAY
652
+ // the construct exists to make unwritable.
653
+ const joinNode = { kind: joinKind, id: join.id ?? `${split.id}_join`, name: join.name };
654
+ for (const e of exits)
655
+ this.writeImpliedFlow(e, joinNode.id);
656
+ this._nodes.push(joinNode);
657
+ this._open = [{ source: joinNode.id, origin: `the join of ${what}` }];
658
+ }
659
+ else {
660
+ this._open = exits;
661
+ }
137
662
  return this;
138
663
  }
139
664
  // ── Events ──
@@ -162,17 +687,21 @@ class ScopeBuilder {
162
687
  if (opts.trigger !== undefined && opts.payload !== undefined) {
163
688
  throw new Error(`startEvent "${id}": \`trigger\` and \`payload\` both write the event's extension element; pass one.`);
164
689
  }
690
+ if (opts.interrupting === false && opts.error !== undefined) {
691
+ throw new Error(`startEvent "${id}": an error start is always interrupting — the throw has already ended the scope it came from.`);
692
+ }
165
693
  return this.push({
166
694
  kind: 'startEvent',
167
695
  id,
168
696
  name: opts.name,
169
697
  payload: opts.payload,
698
+ interrupting: opts.interrupting === false ? false : undefined,
170
699
  // `error` is handled here rather than in `startCatchDef` because it needs the
171
700
  // definitions registry to intern the `bpmn:error`, exactly as `endEvent` does —
172
701
  // and because an intermediate catch event (the other `startCatchDef` caller)
173
702
  // cannot carry one: BPMN catches errors at a boundary or an event sub-process
174
703
  // start, never mid-flow.
175
- event: opts.error !== undefined ? toErrorDef(this._defs, opts.error) : this.startCatchDef(opts),
704
+ event: opts.error !== undefined ? toErrorDef(this._defs, opts.error, `startEvent "${id}"`) : this.startCatchDef(opts),
176
705
  trigger: opts.trigger === undefined
177
706
  ? undefined
178
707
  : { type: 'Intsvc.TimerTrigger', outputVar: opts.trigger === true ? undefined : opts.trigger.outputVar },
@@ -190,7 +719,7 @@ class ScopeBuilder {
190
719
  if (opts.terminate)
191
720
  event = { type: 'terminate' };
192
721
  else if (opts.error !== undefined)
193
- event = toErrorDef(this._defs, opts.error);
722
+ event = toErrorDef(this._defs, opts.error, `endEvent "${id}"`);
194
723
  else if (opts.message !== undefined)
195
724
  event = { type: 'message', messageRef: this._defs.messageRef(opts.message) };
196
725
  return this.push({ kind: 'endEvent', id, name: opts.name, event, payload: opts.payload });
@@ -232,7 +761,7 @@ class ScopeBuilder {
232
761
  boundaryEvent(id, opts) {
233
762
  let event;
234
763
  if (opts.error !== undefined)
235
- event = toErrorDef(this._defs, opts.error);
764
+ event = toErrorDef(this._defs, opts.error, `boundaryEvent "${id}"`);
236
765
  else if (opts.timer !== undefined)
237
766
  event = { type: 'timer', timer: normTimer(opts.timer) };
238
767
  else if (opts.message !== undefined)
@@ -303,15 +832,17 @@ class ScopeBuilder {
303
832
  *
304
833
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
305
834
  * @param opts - The Jint JavaScript `script`, and its input/output mappings.
835
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
306
836
  * @returns This builder, so calls chain.
307
837
  */
308
- scriptTask(id, opts) {
309
- return this.push({
838
+ scriptTask(id, opts, body) {
839
+ return this.pushActivity({
310
840
  kind: 'scriptTask',
311
841
  id,
312
842
  name: opts.name,
313
843
  script: opts.script,
314
844
  scriptFormat: opts.scriptFormat ?? 'JavaScript',
845
+ scriptVersion: opts.scriptVersion ?? 'v3',
315
846
  inputs: opts.inputs ?? {},
316
847
  outputs: opts.outputs ?? {},
317
848
  outputRows: opts.outputRows,
@@ -321,17 +852,18 @@ class ScopeBuilder {
321
852
  retry: opts.retry,
322
853
  errorMapping: opts.errorMapping,
323
854
  loop: opts.loop,
324
- });
855
+ }, body);
325
856
  }
326
857
  /**
327
858
  * A plain task that assigns variables (`BPMN.Variables`).
328
859
  *
329
860
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
330
861
  * @param opts - `name`, and the variable assignments the task makes.
862
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
331
863
  * @returns This builder, so calls chain.
332
864
  */
333
- task(id, opts = {}) {
334
- return this.push({
865
+ task(id, opts = {}, body) {
866
+ return this.pushActivity({
335
867
  kind: 'task',
336
868
  id,
337
869
  name: opts.name,
@@ -340,7 +872,7 @@ class ScopeBuilder {
340
872
  retry: opts.retry,
341
873
  loop: opts.loop,
342
874
  errorMapping: opts.errorMapping,
343
- });
875
+ }, body);
344
876
  }
345
877
  /**
346
878
  * A task element carrying NO `uipath:*` payload — an abstract task, in BPMN's terms.
@@ -360,6 +892,7 @@ class ScopeBuilder {
360
892
  *
361
893
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
362
894
  * @param opts - `name`, and which task `element` to emit (default `bpmn:task`).
895
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
363
896
  * @returns This builder, so calls chain.
364
897
  *
365
898
  * @example
@@ -370,8 +903,8 @@ class ScopeBuilder {
370
903
  * .endEvent('e')
371
904
  * ```
372
905
  */
373
- plainTask(id, opts = {}) {
374
- return this.push({
906
+ plainTask(id, opts = {}, body) {
907
+ return this.pushActivity({
375
908
  kind: 'plainTask',
376
909
  id,
377
910
  name: opts.name,
@@ -380,18 +913,21 @@ class ScopeBuilder {
380
913
  retry: opts.retry,
381
914
  errorMapping: opts.errorMapping,
382
915
  loop: opts.loop,
383
- });
916
+ }, body);
384
917
  }
385
- connector(id, a, b, c, d) {
918
+ connector(id, a, b, c, d, e) {
919
+ // The two forms put `opts` and `body` one position apart; sort them out first.
920
+ const stringly = typeof a === 'string';
921
+ const bpmnOpts = (stringly ? (typeof d === 'function' ? undefined : d) : c) ?? {};
922
+ const body = stringly ? e : typeof d === 'function' ? d : undefined;
386
923
  // Reuse the shared connector() overload to parse descriptor-or-string into a
387
924
  // ConnectorSpec; then carry its fields onto the BPMN node.
388
- const spec = typeof a === 'string'
389
- ? makeConnector(a, b, c, d)
390
- : makeConnector(a, b, c);
925
+ const spec = stringly
926
+ ? makeConnector(a, b, c, bpmnOpts)
927
+ : makeConnector(a, b, bpmnOpts);
391
928
  if (spec.kind !== 'connector')
392
929
  throw new Error('connector(): unexpected spec kind');
393
- const bpmnOpts = (typeof a === 'string' ? d : c) ?? {};
394
- return this.push({
930
+ return this.pushActivity({
395
931
  kind: 'connector',
396
932
  id,
397
933
  name: bpmnOpts.name,
@@ -406,7 +942,7 @@ class ScopeBuilder {
406
942
  skipCondition: bpmnOpts.skipCondition,
407
943
  retry: bpmnOpts.retry,
408
944
  loop: bpmnOpts.loop,
409
- });
945
+ }, body);
410
946
  }
411
947
  /**
412
948
  * An **HTTP request** service task (`bpmn:sendTask` +
@@ -439,11 +975,12 @@ class ScopeBuilder {
439
975
  *
440
976
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
441
977
  * @param opts - The request, plus `name`, `outputVar`, and the shared activity options.
978
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
442
979
  * @returns This builder, so calls chain.
443
980
  */
444
- http(id, opts) {
445
- const { name, url, method, headers, parameters, body, outputs, outputVar, skipCondition, retry, loop, errorMapping } = opts;
446
- return this.push({
981
+ http(id, opts, body) {
982
+ const { name, url, method, headers, parameters, body: requestBody, outputs, outputVar, skipCondition, retry, loop, errorMapping } = opts;
983
+ return this.pushActivity({
447
984
  kind: 'typed',
448
985
  id,
449
986
  name,
@@ -458,7 +995,7 @@ class ScopeBuilder {
458
995
  url,
459
996
  ...(headers !== undefined ? { headers } : {}),
460
997
  ...(parameters !== undefined ? { parameters } : {}),
461
- ...(body !== undefined ? { body } : {}),
998
+ ...(requestBody !== undefined ? { body: requestBody } : {}),
462
999
  },
463
1000
  inputs: {},
464
1001
  outputs,
@@ -467,7 +1004,102 @@ class ScopeBuilder {
467
1004
  retry,
468
1005
  loop,
469
1006
  errorMapping,
1007
+ }, body);
1008
+ }
1009
+ /**
1010
+ * A UiPath **Data Fabric** (Data Service) record or file operation — create,
1011
+ * update, get, query, delete a record, or download / upload / delete a record's
1012
+ * file field — as an `Intsvc.ActivityExecution` send task in the exact shape the
1013
+ * Flow→BPMN converter and Studio Web write.
1014
+ *
1015
+ * @remarks
1016
+ * The connector library ships one Data Service operation, so `.connector()` cannot
1017
+ * author the rest, and hand-filling `.activity('Intsvc.ActivityExecution', …)` gets
1018
+ * the wire details wrong in ways `validate` does not catch (a `recordId` in the body,
1019
+ * a curated `objectName` on an entity-form path, a folder binding keyed by the folder).
1020
+ * This writes each of the eight operations the way the platform's own tools do:
1021
+ * create and update in the ENTITY form (`POST /<Entity>`, `PUT /<Entity>/{id}`,
1022
+ * `objectName` = the entity), the others in the CURATED form (`entityName` as a
1023
+ * path parameter, `objectName` = the curated operation), every parameter its own
1024
+ * `path` / `query` / `file` row and the record as the one json `body` row. Needs no
1025
+ * connector library at compile time.
1026
+ *
1027
+ * The response lands in `outputVar` (default `<id>_response`) and the error payload in
1028
+ * `<id>_Error`, both declared for you.
1029
+ *
1030
+ * @param id - The element id, unique in the process and used by `sequenceFlow`.
1031
+ * @param opts - The entity, connection and folder, plus the operation and its parameters — see {@link DataServiceOpts} and {@link DataServiceOperation}.
1032
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
1033
+ * @returns This builder, so calls chain.
1034
+ */
1035
+ dataService(id, opts, body) {
1036
+ const wire = dataServiceWire(opts.entity, opts);
1037
+ const connection = this.dataServiceBinding(id, 'connection', opts.connection, 'ConnectionId', 'Data Fabric connection');
1038
+ const folder = opts.folder !== undefined
1039
+ ? this.dataServiceBinding(id, 'folder', opts.folder, 'folderKey', 'Data Fabric folder', connection.key)
1040
+ : undefined;
1041
+ const responseVar = opts.outputVar ?? `${id}_response`;
1042
+ return this.pushActivity({
1043
+ kind: 'typed',
1044
+ id,
1045
+ name: opts.name ?? wire.label,
1046
+ type: DATA_SERVICE_ACTIVITY_TYPE,
1047
+ context: {
1048
+ // `activity` is the selected-operation key the canvas validator requires
1049
+ // (EMPTY_REQUIRED_FIELD without it); the converter omits it and the CLI accepts
1050
+ // both, so it carries the same verb `operation` does — as `.connector()` writes it.
1051
+ activity: wire.context.operation,
1052
+ connectorKey: DATA_SERVICE_CONNECTOR_KEY,
1053
+ connection: connection.ref,
1054
+ ...(folder ? { folderKey: folder.ref } : {}),
1055
+ ...wire.context,
1056
+ activityConfigurationVersion: 'v1',
1057
+ metadata: {},
1058
+ },
1059
+ inputs: {},
1060
+ inputRows: wire.inputRows,
1061
+ outputRows: [
1062
+ {
1063
+ name: 'response',
1064
+ type: 'jsonSchema',
1065
+ source: '=response',
1066
+ var: responseVar,
1067
+ ...(opts.schema !== undefined ? { schema: opts.schema } : {}),
1068
+ },
1069
+ { name: 'Error', type: 'jsonSchema', source: '=Error', var: `${id}_Error`, schema: errorSchema },
1070
+ ],
1071
+ skipCondition: opts.skipCondition,
1072
+ tags: opts.tags,
1073
+ retry: opts.retry,
1074
+ errorMapping: opts.errorMapping,
1075
+ loop: opts.loop,
1076
+ }, body);
1077
+ }
1078
+ /**
1079
+ * @internal Resolve a `.dataService()` connection or folder to a `=bindings.<id>`
1080
+ * reference: an existing binding by id, else one already declared for the same
1081
+ * key, else a new one declared on the author's behalf. `companionKey` is the
1082
+ * connection's key a folder binding must carry as its `resourceKey`.
1083
+ */
1084
+ dataServiceBinding(nodeId, role, given, propertyAttribute, displayName, companionKey) {
1085
+ const byId = given.startsWith('=bindings.') ? given.slice('=bindings.'.length) : given;
1086
+ const same = (b) => b.resource === 'Connection' && b.propertyAttribute.toLowerCase() === propertyAttribute.toLowerCase();
1087
+ const existing = this._bindingRegistry.bindings.find((b) => b.id === byId) ??
1088
+ this._bindingRegistry.bindings.find((b) => same(b) && b.default === given);
1089
+ if (existing) {
1090
+ // A folder given as a binding the author declared is re-keyed at serialize, once
1091
+ // its companion is known — see `folderCompanionKeys` in serialize.ts.
1092
+ return { ref: `=bindings.${existing.id}`, key: existing.resourceKey ?? existing.default ?? existing.id };
1093
+ }
1094
+ const decl = this._bindingRegistry.declare({
1095
+ id: `${nodeId}_${role}`,
1096
+ name: displayName,
1097
+ resource: 'Connection',
1098
+ propertyAttribute,
1099
+ default: given,
1100
+ resourceKey: companionKey ?? given,
470
1101
  });
1102
+ return { ref: `=bindings.${decl.id}`, key: decl.resourceKey ?? given };
471
1103
  }
472
1104
  /**
473
1105
  * @internal Turn a literal into a binding reference where the platform demands
@@ -504,8 +1136,8 @@ class ScopeBuilder {
504
1136
  return out;
505
1137
  }
506
1138
  /** @internal Shared body of the Orchestrator process-invocation methods. */
507
- orchestratorNode(id, type, opts) {
508
- return this.push({
1139
+ orchestratorNode(id, type, opts, body) {
1140
+ return this.pushActivity({
509
1141
  kind: 'typed',
510
1142
  id,
511
1143
  name: opts.name,
@@ -523,7 +1155,7 @@ class ScopeBuilder {
523
1155
  retry: opts.retry,
524
1156
  errorMapping: opts.errorMapping,
525
1157
  loop: opts.loop,
526
- });
1158
+ }, body);
527
1159
  }
528
1160
  /**
529
1161
  * Start an **RPA process** and wait for it (`Orchestrator.StartJob`).
@@ -541,20 +1173,22 @@ class ScopeBuilder {
541
1173
  *
542
1174
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
543
1175
  * @param opts - Which process, folder, and input arguments.
1176
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
544
1177
  * @returns This builder, so calls chain.
545
1178
  */
546
- startProcess(id, opts) {
547
- return this.orchestratorNode(id, 'Orchestrator.StartJob', opts);
1179
+ startProcess(id, opts, body) {
1180
+ return this.orchestratorNode(id, 'Orchestrator.StartJob', opts, body);
548
1181
  }
549
1182
  /**
550
1183
  * Start an **agent** and wait for it (`Orchestrator.StartAgentJob`).
551
1184
  *
552
1185
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
553
1186
  * @param opts - Which agent process, folder, and input arguments.
1187
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
554
1188
  * @returns This builder, so calls chain.
555
1189
  */
556
- startAgent(id, opts) {
557
- return this.orchestratorNode(id, 'Orchestrator.StartAgentJob', opts);
1190
+ startAgent(id, opts, body) {
1191
+ return this.orchestratorNode(id, 'Orchestrator.StartAgentJob', opts, body);
558
1192
  }
559
1193
  /**
560
1194
  * Invoke an **agentic process** as a call activity
@@ -562,10 +1196,11 @@ class ScopeBuilder {
562
1196
  *
563
1197
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
564
1198
  * @param opts - Which process, folder, input arguments, and whether to wait.
1199
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
565
1200
  * @returns This builder, so calls chain.
566
1201
  */
567
- startAgenticProcess(id, opts) {
568
- return this.orchestratorNode(id, opts.async ? 'Orchestrator.StartAgenticProcessAsync' : 'Orchestrator.StartAgenticProcess', opts);
1202
+ startAgenticProcess(id, opts, body) {
1203
+ return this.orchestratorNode(id, opts.async ? 'Orchestrator.StartAgenticProcessAsync' : 'Orchestrator.StartAgenticProcess', opts, body);
569
1204
  }
570
1205
  /**
571
1206
  * Invoke a **case-management process** as a call activity
@@ -573,10 +1208,11 @@ class ScopeBuilder {
573
1208
  *
574
1209
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
575
1210
  * @param opts - Which case process, folder, input arguments, and whether to wait.
1211
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
576
1212
  * @returns This builder, so calls chain.
577
1213
  */
578
- startCaseProcess(id, opts) {
579
- return this.orchestratorNode(id, opts.async ? 'Orchestrator.StartCaseMgmtProcessAsync' : 'Orchestrator.StartCaseMgmtProcess', opts);
1214
+ startCaseProcess(id, opts, body) {
1215
+ return this.orchestratorNode(id, opts.async ? 'Orchestrator.StartCaseMgmtProcessAsync' : 'Orchestrator.StartCaseMgmtProcess', opts, body);
580
1216
  }
581
1217
  /**
582
1218
  * Execute an **API workflow**, fire-and-forget
@@ -588,10 +1224,11 @@ class ScopeBuilder {
588
1224
  *
589
1225
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
590
1226
  * @param opts - Which workflow, folder, and input arguments.
1227
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
591
1228
  * @returns This builder, so calls chain.
592
1229
  */
593
- executeApiWorkflow(id, opts) {
594
- return this.orchestratorNode(id, 'Orchestrator.ExecuteApiWorkflowAsync', opts);
1230
+ executeApiWorkflow(id, opts, body) {
1231
+ return this.orchestratorNode(id, 'Orchestrator.ExecuteApiWorkflowAsync', opts, body);
595
1232
  }
596
1233
  /**
597
1234
  * Execute a **business rule** and wait for it (`Orchestrator.BusinessRules`, on a
@@ -636,9 +1273,10 @@ class ScopeBuilder {
636
1273
  * @param opts - Which rule package, folder, and input arguments. `outputVar` defaults
637
1274
  * to `<id>_businessRuleResponse` — see the note in the implementation for why this one
638
1275
  * type does not take its default from the spec.
1276
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
639
1277
  * @returns This builder, so calls chain.
640
1278
  */
641
- businessRule(id, opts) {
1279
+ businessRule(id, opts, body) {
642
1280
  // The ONE place this type departs from its family, and deliberately.
643
1281
  //
644
1282
  // Every other member takes its default output variable from the registry's
@@ -653,10 +1291,7 @@ class ScopeBuilder {
653
1291
  //
654
1292
  // Pinning it here makes the default independent of the label. The output ROW still
655
1293
  // carries the registry's own `name`, so the artifact is unchanged.
656
- return this.orchestratorNode(id, 'Orchestrator.BusinessRules', {
657
- ...opts,
658
- outputVar: opts.outputVar ?? `${id}_businessRuleResponse`,
659
- });
1294
+ return this.orchestratorNode(id, 'Orchestrator.BusinessRules', { ...opts, outputVar: opts.outputVar ?? `${id}_businessRuleResponse` }, body);
660
1295
  }
661
1296
  /**
662
1297
  * Add an item to an Orchestrator **queue** (`Orchestrator.CreateQueueItem`, or
@@ -675,10 +1310,11 @@ class ScopeBuilder {
675
1310
  *
676
1311
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
677
1312
  * @param opts - Which queue and folder, the item content, and whether to wait for it.
1313
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
678
1314
  * @returns This builder, so calls chain.
679
1315
  */
680
- queueItem(id, opts) {
681
- return this.push({
1316
+ queueItem(id, opts, body) {
1317
+ return this.pushActivity({
682
1318
  kind: 'typed',
683
1319
  id,
684
1320
  name: opts.name,
@@ -695,7 +1331,7 @@ class ScopeBuilder {
695
1331
  retry: opts.retry,
696
1332
  errorMapping: opts.errorMapping,
697
1333
  loop: opts.loop,
698
- });
1334
+ }, body);
699
1335
  }
700
1336
  /**
701
1337
  * A **human task** — an Action App task a person completes (`bpmn:userTask` +
@@ -745,10 +1381,11 @@ class ScopeBuilder {
745
1381
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
746
1382
  * @param opts - The app, the outcomes offered, what the human sees, and which
747
1383
  * response fields to keep.
1384
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
748
1385
  * @returns This builder, so calls chain.
749
1386
  */
750
- humanTask(id, opts) {
751
- return this.push({
1387
+ humanTask(id, opts, body) {
1388
+ return this.pushActivity({
752
1389
  kind: 'typed',
753
1390
  id,
754
1391
  name: opts.name,
@@ -768,7 +1405,7 @@ class ScopeBuilder {
768
1405
  retry: opts.retry,
769
1406
  errorMapping: opts.errorMapping,
770
1407
  loop: opts.loop,
771
- });
1408
+ }, body);
772
1409
  }
773
1410
  /**
774
1411
  * Wait for an internal message from another Maestro process
@@ -807,10 +1444,11 @@ class ScopeBuilder {
807
1444
  *
808
1445
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
809
1446
  * @param opts - The message name, the correlation reference, and what to keep.
1447
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
810
1448
  * @returns This builder, so calls chain.
811
1449
  */
812
- receiveMessage(id, opts) {
813
- return this.push({
1450
+ receiveMessage(id, opts, body) {
1451
+ return this.pushActivity({
814
1452
  kind: 'typed',
815
1453
  id,
816
1454
  name: opts.name,
@@ -823,7 +1461,7 @@ class ScopeBuilder {
823
1461
  retry: opts.retry,
824
1462
  errorMapping: opts.errorMapping,
825
1463
  loop: opts.loop,
826
- });
1464
+ }, body);
827
1465
  }
828
1466
  /**
829
1467
  * Wait mid-flow for an Integration Service connector event
@@ -863,10 +1501,11 @@ class ScopeBuilder {
863
1501
  * @param connector - The connector key, e.g. `'uipath-microsoft-outlook365'`.
864
1502
  * @param event - The event name, e.g. `'email-received'`.
865
1503
  * @param opts - Connection/folder bindings, the `where` scope, and what to keep.
1504
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
866
1505
  * @returns This builder, so calls chain.
867
1506
  */
868
- waitForEvent(id, connector, event, opts) {
869
- return this.push({
1507
+ waitForEvent(id, connector, event, opts, body) {
1508
+ return this.pushActivity({
870
1509
  kind: 'connectorEvent',
871
1510
  id,
872
1511
  name: opts.name,
@@ -886,7 +1525,7 @@ class ScopeBuilder {
886
1525
  retry: opts.retry,
887
1526
  errorMapping: opts.errorMapping,
888
1527
  loop: opts.loop,
889
- });
1528
+ }, body);
890
1529
  }
891
1530
  /**
892
1531
  * START the process when an Integration Service connector event fires
@@ -966,10 +1605,11 @@ class ScopeBuilder {
966
1605
  * @param action - The `AgentExecution` operation name.
967
1606
  * @param inputs - The invocation payload.
968
1607
  * @param opts - `mode`, the connection/folder bindings, and what to keep.
1608
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
969
1609
  * @returns This builder, so calls chain.
970
1610
  */
971
- externalAgent(id, connector, action, inputs, opts) {
972
- return this.externalTask('agent', id, connector, action, inputs, opts);
1611
+ externalAgent(id, connector, action, inputs, opts, body) {
1612
+ return this.externalTask('agent', id, connector, action, inputs, opts, body);
973
1613
  }
974
1614
  /**
975
1615
  * Invoke an external workflow through an Integration Service connector
@@ -984,14 +1624,15 @@ class ScopeBuilder {
984
1624
  * @param action - The `ProcessExecution` operation name.
985
1625
  * @param inputs - The invocation payload.
986
1626
  * @param opts - `mode`, the connection/folder bindings, and what to keep.
1627
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
987
1628
  * @returns This builder, so calls chain.
988
1629
  */
989
- externalWorkflow(id, connector, action, inputs, opts) {
990
- return this.externalTask('workflow', id, connector, action, inputs, opts);
1630
+ externalWorkflow(id, connector, action, inputs, opts, body) {
1631
+ return this.externalTask('workflow', id, connector, action, inputs, opts, body);
991
1632
  }
992
1633
  /** @internal Shared body of the two external-invocation methods. */
993
- externalTask(family, id, connector, action, inputs, opts) {
994
- return this.push({
1634
+ externalTask(family, id, connector, action, inputs, opts, body) {
1635
+ return this.pushActivity({
995
1636
  kind: 'externalTask',
996
1637
  id,
997
1638
  name: opts.name,
@@ -1010,7 +1651,7 @@ class ScopeBuilder {
1010
1651
  retry: opts.retry,
1011
1652
  errorMapping: opts.errorMapping,
1012
1653
  loop: opts.loop,
1013
- });
1654
+ }, body);
1014
1655
  }
1015
1656
  /**
1016
1657
  * ANY registry-backed node, by extension type — the generic form the typed
@@ -1042,17 +1683,20 @@ class ScopeBuilder {
1042
1683
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
1043
1684
  * @param type - The registry extension type, e.g. `'A2A.AgentExecution'`.
1044
1685
  * @param opts - Context values, payload, output mapping and the shared activity options.
1686
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
1045
1687
  * @returns This builder, so calls chain.
1046
1688
  */
1047
- activity(id, type, opts = {}) {
1048
- return this.push({
1689
+ activity(id, type, opts = {}, body) {
1690
+ return this.pushActivity({
1049
1691
  kind: 'typed',
1050
1692
  id,
1051
1693
  name: opts.name,
1052
1694
  type,
1053
- context: opts.context ?? {},
1054
- contextRows: opts.contextRows,
1695
+ context: defaultActivityIdentity(type, opts.context ?? {}, opts.contextRows),
1696
+ contextRows: defaultActivityIdentityRows(type, opts.context ?? {}, opts.contextRows),
1055
1697
  inputs: opts.inputs ?? {},
1698
+ inputRows: opts.inputRows,
1699
+ spec: opts.spec,
1056
1700
  typeVersion: opts.typeVersion,
1057
1701
  inputSchema: opts.inputSchema,
1058
1702
  outputVar: opts.outputVar,
@@ -1064,7 +1708,7 @@ class ScopeBuilder {
1064
1708
  retry: opts.retry,
1065
1709
  errorMapping: opts.errorMapping,
1066
1710
  loop: opts.loop,
1067
- });
1711
+ }, body);
1068
1712
  }
1069
1713
  // ── Sub-process (nested graph) ──
1070
1714
  /**
@@ -1073,14 +1717,312 @@ class ScopeBuilder {
1073
1717
  * @param id - The element id, unique in the process and used by `sequenceFlow`.
1074
1718
  * @param fn - Receives a sub-builder for the elements inside it.
1075
1719
  * @param opts - `name`, and the loop characteristics when it repeats.
1720
+ * @param body - Boundary events attached to this activity, declared on the {@link ActivityBuilder} it receives.
1076
1721
  * @returns This builder, so calls chain.
1077
1722
  */
1078
- subProcess(id, fn, opts = {}) {
1079
- const sb = new SubProcessBuilder(this._defs, this._bindingRegistry);
1723
+ subProcess(id, fn, opts = {}, body) {
1724
+ // Inherits the enclosing REAL scope's mode — an arm passes its parent's on, not its
1725
+ // own always-sequence — and the body may still call `.flowMode()` first to differ.
1726
+ const sb = new SubProcessBuilder(this._defs, this._bindingRegistry, undefined, this._scopeMode, this._sugar);
1080
1727
  fn(sb);
1081
- return this.push(sb._buildNode(id, opts));
1728
+ sb.closeScope(`sub-process "${id}"`);
1729
+ return this.pushActivity(sb._buildNode(id, opts), body);
1730
+ }
1731
+ /**
1732
+ * An EVENT sub-process — a handler that lives inside this scope and starts when
1733
+ * an error, a message, or a timer occurs anywhere in it (`bpmn:subProcess
1734
+ * triggeredByEvent="true"`, with its single start event synthesized).
1735
+ *
1736
+ * @remarks
1737
+ * The construct BPMN has for a safety net: it has no incoming flow, and it
1738
+ * catches what nothing closer to the failing element caught. Distinct from a
1739
+ * boundary handler (`.onError()` on one activity), which guards one element; this
1740
+ * guards the whole container it sits in, so where it sits is what it guards — put
1741
+ * it inside a multi-instance sub-process to fail one iteration rather than the
1742
+ * whole run.
1743
+ *
1744
+ * Its body is written in order: elements chain from the synthesized start, and
1745
+ * branch with `.choose()` / `.fork()` / `.race()`. Every path must end — with an
1746
+ * end event or an explicit `.sequenceFlow()` off its last element — because an
1747
+ * event sub-process has nowhere else to go.
1748
+ *
1749
+ * @example
1750
+ * **A central failure net that classifies on the error it caught**
1751
+ * ```ts
1752
+ * bpmn('payroll')
1753
+ * .var('outcome', 'string', { default: 'RUNNING' })
1754
+ * .startEvent('start')
1755
+ * .scriptTask('calculate', { script: 'return { ok: true };' })
1756
+ * .endEvent('done')
1757
+ * .sequenceFlow('start', 'calculate')
1758
+ * .sequenceFlow('calculate', 'done')
1759
+ * .eventSubProcess('failures', { error: true, name: 'Failure net' }, (h) =>
1760
+ * h.choose('kind', [
1761
+ * {
1762
+ * // The start captures the error into `vars.Error` by default (`errorVar`).
1763
+ * when: '=js:String(vars.Error.code).indexOf("DATA") >= 0',
1764
+ * label: 'Bad data',
1765
+ * body: (b) => b.task('flagData', { set: { outcome: 'FAILED_BAD_DATA' } }).endEvent('badData', { name: 'Bad data handled' }),
1766
+ * },
1767
+ * {
1768
+ * otherwise: true,
1769
+ * label: 'Outage',
1770
+ * body: (b) => b.task('flagOutage', { set: { outcome: 'FAILED_OUTAGE' } }).endEvent('outage', { name: 'Outage escalated' }),
1771
+ * },
1772
+ * ]),
1773
+ * )
1774
+ * .build();
1775
+ * ```
1776
+ *
1777
+ * @param id - The sub-process's element id. The start event is `<id>_start` unless `startId` says otherwise.
1778
+ * @param opts - What starts it — exactly one of `error` / `message` / `timer` — and the rest of {@link EventSubProcessOpts}.
1779
+ * @param fn - Receives the body builder; elements chain in the order they are written.
1780
+ * @returns This builder, so calls chain.
1781
+ * @throws If none or several of `error` / `message` / `timer` are given, if an error start
1782
+ * is asked to be non-interrupting, or if a path in the body is left open.
1783
+ */
1784
+ eventSubProcess(id, opts, fn) {
1785
+ const given = [opts.error !== undefined, opts.message !== undefined, opts.timer !== undefined].filter(Boolean).length;
1786
+ if (given !== 1) {
1787
+ throw new Error(`eventSubProcess "${id}" needs exactly one of { error, message, timer } — that is what starts it.`);
1788
+ }
1789
+ if (opts.error !== undefined && opts.interrupting === false) {
1790
+ throw new Error(`eventSubProcess "${id}": an error start is always interrupting — the throw has already ended the scope it came from.`);
1791
+ }
1792
+ if (opts.errorVar !== undefined && opts.errorVar !== false && opts.error === undefined) {
1793
+ throw new Error(`eventSubProcess "${id}": \`errorVar\` captures an error, so it needs \`error\`.`);
1794
+ }
1795
+ if (typeof opts.errorVar === 'string' && opts.startPayload !== undefined) {
1796
+ throw new Error(`eventSubProcess "${id}": pass \`errorVar\` or \`startPayload\`, not both — both write the start event's output row.`);
1797
+ }
1798
+ // The capture row is on by default for an error start: without a declared `Error`
1799
+ // the platform validator rejects every `vars.Error` read in the net. An explicit
1800
+ // `startPayload` owns the row instead, and `errorVar: false` writes none.
1801
+ const capture = opts.errorVar === false || opts.startPayload !== undefined
1802
+ ? undefined
1803
+ : (opts.errorVar ?? (opts.error !== undefined ? `${id}_Error` : undefined));
1804
+ // The same collision one scope out: two error nets in one container, the platform's
1805
+ // MULTIPLE_CATCH_ALL_ERROR_EVENT_SUBPROCESS / DUPLICATE_ERROR_EVENT_SUBPROCESS. The
1806
+ // error name is interned once, so two nets naming one error share its ref and compare
1807
+ // equal here.
1808
+ if (opts.error !== undefined) {
1809
+ const mine = toErrorDef(this._defs, opts.error, `eventSubProcess "${id}"`);
1810
+ const mineRef = mine.type === 'error' ? mine.errorRef : undefined;
1811
+ const clash = this._nodes.find((n) => {
1812
+ if (n.kind !== 'subProcess' || n.triggeredByEvent !== true)
1813
+ return false;
1814
+ const start = loneStart(n.nodes);
1815
+ const other = start === undefined ? undefined : errorCaught(start);
1816
+ return other !== undefined && other.catchAll === (mineRef === undefined) && other.ref === mineRef;
1817
+ });
1818
+ if (clash !== undefined) {
1819
+ throw new Error(`eventSubProcess "${id}" catches the same error as "${clash.id}" in this scope ` +
1820
+ `(${mineRef === undefined ? 'both catch ANY error' : `both catch "${mineRef}"`}), so one of the ` +
1821
+ 'two can never run. Catch a specific error in each net, or keep the one catch-all.');
1822
+ }
1823
+ }
1824
+ const startId = opts.startId ?? `${id}_start`;
1825
+ let event;
1826
+ if (opts.error !== undefined)
1827
+ event = toErrorDef(this._defs, opts.error, `eventSubProcess "${id}"`);
1828
+ else if (opts.timer !== undefined)
1829
+ event = { type: 'timer', timer: normTimer(opts.timer) };
1830
+ else
1831
+ event = { type: 'message', messageRef: this._defs.messageRef(opts.message) };
1832
+ const sb = new SubProcessBuilder(this._defs, this._bindingRegistry, undefined, 'sequence', this._sugar);
1833
+ sb._begin({
1834
+ kind: 'startEvent',
1835
+ id: startId,
1836
+ name: opts.startName,
1837
+ event,
1838
+ payload: opts.startPayload ?? (capture !== undefined ? errorCapturePayload(capture, 'startEvent') : undefined),
1839
+ interrupting: opts.interrupting === false ? false : undefined,
1840
+ });
1841
+ if (capture !== undefined)
1842
+ this._sugar.autoVars.push({ id: capture, vars: sb._varsOf(), elementId: startId });
1843
+ fn(sb);
1844
+ sb._finishBody(`event sub-process "${id}"`);
1845
+ return this.push(sb._buildNode(id, { name: opts.name, triggeredByEvent: true, tags: opts.tags, payload: opts.payload }));
1846
+ }
1847
+ // ── Branching by nesting ──
1848
+ /**
1849
+ * An exclusive decision whose arms are written in place — the splitting gateway,
1850
+ * each arm's conditioned flow, the arm's elements and the rejoining gateway, from
1851
+ * one call.
1852
+ *
1853
+ * @remarks
1854
+ * Each arm has a `when` condition or is the single `otherwise` fallback, so an arm
1855
+ * with no condition cannot be written, and the fallback is the gateway's default
1856
+ * flow without anyone naming a flow id. Arms rejoin at the next element placed
1857
+ * after the `choose`; a join gateway (`<id>_join`) is emitted only when more than
1858
+ * one arm actually reaches it, so an arm that ends in an end event adds no join.
1859
+ *
1860
+ * Inside an arm, consecutive elements are wired in order, and an arm may nest
1861
+ * further `choose` / `fork` / `race` calls, activities with boundary handlers, or
1862
+ * `.goto()` to jump to an element written elsewhere. An arm with no body is an
1863
+ * empty branch straight to the continuation.
1864
+ *
1865
+ * @example
1866
+ * **Approve or reject, then carry on**
1867
+ * ```ts
1868
+ * bpmn('review')
1869
+ * .var('action', 'string')
1870
+ * .var('status', 'string')
1871
+ * .startEvent('start')
1872
+ * .task('decide', { set: { action: 'Approved' } })
1873
+ * .choose('approved', [
1874
+ * { when: '=vars.action == "Approved"', label: 'Yes', body: (b) => b.task('post', { set: { status: 'posted' } }) },
1875
+ * { otherwise: true, label: 'No', body: (b) => b.task('reject', { set: { status: 'rejected' } }) },
1876
+ * ])
1877
+ * .endEvent('done')
1878
+ * .sequenceFlow('start', 'decide')
1879
+ * .sequenceFlow('decide', 'approved')
1880
+ * .build();
1881
+ * ```
1882
+ *
1883
+ * @param id - The splitting gateway's element id; the join, when emitted, is `<id>_join`.
1884
+ * @param arms - The arms — see {@link ChooseArm}.
1885
+ * @param opts - `name`, `inclusive` for an inclusive gateway, and `join` to name the join gateway.
1886
+ * @returns This builder, so calls chain.
1887
+ * @throws If there are no arms, more than one `otherwise`, or more than one empty arm.
1888
+ */
1889
+ choose(id, arms, opts = {}) {
1890
+ if (!arms.length)
1891
+ throw new Error(`choose "${id}" has no arms.`);
1892
+ if (arms.filter((a) => a.otherwise).length > 1) {
1893
+ throw new Error(`choose "${id}" has more than one \`otherwise\` arm; at most one.`);
1894
+ }
1895
+ for (const a of arms) {
1896
+ if (a.when === undefined && !a.otherwise)
1897
+ throw new Error(`choose "${id}": every arm needs \`when\` or \`otherwise: true\`.`);
1898
+ }
1899
+ const kind = opts.inclusive ? 'inclusiveGateway' : 'exclusiveGateway';
1900
+ const split = { kind, id, name: opts.name };
1901
+ return this.branch(split, kind, arms.map((a, i) => ({
1902
+ entry: {
1903
+ source: id,
1904
+ condition: a.when,
1905
+ name: a.label,
1906
+ defaultOf: a.otherwise ? split : undefined,
1907
+ origin: `arm ${i + 1}${a.label ? ` ("${a.label}")` : ''} of choose "${id}"`,
1908
+ },
1909
+ body: a.body,
1910
+ })), `choose "${id}"`, false, opts.join);
1911
+ }
1912
+ /**
1913
+ * A parallel split whose arms are written in place: every arm runs, and they
1914
+ * rejoin through a parallel join (`<id>_join`) before the next element.
1915
+ *
1916
+ * @example
1917
+ * **Two lookups at once, then one summary**
1918
+ * ```ts
1919
+ * bpmn('enrich')
1920
+ * .startEvent('start')
1921
+ * .fork('gather', [
1922
+ * (b) => b.http('poLookup', { method: 'GET', url: 'https://erp.example.com/po/1' }),
1923
+ * (b) => b.http('vendorLookup', { method: 'GET', url: 'https://erp.example.com/vendor/1' }),
1924
+ * ])
1925
+ * .scriptTask('summarise', { script: 'return { ok: true };' })
1926
+ * .endEvent('done')
1927
+ * .sequenceFlow('start', 'gather')
1928
+ * .sequenceFlow('summarise', 'done')
1929
+ * .build();
1930
+ * ```
1931
+ *
1932
+ * @param id - The splitting gateway's element id; the join is `<id>_join`.
1933
+ * @param arms - One body per parallel arm.
1934
+ * @param opts - `name` for the splitting gateway, and `join` to name the join gateway.
1935
+ * @returns This builder, so calls chain.
1936
+ */
1937
+ fork(id, arms, opts = {}) {
1938
+ if (!arms.length)
1939
+ throw new Error(`fork "${id}" has no arms.`);
1940
+ return this.branch({ kind: 'parallelGateway', id, name: opts.name }, 'parallelGateway', arms.map((body, i) => ({ entry: { source: id, origin: `arm ${i + 1} of fork "${id}"` }, body })), `fork "${id}"`, false, opts.join);
1941
+ }
1942
+ /**
1943
+ * An event-based gateway whose arms are written in place: each arm begins with
1944
+ * something to wait for, the first to happen wins, and the rest are cancelled.
1945
+ *
1946
+ * @remarks
1947
+ * An arm must begin with an `intermediateCatchEvent` (message or timer), a
1948
+ * `receiveMessage`, or a `waitForEvent` — that is what the gateway waits on, and
1949
+ * the builder refuses anything else in first position. Arms rejoin through an
1950
+ * exclusive join (`<id>_join`) when more than one falls through.
1951
+ *
1952
+ * @example
1953
+ * **A reply, or a three-day timeout**
1954
+ * ```ts
1955
+ * bpmn('vendor')
1956
+ * .startEvent('start')
1957
+ * .race('waitForEither', [
1958
+ * (b) => b.intermediateCatchEvent('reply', { message: 'VendorReplied' }),
1959
+ * (b) => b.intermediateCatchEvent('timeout', { timer: 'P3D' }).endEvent('gaveUp', { name: 'No reply' }),
1960
+ * ])
1961
+ * .endEvent('done')
1962
+ * .sequenceFlow('start', 'waitForEither')
1963
+ * .build();
1964
+ * ```
1965
+ *
1966
+ * @param id - The gateway's element id; the join, when emitted, is `<id>_join`.
1967
+ * @param arms - One body per alternative, each beginning with the event it waits for.
1968
+ * @param opts - `name` for the gateway, and `join` to name the join gateway.
1969
+ * @returns This builder, so calls chain.
1970
+ * @throws If an arm does not begin with something that waits.
1971
+ */
1972
+ race(id, arms, opts = {}) {
1973
+ if (!arms.length)
1974
+ throw new Error(`race "${id}" has no arms.`);
1975
+ return this.branch({ kind: 'eventBasedGateway', id, name: opts.name }, 'exclusiveGateway', arms.map((body, i) => ({ entry: { source: id, origin: `arm ${i + 1} of race "${id}"` }, body })), `race "${id}"`, true, opts.join);
1082
1976
  }
1083
1977
  // ── Flows ──
1978
+ /**
1979
+ * Give the edges the nesting constructs IMPLY the ids you choose.
1980
+ *
1981
+ * @remarks
1982
+ * A construct's edges have no call of their own — you write `.choose()` or place two
1983
+ * statements in a sequence scope, and the edge between them follows — so there is
1984
+ * nowhere to name one, and its id is derived as `Flow_<source>_<target>`. That is
1985
+ * fine for a new process and wrong for an edited one: a `.bpmn` that came from
1986
+ * somewhere carries ids like `edge_zOoHdO`, and `uip maestro bpmn merge` keys every
1987
+ * element by id, flows included. Re-derive them and the merge treats every flow as
1988
+ * new, deletes the originals, and takes their diagram edges with them — and any
1989
+ * gateway `default` preserved from the original is left naming an id that no longer
1990
+ * exists, which `uip maestro bpmn validate` then rejects.
1991
+ *
1992
+ * So this is how a decompiled process keeps the ids it arrived with while still
1993
+ * being written in the constructs. `bpmn decompile --style nested` emits one call
1994
+ * listing every implied edge whose original id is not the derived one.
1995
+ *
1996
+ * Only implied edges: an explicit `.sequenceFlow()` already takes an `id`, and a
1997
+ * pair listed here that no implied edge ever connects is simply unused.
1998
+ *
1999
+ * @example
2000
+ * **Keep an import's edge ids through a lift**
2001
+ * ```ts
2002
+ * bpmn('orders')
2003
+ * .var('total', 'number', { default: 0 })
2004
+ * .flowIds([
2005
+ * { source: 'Gw_1', target: 'Review', id: 'edge_zOoHdO' },
2006
+ * { source: 'Gw_1', target: 'Pay', id: 'edge_Kp2mQ1' },
2007
+ * ])
2008
+ * .startEvent('Start_1')
2009
+ * .choose('Gw_1', [
2010
+ * { when: '=vars.total > 1000', body: (b) => b.task('Review').goto('Done') },
2011
+ * { otherwise: true, body: (b) => b.task('Pay').goto('Done') },
2012
+ * ])
2013
+ * .endEvent('Done')
2014
+ * .sequenceFlow('Start_1', 'Gw_1')
2015
+ * .build();
2016
+ * ```
2017
+ *
2018
+ * @param entries - The edges to pin, by their endpoints.
2019
+ * @returns This builder, so calls chain.
2020
+ */
2021
+ flowIds(entries) {
2022
+ for (const e of entries)
2023
+ this._sugar.pinnedFlowIds.set(`${e.source}\u0000${e.target}`, e.id);
2024
+ return this;
2025
+ }
1084
2026
  /**
1085
2027
  * A sequence flow from `source` to `target` (1-1 with `bpmn:sequenceFlow`).
1086
2028
  *
@@ -1090,13 +2032,35 @@ class ScopeBuilder {
1090
2032
  * @returns This builder, so calls chain.
1091
2033
  */
1092
2034
  sequenceFlow(source, target, opts = {}) {
1093
- this._flows.push({
1094
- id: opts.id ?? `Flow_${source}_${target}`,
1095
- source,
1096
- target,
1097
- name: opts.name,
1098
- condition: opts.condition,
1099
- });
2035
+ // An explicit flow out of an element whose exit is still open is how that path
2036
+ // LEAVES — the same thing `.goto()` says, for a source named rather than implied.
2037
+ // Without this the exit stayed open and `continueTo` silently dropped it (its
2038
+ // source now has an explicit outgoing flow), so the next element was placed with
2039
+ // no incoming edge at all and nothing reported it. Dropping the exit here means
2040
+ // the next element meets the existing "unreachable" refusal instead, which says
2041
+ // what is wrong at the call site that caused it.
2042
+ //
2043
+ // Only a PLAIN exit: one carrying a condition or a gateway default is an edge in
2044
+ // its own right and is always written.
2045
+ const openIndex = this._open.findIndex((e) => e.source === source && e.condition === undefined && e.defaultOf === undefined);
2046
+ if (this._mode === 'sequence' && openIndex !== -1)
2047
+ this._open.splice(openIndex, 1);
2048
+ const id = opts.id ?? `Flow_${source}_${target}`;
2049
+ // An explicit flow that restates one the builder implied — a handler's tail
2050
+ // wired to the statement it was going to reach anyway — is the same edge, and
2051
+ // becomes the author's: their label and condition win, and it no longer counts
2052
+ // as implied.
2053
+ const implied = this._flows.find((f) => f.id === id && f.source === source && f.target === target && this._sugar.impliedFlowIds.has(f.id));
2054
+ if (implied) {
2055
+ if (opts.name !== undefined)
2056
+ implied.name = opts.name;
2057
+ if (opts.condition !== undefined)
2058
+ implied.condition = opts.condition;
2059
+ delete implied.implied;
2060
+ this._sugar.impliedFlowIds.delete(id);
2061
+ return this;
2062
+ }
2063
+ this._flows.push({ id, source, target, name: opts.name, condition: opts.condition });
1100
2064
  return this;
1101
2065
  }
1102
2066
  // ── Variables ──
@@ -1185,9 +2149,52 @@ export function typedNodeDeclaredVars(n) {
1185
2149
  // Spelled-out rows REPLACE the derived pair, so they are the whole answer.
1186
2150
  if (n.outputRows)
1187
2151
  return n.outputRows.map((r) => r.var).filter((v) => v !== undefined);
1188
- const v = typedNodeOutputVar(n.id, n.type, n.outputVar);
2152
+ const v = typedNodeOutputVar(n.id, n.type, n.outputVar, n.spec);
1189
2153
  return [...(v ? [v] : []), ...Object.keys(n.outputs ?? {})];
1190
2154
  }
2155
+ /**
2156
+ * Fill in `activity` on a connector task from the `operation` beside it.
2157
+ *
2158
+ * `Intsvc.ActivityExecution` must carry `activity` — the platform's required-field
2159
+ * rule reports `EMPTY_REQUIRED_FIELD` without it and the artifact is refused. The two
2160
+ * fields name the same thing: the SDK's own connector emitter writes
2161
+ * `op.name ?? op.objectName ?? n.action` into BOTH, so they are never independently
2162
+ * chosen, and `connector.ts` calls `activity` "the primary-key required field (the
2163
+ * selected op identity)".
2164
+ *
2165
+ * So a hand-rolled `.activity()` that sets `operation` and not `activity` has already
2166
+ * said which operation it means, and making the author say it twice buys nothing. This
2167
+ * is the same shape as the `operation` field's own history, recorded in `connector.ts`:
2168
+ * every connector task the SDK emitted was rejected until the emitter supplied it.
2169
+ *
2170
+ * Only the absent case is filled. An author (or an imported artifact) that states
2171
+ * `activity` explicitly keeps what it stated, even where the two differ.
2172
+ */
2173
+ function operationToBorrow(type, context, rows) {
2174
+ if (type !== 'Intsvc.ActivityExecution')
2175
+ return undefined;
2176
+ // Context reaches a node two ways and either spelling can carry either field, so
2177
+ // both are read before deciding anything is missing — the same rule `check` applies.
2178
+ const stated = (name) => context[name] !== undefined ? context[name] : rows?.find((r) => r.name === name)?.value;
2179
+ if (stated('activity') !== undefined)
2180
+ return undefined;
2181
+ const operation = stated('operation');
2182
+ return typeof operation === 'string' && operation !== '' ? operation : undefined;
2183
+ }
2184
+ function defaultActivityIdentity(type, context, rows) {
2185
+ const operation = operationToBorrow(type, context, rows);
2186
+ // When the node spells its context out as rows, the filled-in field belongs beside
2187
+ // them rather than in a record the emitter would write separately.
2188
+ if (operation === undefined || rows?.length)
2189
+ return context;
2190
+ return { ...context, activity: operation };
2191
+ }
2192
+ function defaultActivityIdentityRows(type, context, rows) {
2193
+ const operation = operationToBorrow(type, context, rows);
2194
+ if (operation === undefined || !rows?.length)
2195
+ return rows;
2196
+ return [...rows, { name: 'activity', type: 'string', value: operation }];
2197
+ }
1191
2198
  /**
1192
2199
  * Every variable name a node DECLARES by writing to it — the platform counts a
1193
2200
  * node's output `var` as a declaration, so a downstream `=vars.<name>` resolves
@@ -1214,8 +2221,49 @@ export function nodeDeclaredVars(n) {
1214
2221
  }
1215
2222
  /** A sub-process body: the same graph methods, plus an internal node builder. */
1216
2223
  export class SubProcessBuilder extends ScopeBuilder {
2224
+ /**
2225
+ * Open an event sub-process body on its synthesized start event.
2226
+ *
2227
+ * @param start - The start event node.
2228
+ * @internal
2229
+ */
2230
+ _begin(start) {
2231
+ this._nodes.push(start);
2232
+ this._open = [{ source: start.id, origin: `the start event "${start.id}"` }];
2233
+ this._started = true;
2234
+ this._synthesizedStart = start.id;
2235
+ }
2236
+ /**
2237
+ * Close a body written in order: every path in it must have ended.
2238
+ *
2239
+ * @param what - The scope, for the message.
2240
+ * @internal
2241
+ */
2242
+ _finishBody(what) {
2243
+ this.closeScope(what);
2244
+ }
1217
2245
  /** @internal */
1218
2246
  _buildNode(id, opts) {
2247
+ // An EVENT sub-process is started by its own start event, so that start must carry a
2248
+ // definition; a plain sub-process is started by an incoming flow, so its start must
2249
+ // carry none. The platform rejects each the other way round
2250
+ // (START_EVENT_WITHOUT_DEFINITION_IN_EVENT_SUBPROCESS,
2251
+ // START_EVENT_WITH_DEFINITION_IN_SUBPROCESS). `.eventSubProcess()` always synthesizes
2252
+ // a definition, so this only fires on the hand-rolled `{ triggeredByEvent: true }`
2253
+ // form and on a plain `.subProcess()` whose start was given one.
2254
+ for (const n of this._nodes) {
2255
+ if (n.kind !== 'startEvent')
2256
+ continue;
2257
+ const hasDefinition = n.event !== undefined || n.trigger !== undefined;
2258
+ if (opts.triggeredByEvent === true && !hasDefinition) {
2259
+ throw new Error(`sub-process "${id}" is \`triggeredByEvent\`, so its start event "${n.id}" needs the event that starts it — ` +
2260
+ 'an `error`, `message` or `timer`. `.eventSubProcess(id, { error: true }, fn)` writes that start for you.');
2261
+ }
2262
+ if (opts.triggeredByEvent !== true && hasDefinition) {
2263
+ throw new Error(`sub-process "${id}" is entered by an incoming flow, so its start event "${n.id}" must not carry an event ` +
2264
+ 'definition. Drop it, or make this an event sub-process with `.eventSubProcess()`.');
2265
+ }
2266
+ }
1219
2267
  return {
1220
2268
  kind: 'subProcess',
1221
2269
  id,
@@ -1232,6 +2280,149 @@ export class SubProcessBuilder extends ScopeBuilder {
1232
2280
  };
1233
2281
  }
1234
2282
  }
2283
+ /**
2284
+ * One arm of a `choose` / `fork` / `race`, or one boundary handler's path: the same
2285
+ * graph methods as any scope, written INTO the enclosing scope, with consecutive
2286
+ * elements wired in order.
2287
+ *
2288
+ * @remarks
2289
+ * An arm creates no BPMN scope of its own — its elements and flows belong to the
2290
+ * scope the construct was written in, and variables declared here are that scope's
2291
+ * — so an explicit `.sequenceFlow()` may reach any element of that scope, and
2292
+ * `.goto()` jumps to one without leaving the nesting. Its path ends with an end
2293
+ * event or `.goto()`, or falls through to whatever follows the construct.
2294
+ *
2295
+ * Inside an arm, branch with `.choose()` / `.fork()` / `.race()`; a bare gateway is
2296
+ * refused, because its outgoing flows cannot be implied.
2297
+ */
2298
+ export class PathBuilder extends ScopeBuilder {
2299
+ /** @internal The first element placed — what a `race` arm waits on. */
2300
+ _first;
2301
+ /**
2302
+ * @param defs - The process's message/error declarations.
2303
+ * @param bindings - The process's bindings.
2304
+ * @param storage - The enclosing scope's arrays, which this arm writes into.
2305
+ * @param sugar - Process-wide bookkeeping for the nesting sugar.
2306
+ * @param entry - The edge(s) this arm continues from.
2307
+ * @param scopeMode - The mode of the real scope the arm writes into, for a nested `.subProcess()` to inherit.
2308
+ * @internal
2309
+ */
2310
+ constructor(defs, bindings, storage, sugar, entry, scopeMode) {
2311
+ super(defs, bindings, storage, 'sequence', sugar);
2312
+ this._isArm = true;
2313
+ this._scopeMode = scopeMode;
2314
+ this._open = entry;
2315
+ this._started = true;
2316
+ }
2317
+ /** @internal */
2318
+ place(node, viaSugar = false) {
2319
+ super.place(node, viaSugar);
2320
+ this._first ??= node;
2321
+ }
2322
+ /**
2323
+ * Leave this arm for an element written elsewhere — a loop back to an earlier
2324
+ * step, a jump into another arm, a shared exit — writing the edge without leaving
2325
+ * the nesting. Nothing may follow it in the arm.
2326
+ *
2327
+ * @example
2328
+ * **Retry from the top when the check fails**
2329
+ * ```ts
2330
+ * bpmn('retrying')
2331
+ * .var('ok', 'boolean')
2332
+ * .startEvent('start')
2333
+ * .scriptTask('attempt', { script: 'return { ok: true };', outputs: { ok: '=result.ok' } })
2334
+ * .choose('worked', [
2335
+ * { when: '=vars.ok == true', body: (b) => b.endEvent('done') },
2336
+ * { otherwise: true, body: (b) => b.goto('attempt') },
2337
+ * ])
2338
+ * .sequenceFlow('start', 'attempt')
2339
+ * .sequenceFlow('attempt', 'worked')
2340
+ * .build();
2341
+ * ```
2342
+ *
2343
+ * @param target - The id of the element to continue at.
2344
+ * @returns This builder; nothing more may be placed on this arm.
2345
+ */
2346
+ goto(target) {
2347
+ if (this._closedBy !== undefined)
2348
+ throw new Error(`.goto("${target}") follows ${this._closedBy}; an arm leaves once.`);
2349
+ for (const exit of this._open)
2350
+ this.writeImpliedFlow(exit, target);
2351
+ this._open = [];
2352
+ this._closedBy = `.goto("${target}")`;
2353
+ return this;
2354
+ }
2355
+ /**
2356
+ * Finish the arm and hand its open ends back to the construct that owns it.
2357
+ *
2358
+ * @returns The exits still open — empty when every path ended.
2359
+ * @internal
2360
+ */
2361
+ _finish() {
2362
+ return this._open;
2363
+ }
2364
+ }
2365
+ /**
2366
+ * The boundary events of one activity, declared on the activity itself — what the
2367
+ * body callback every activity method accepts receives.
2368
+ *
2369
+ * @remarks
2370
+ * `attachedTo` is the receiver: a handler declared here is attached to the activity
2371
+ * whose body it is written in, so a boundary event on nothing, or on a gateway,
2372
+ * cannot be written. Each handler's path is written in place, in order, and
2373
+ * continues as {@link TimerHandlerOpts.interrupting} describes: an interrupting
2374
+ * handler rejoins at the statement after the activity, a non-interrupting one must
2375
+ * end on its own.
2376
+ *
2377
+ * @example
2378
+ * **A human task with a reminder and a failure exit**
2379
+ * ```ts
2380
+ * bpmn('approval')
2381
+ * .var('reminded', 'string')
2382
+ * .startEvent('start')
2383
+ * .humanTask('approve', { app: 'InvoiceApproval', actions: ['Approve', 'Reject'] }, (t) => {
2384
+ * t.onTimer('PT1H', { interrupting: false }, (b) =>
2385
+ * b.task('remind', { set: { reminded: 'true' } }).endEvent('reminded'));
2386
+ * t.onError((b) => b.endEvent('failed', { name: 'Approval failed' }));
2387
+ * })
2388
+ * .endEvent('done')
2389
+ * .sequenceFlow('start', 'approve')
2390
+ * .sequenceFlow('approve', 'done')
2391
+ * .build();
2392
+ * ```
2393
+ */
2394
+ export class ActivityBuilder {
2395
+ scope;
2396
+ activity;
2397
+ /**
2398
+ * @param scope - The scope the activity was placed in.
2399
+ * @param activity - The activity's id.
2400
+ * @internal
2401
+ */
2402
+ constructor(scope, activity) {
2403
+ this.scope = scope;
2404
+ this.activity = activity;
2405
+ }
2406
+ onError(a, b, c) {
2407
+ const error = a === undefined || typeof a === 'function' ? true : a;
2408
+ const opts = b !== undefined && typeof b === 'object' ? b : {};
2409
+ const body = typeof a === 'function' ? a : typeof b === 'function' ? b : c;
2410
+ this.scope._attach(this.activity, 'onError', { event: toErrorDef(this.scope._defsOf(), error, `.onError() on "${this.activity}"`), interrupting: true, opts }, body);
2411
+ return this;
2412
+ }
2413
+ onTimer(timer, b, c) {
2414
+ const opts = b !== undefined && typeof b === 'object' ? b : {};
2415
+ const body = typeof b === 'function' ? b : c;
2416
+ this.scope._attach(this.activity, 'onTimer', { event: { type: 'timer', timer: normTimer(timer) }, interrupting: opts.interrupting ?? true, opts }, body);
2417
+ return this;
2418
+ }
2419
+ onMessage(message, b, c) {
2420
+ const opts = b !== undefined && typeof b === 'object' ? b : {};
2421
+ const body = typeof b === 'function' ? b : c;
2422
+ this.scope._attach(this.activity, 'onMessage', { event: { type: 'message', messageRef: this.scope._defsOf().messageRef(message) }, interrupting: opts.interrupting ?? true, opts }, body);
2423
+ return this;
2424
+ }
2425
+ }
1235
2426
  // ─── Top-level process builder ────────────────────────────────────────────────
1236
2427
  /**
1237
2428
  * The top-level process builder `bpmn()` returns — every graph method of the
@@ -1377,7 +2568,10 @@ export class BpmnBuilder extends ScopeBuilder {
1377
2568
  propertyAttribute: opts.propertyAttribute ?? 'value',
1378
2569
  resourceSubType: opts.resourceSubType,
1379
2570
  default: opts.value,
1380
- resourceKey: opts.resourceKey ?? opts.value,
2571
+ // Left unset when not given, rather than collapsed onto `value`: the serializer
2572
+ // fills it — with the companion connection's key for a `folderKey` binding, and
2573
+ // with `value` for every other — see `folderCompanionKeys` in serialize.ts.
2574
+ resourceKey: opts.resourceKey,
1381
2575
  });
1382
2576
  return this;
1383
2577
  }
@@ -1387,6 +2581,57 @@ export class BpmnBuilder extends ScopeBuilder {
1387
2581
  * @returns The built process — its elements, flows and variables.
1388
2582
  */
1389
2583
  build() {
2584
+ this.closeScope('the process');
2585
+ // A variable an `errorVar` asked for, declared the way the product declares one
2586
+ // — unless the author declared that id themselves, anywhere in the document.
2587
+ const declared = new Set();
2588
+ const collect = (nodes, vars) => {
2589
+ for (const v of vars)
2590
+ declared.add(v.id);
2591
+ for (const n of nodes)
2592
+ if (n.kind === 'subProcess')
2593
+ collect(n.nodes, n.variables);
2594
+ };
2595
+ collect(this._nodes, this._vars);
2596
+ // One `errorVar` id is ONE declaration, wherever the handlers asking for it sit.
2597
+ // A variable id is unique across the whole document — two `uipath:inputOutput`
2598
+ // elements sharing one id is DUPLICATE_DOCUMENT_ID, and the XML parser drops the
2599
+ // second — so "declare it again in the other scope" is not available.
2600
+ //
2601
+ // Declared once and scoped to whichever boundary came first, a second handler's
2602
+ // `=Error` row wrote a variable belonging to an element it has nothing to do with.
2603
+ // So a reused id widens the declaration off that boundary, and when the handlers
2604
+ // asking for it are in different scopes the declaration moves to the root, which
2605
+ // is the one scope all of them can see.
2606
+ const autoDecls = new Map();
2607
+ for (const auto of this._sugar.autoVars) {
2608
+ const prior = autoDecls.get(auto.id);
2609
+ if (prior) {
2610
+ // Shared: no longer one boundary's private variable.
2611
+ prior.decl.elementId = undefined;
2612
+ if (prior.vars !== auto.vars && prior.vars !== this._vars) {
2613
+ // Asked for from two scopes: hoist the single declaration to the root, the
2614
+ // only scope in view of both.
2615
+ prior.vars.splice(prior.vars.indexOf(prior.decl), 1);
2616
+ this._vars.push(prior.decl);
2617
+ prior.vars = this._vars;
2618
+ }
2619
+ continue;
2620
+ }
2621
+ // An id the AUTHOR declared anywhere stays theirs, scoped where they put it.
2622
+ if (declared.has(auto.id))
2623
+ continue;
2624
+ const decl = {
2625
+ id: auto.id,
2626
+ name: 'Error',
2627
+ type: 'jsonSchema',
2628
+ direction: 'inputOutput',
2629
+ elementId: auto.elementId,
2630
+ schema: errorSchema,
2631
+ };
2632
+ auto.vars.push(decl);
2633
+ autoDecls.set(auto.id, { decl, vars: auto.vars });
2634
+ }
1390
2635
  const built = {
1391
2636
  id: this._id,
1392
2637
  name: this._name,