@uipath/maestro-builder-sdk 6.17.0 → 6.17.2

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.
@@ -321,18 +321,28 @@ export function checkConnectorSchema(where, spec, opts, dialect, report) {
321
321
  }
322
322
  }
323
323
  /**
324
- * The field contract and the binding names `compile` enforces, reported here first.
324
+ * The operation, its field contract and the binding names `compile` enforces,
325
+ * reported here first.
325
326
  *
326
- * Both rules run only with the facts in hand (a library for the fields, a
327
+ * Each rule runs only with the facts in hand (a library for the operation and its fields, a
327
328
  * bindings.json for the names); without them `check` stays a pure function of the
328
329
  * built model and says nothing, as before.
329
330
  */
330
331
  export function checkConnectorContract(where, spec, opts, report,
331
332
  /** The activity's own id, for {@link connectionConnectorConflicts}; omit to record nothing. */
332
333
  id) {
333
- const inputErr = opts.connectorInputs?.({ key: spec.key, action: spec.action, version: spec.version, object: spec.object }, spec.inputs ?? {});
334
- if (inputErr) {
335
- report({ level: 'error', code: 'CONNECTOR_INPUT', message: `${where}: ${inputErr}` });
334
+ const target = { key: spec.key, action: spec.action, version: spec.version, object: spec.object };
335
+ // The operation itself first: an operation compile cannot resolve has no field
336
+ // contract to check, and `connectorInputs` answers "unknown" for it.
337
+ const unresolved = opts.connectorResolveError?.(target);
338
+ if (unresolved) {
339
+ report({ level: 'error', code: unresolved.code, message: `${where}: ${unresolved.message}` });
340
+ }
341
+ else {
342
+ const inputErr = opts.connectorInputs?.(target, spec.inputs ?? {});
343
+ if (inputErr) {
344
+ report({ level: 'error', code: 'CONNECTOR_INPUT', message: `${where}: ${inputErr}` });
345
+ }
336
346
  }
337
347
  checkBindingLabels(where, spec, opts, (code, message) => report({ level: 'error', code, message }), { key: spec.key, action: spec.action }, id === undefined ? undefined : { id, connector: spec.key });
338
348
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Does a node definition have an `error` port? One answer for `check` and
3
+ * `serialize` (flow-builder-sdk#943).
4
+ *
5
+ * The product's rule, read from @uipath/flow-core and @uipath/flow-tool:
6
+ *
7
+ * - the manifest client injects an `error` source handle into every served
8
+ * definition with `supportsErrorHandling: true` that does not declare one
9
+ * (`withErrorHandles` / `nodeSupportsErrorHandling`);
10
+ * - `uip maestro flow validate` does the same when it checks edge handles
11
+ * (`validateEdgeHandlesAgainstCurrentManifests`: `supportsErrorHandling ===
12
+ * true` adds `error` to the node's source handles).
13
+ *
14
+ * So the flag alone grants the port. For instance, the manifest
15
+ * `registry get --local` returns for a sibling Flow or RPA project
16
+ * (`buildNodeManifest`) has `supportsErrorHandling: true` and only `input` +
17
+ * `output` handles, and an `error` edge from it is valid. A definition that
18
+ * declares an `error` source handle without the flag counts too.
19
+ */
20
+ export declare function definitionHasErrorPort(def: any): boolean;
21
+ /**
22
+ * The ONERROR_NO_ERROR_PORT sentence for a definition-backed node, naming its
23
+ * node type. Shared with serialize, which throws it for the same node.
24
+ */
25
+ export declare function errorPortRefusal(stepName: string, def: any): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Does a node definition have an `error` port? One answer for `check` and
3
+ * `serialize` (flow-builder-sdk#943).
4
+ *
5
+ * The product's rule, read from @uipath/flow-core and @uipath/flow-tool:
6
+ *
7
+ * - the manifest client injects an `error` source handle into every served
8
+ * definition with `supportsErrorHandling: true` that does not declare one
9
+ * (`withErrorHandles` / `nodeSupportsErrorHandling`);
10
+ * - `uip maestro flow validate` does the same when it checks edge handles
11
+ * (`validateEdgeHandlesAgainstCurrentManifests`: `supportsErrorHandling ===
12
+ * true` adds `error` to the node's source handles).
13
+ *
14
+ * So the flag alone grants the port. For instance, the manifest
15
+ * `registry get --local` returns for a sibling Flow or RPA project
16
+ * (`buildNodeManifest`) has `supportsErrorHandling: true` and only `input` +
17
+ * `output` handles, and an `error` edge from it is valid. A definition that
18
+ * declares an `error` source handle without the flag counts too.
19
+ */
20
+ export function definitionHasErrorPort(def) {
21
+ if (def?.supportsErrorHandling === true)
22
+ return true;
23
+ const groups = Array.isArray(def?.handleConfiguration) ? def.handleConfiguration : [];
24
+ return groups.some((g) => (Array.isArray(g?.handles) ? g.handles : [])
25
+ .some((h) => h?.id === 'error' && h?.type === 'source'));
26
+ }
27
+ /**
28
+ * The ONERROR_NO_ERROR_PORT sentence for a definition-backed node, naming its
29
+ * node type. Shared with serialize, which throws it for the same node.
30
+ */
31
+ export function errorPortRefusal(stepName, def) {
32
+ return `Step "${stepName}" is a ${def?.nodeType}@${def?.version} node, and its definition has no error port: `
33
+ + `it does not set supportsErrorHandling: true and declares no "error" source handle, so the edge would leave a `
34
+ + `handle the node does not have. Handle the failure downstream instead, or pass the definition exactly as `
35
+ + `\`uip maestro flow registry get\` returns it.`;
36
+ }
@@ -145,8 +145,8 @@ export declare function ran(step: string): Expr;
145
145
  * The first five are what every error-capable node's definition declares
146
146
  * (`outputDefinition.error.schema`, `required`, `additionalProperties: false`).
147
147
  * `response` and `element` are NOT declared and are present at run time anyway —
148
- * measured on every family probed (2026-09-10), `element` carrying the id of the
149
- * step that failed. They are typed here because a field that exists should be
148
+ * measured on every family probed (2026-09-10), `element` carrying the label of
149
+ * the step that failed. They are typed here because a field that exists should be
150
150
  * readable; the declared five stay first because those are the contract.
151
151
  */
152
152
  export type ErrorEnvelopeField = 'code' | 'message' | 'detail' | 'category' | 'status' | 'response' | 'element';
@@ -166,15 +166,21 @@ export type ErrorEnvelopeField = 'code' | 'message' | 'detail' | 'category' | 's
166
166
  * | `category` | string | `'User'` |
167
167
  * | `status` | number | `404` |
168
168
  * | `response` | unknown | undeclared; `null` in every probe |
169
- * | `element` | string | undeclared; the id of the step that failed |
169
+ * | `element` | string | undeclared; the failing step's label |
170
170
  *
171
171
  * The last two resolve at run time — measured, a container handler reading
172
- * `element` came back with the failing body step's id — but they are absent from
173
- * the declared schema, so `uip maestro flow validate` reports
172
+ * `element` came back with the failing body step's label — but they are absent
173
+ * from the declared schema, so `uip maestro flow validate` reports
174
174
  * `EXPRESSION_DIAGNOSTIC: Property 'element' does not exist on type
175
175
  * '<Step>_Error'` for them. Valid, and noisy: prefer the declared five unless you
176
176
  * specifically need one of these.
177
177
  *
178
+ * **`element` is the step's canvas label (`display.label`), not its node id.** The
179
+ * label defaults to the step name, so it matches the name (not always the node
180
+ * id) only while no `label` is set: `.step('failA', …, { label: 'Fail A' })`
181
+ * failing gives `element: 'Fail A'`. Route on `code` / `message`, never on
182
+ * `element`.
183
+ *
178
184
  * **Which VARIABLE holds it is per node family, and this helper owns that.**
179
185
  * Measured on the product (2026-09-10), one debug run per family:
180
186
  *
package/dist/core/expr.js CHANGED
@@ -179,15 +179,21 @@ export function ran(step) {
179
179
  * | `category` | string | `'User'` |
180
180
  * | `status` | number | `404` |
181
181
  * | `response` | unknown | undeclared; `null` in every probe |
182
- * | `element` | string | undeclared; the id of the step that failed |
182
+ * | `element` | string | undeclared; the failing step's label |
183
183
  *
184
184
  * The last two resolve at run time — measured, a container handler reading
185
- * `element` came back with the failing body step's id — but they are absent from
186
- * the declared schema, so `uip maestro flow validate` reports
185
+ * `element` came back with the failing body step's label — but they are absent
186
+ * from the declared schema, so `uip maestro flow validate` reports
187
187
  * `EXPRESSION_DIAGNOSTIC: Property 'element' does not exist on type
188
188
  * '<Step>_Error'` for them. Valid, and noisy: prefer the declared five unless you
189
189
  * specifically need one of these.
190
190
  *
191
+ * **`element` is the step's canvas label (`display.label`), not its node id.** The
192
+ * label defaults to the step name, so it matches the name (not always the node
193
+ * id) only while no `label` is set: `.step('failA', …, { label: 'Fail A' })`
194
+ * failing gives `element: 'Fail A'`. Route on `code` / `message`, never on
195
+ * `element`.
196
+ *
191
197
  * **Which VARIABLE holds it is per node family, and this helper owns that.**
192
198
  * Measured on the product (2026-09-10), one debug run per family:
193
199
  *
@@ -218,6 +218,25 @@ export interface LibraryOpts {
218
218
  */
219
219
  overlayDir?: string;
220
220
  }
221
+ /** Why {@link Library.resolve} refused a connector action. */
222
+ export type ConnectorResolveCode =
223
+ /** The library carries no such operation (or no such connector, or not at that version). */
224
+ 'CONNECTOR_NOT_IN_LIBRARY'
225
+ /** A GENERIC operation (one entry per object) named without an `object`. */
226
+ | 'CONNECTOR_OBJECT_REQUIRED'
227
+ /** An `object` on a single, non-generic operation. */
228
+ | 'CONNECTOR_OBJECT_NOT_GENERIC';
229
+ /**
230
+ * The refusal {@link Library.resolve} throws, with a `code` for each reason.
231
+ *
232
+ * Typed so a caller can tell a refusal from a bug without matching on message
233
+ * text: `check` reports it under this code with this message, and a compile
234
+ * that throws it carries the same code into its `--json` diagnostics (#948).
235
+ */
236
+ export declare class ConnectorResolveError extends Error {
237
+ readonly code: ConnectorResolveCode;
238
+ constructor(code: ConnectorResolveCode, message: string);
239
+ }
221
240
  export declare class Library {
222
241
  private layers;
223
242
  /** The LIST-ENVELOPE LEDGER, overlay first so a prepared entry can add a row. */
@@ -9,10 +9,26 @@
9
9
  import { readFileSync, existsSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
11
  import { prepareCommand } from './cli-spelling.js';
12
+ import { PULL_COMMAND } from './library-cache.js';
12
13
  import { editDistance } from './edit-distance.js';
13
14
  import { loadListEnvelopes, lookupListEnvelope } from './list-envelope.js';
14
15
  import { lookupSpecOf } from './lookups.js';
15
16
  import { loadByoaLedger, loadInvariantCollections, loadLookupResolutions } from './lookup-store.js';
17
+ /**
18
+ * The refusal {@link Library.resolve} throws, with a `code` for each reason.
19
+ *
20
+ * Typed so a caller can tell a refusal from a bug without matching on message
21
+ * text: `check` reports it under this code with this message, and a compile
22
+ * that throws it carries the same code into its `--json` diagnostics (#948).
23
+ */
24
+ export class ConnectorResolveError extends Error {
25
+ code;
26
+ constructor(code, message) {
27
+ super(message);
28
+ this.name = 'ConnectorResolveError';
29
+ this.code = code;
30
+ }
31
+ }
16
32
  export class Library {
17
33
  layers = [];
18
34
  /** The LIST-ENVELOPE LEDGER, overlay first so a prepared entry can add a row. */
@@ -582,7 +598,7 @@ export class Library {
582
598
  + `the library index (\`index.json\`), and remember the two namespaces: `
583
599
  + `\`…connector.trigger.…\` starts a flow, \`…connector.event.…\` waits mid-flow.`
584
600
  : '';
585
- throw new Error(`connector not in library: ${nodeType}${version ? '@' + version : ''}.${hint}`
601
+ throw new ConnectorResolveError('CONNECTOR_NOT_IN_LIBRARY', `connector not in library: ${nodeType}${version ? '@' + version : ''}.${hint}`
586
602
  + this.describeMiss(nodeType, version, objectName));
587
603
  }
588
604
  /**
@@ -622,7 +638,7 @@ export class Library {
622
638
  + where
623
639
  + ' Note the operation slug is the nodeType segment, NOT the tenant\'s `objectName`'
624
640
  + ' — `uip maestro case spec` reports the latter, and the two can differ.'
625
- + ' If the operation is newer than the library, refresh it with `uip maestro flow registry pull --force`.';
641
+ + ` If the operation is newer than the library, refresh it with \`${PULL_COMMAND}\`.`;
626
642
  }
627
643
  /** Every direct operation id under `prefix` (no further dot), across layers, sorted and de-duplicated. */
628
644
  opsUnder(prefix) {
@@ -646,7 +662,7 @@ export class Library {
646
662
  return ` No connector "${key}" in the library (${keys.length} indexed).`
647
663
  + (near && near !== key ? ` Closest: ${near}.` : '')
648
664
  + where
649
- + ' If the connector is newer than the library, refresh it with `uip maestro flow registry pull --force`.';
665
+ + ` If the connector is newer than the library, refresh it with \`${PULL_COMMAND}\`.`;
650
666
  }
651
667
  /**
652
668
  * The event half of {@link describeMiss}: which of the connector's EVENTS the
@@ -666,7 +682,7 @@ export class Library {
666
682
  const waits = this.opsUnder(`uipath.connector.event.${key}.`);
667
683
  // A start trigger resolves from either form; a mid-flow wait needs the `.event.` form itself.
668
684
  const usable = kind === 'trigger' ? [...new Set([...starts, ...waits])].sort() : waits;
669
- const refresh = ' If the event is newer than the library, refresh it with `uip maestro flow registry pull --force`.';
685
+ const refresh = ` If the event is newer than the library, refresh it with \`${PULL_COMMAND}\`.`;
670
686
  if (usable.length) {
671
687
  if (usable.includes(asked)) {
672
688
  // Only a version can make a carried event miss here: every other refusal
@@ -755,7 +771,7 @@ export class Library {
755
771
  + `dispatch. If it is a typo, compile will not catch it.\n`);
756
772
  return this.build(layer.dir, { ...only, objectName }, nodeType, { objectNameUnverified: true });
757
773
  }
758
- throw new Error(`connector ${nodeType} does not have an object "${objectName}": it is a single, `
774
+ throw new ConnectorResolveError('CONNECTOR_OBJECT_NOT_GENERIC', `connector ${nodeType} does not have an object "${objectName}": it is a single, `
759
775
  + `non-generic operation addressing ${only.objectName ? `"${only.objectName}"` : 'no object'}. `
760
776
  + `Drop the \`object\` option (it is only for generic operations, whose node type covers many objects).`);
761
777
  }
@@ -769,7 +785,7 @@ export class Library {
769
785
  .filter((o) => typeof o === 'string' && o.length > 0)
770
786
  .sort();
771
787
  if (objectName === undefined) {
772
- throw new Error(`connector ${nodeType} is a GENERIC operation covering ${matches.length} objects — `
788
+ throw new ConnectorResolveError('CONNECTOR_OBJECT_REQUIRED', `connector ${nodeType} is a GENERIC operation covering ${matches.length} objects — `
773
789
  + `say which one with the \`object\` option, e.g. `
774
790
  + `connector('${nodeType.split('.').slice(2, 3)[0]}', '${nodeType.split('.').slice(3).join('.')}', `
775
791
  + `{ … }, { object: '${objects[0] ?? '<object>'}' }). ${describeObjects(objects)}`);
@@ -824,7 +824,7 @@ declare class StepList {
824
824
  * even a legal call: `out(step, path?)` takes two arguments.
825
825
  *
826
826
  * **Three ways the handler can end**: `.return(...)` answers the flow,
827
- * `.terminate(...)` stops the run, and `h.rejoin('<step>')` compensates and
827
+ * `.terminate(...)` stops the run, and `h.stepToRef('<step>')` compensates and
828
828
  * hands control back to a step the success path also reaches — the shape most
829
829
  * deployed flows use. A handler that just runs out of steps still gets its own
830
830
  * End: rejoining is something the author SAYS, never something wiring infers.
@@ -838,8 +838,9 @@ declare class StepList {
838
838
  *
839
839
  * **A `.loop()` container takes one too.** `core.logic.loop` declares the
840
840
  * handle, and a body step's failure routes to it: the container's envelope
841
- * carries the body's message in `detail` and the failing body step's id in
842
- * `element`, and the instance completes rather than faulting. `.doWhile()` does
841
+ * carries the body's message in `detail` and the failing body step's label
842
+ * (`display.label`, which defaults to the step name) in `element`, and the
843
+ * instance completes rather than faulting. `.doWhile()` does
843
844
  * NOT — it declares an error variable and no error handle — so that one is
844
845
  * refused by name, and its failures belong to the body step that can fail.
845
846
  *
@@ -880,6 +881,10 @@ declare class StepList {
880
881
  * The body is its own path. It ends by answering (`.return`), stopping the run
881
882
  * (`.terminate`), or handing control back with `.stepToRef(...)`; a body that
882
883
  * simply runs out of steps gets its own End, exactly as an error handler does.
884
+ * It does not rejoin the main path. That End maps each flow output from its
885
+ * variable, as the End a flow falls off the bottom into does, so set the
886
+ * outputs on the way with `{ updates }` or end the body with `.return({…})`;
887
+ * `check` warns NO_RETURN when an output is left unset on that path.
883
888
  *
884
889
  * The main path is untouched — an edge out of a NON-default port is a side exit,
885
890
  * so whatever you chain after this call still belongs to the step's normal
package/dist/flow-sdk.js CHANGED
@@ -402,7 +402,7 @@ class StepList {
402
402
  * even a legal call: `out(step, path?)` takes two arguments.
403
403
  *
404
404
  * **Three ways the handler can end**: `.return(...)` answers the flow,
405
- * `.terminate(...)` stops the run, and `h.rejoin('<step>')` compensates and
405
+ * `.terminate(...)` stops the run, and `h.stepToRef('<step>')` compensates and
406
406
  * hands control back to a step the success path also reaches — the shape most
407
407
  * deployed flows use. A handler that just runs out of steps still gets its own
408
408
  * End: rejoining is something the author SAYS, never something wiring infers.
@@ -416,8 +416,9 @@ class StepList {
416
416
  *
417
417
  * **A `.loop()` container takes one too.** `core.logic.loop` declares the
418
418
  * handle, and a body step's failure routes to it: the container's envelope
419
- * carries the body's message in `detail` and the failing body step's id in
420
- * `element`, and the instance completes rather than faulting. `.doWhile()` does
419
+ * carries the body's message in `detail` and the failing body step's label
420
+ * (`display.label`, which defaults to the step name) in `element`, and the
421
+ * instance completes rather than faulting. `.doWhile()` does
421
422
  * NOT — it declares an error variable and no error handle — so that one is
422
423
  * refused by name, and its failures belong to the body step that can fail.
423
424
  *
@@ -464,7 +465,7 @@ class StepList {
464
465
  // `@2.4`, gated on `errorHandlingEnabled` like every other error handle), and
465
466
  // the platform routes it: a body step throwing inside a measured run left the
466
467
  // instance Completed with the container's envelope published — `detail` the
467
- // body's own message, `element` the id of the body step that failed.
468
+ // body's own message, `element` the label of the body step that failed.
468
469
  //
469
470
  // `.doWhile()` is NOT: `core.logic.dowhile@1.0` declares an error VARIABLE
470
471
  // and no error HANDLE, so an edge would leave a handle the node does not
@@ -494,6 +495,10 @@ class StepList {
494
495
  * The body is its own path. It ends by answering (`.return`), stopping the run
495
496
  * (`.terminate`), or handing control back with `.stepToRef(...)`; a body that
496
497
  * simply runs out of steps gets its own End, exactly as an error handler does.
498
+ * It does not rejoin the main path. That End maps each flow output from its
499
+ * variable, as the End a flow falls off the bottom into does, so set the
500
+ * outputs on the way with `{ updates }` or end the body with `.return({…})`;
501
+ * `check` warns NO_RETURN when an output is left unset on that path.
497
502
  *
498
503
  * The main path is untouched — an edge out of a NON-default port is a side exit,
499
504
  * so whatever you chain after this call still belongs to the step's normal
package/dist/serialize.js CHANGED
@@ -15,6 +15,7 @@ import { FlowAction, FlowTrigger } from './core/node-classes.js';
15
15
  import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
16
16
  import { dataFabricUnknownInputs } from './core/typed-raw-node.js';
17
17
  import { planContributionBindings } from './core/contribution-bindings.js';
18
+ import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
18
19
  import { isAnswerField } from './core/hitl-answers.js';
19
20
  import { connectionConnectors } from './bindings-v2.js';
20
21
  import { BindingRefusal, GUID_RE, bindingMissingMessage, bindingSelfNameMessage, bindingUndeclaredMessage, missingBindingLabels, undeclaredBinding } from './core/binding-messages.js';
@@ -4375,9 +4376,9 @@ function newScope(outputs, vars = []) {
4375
4376
  * note at `uipath.ts:118-123`, MST-9544).
4376
4377
  */
4377
4378
  /**
4378
- * Wire every parked `.rejoin('<step>')` edge, now that the whole scope exists.
4379
+ * Wire every parked `.stepToRef('<step>')` edge, now that the whole scope exists.
4379
4380
  *
4380
- * An unresolved target should be impossible — `check`'s REJOIN_UNKNOWN_TARGET
4381
+ * An unresolved target should be impossible — `check`'s REF_UNKNOWN_TARGET
4381
4382
  * runs first and names the steps that do exist — but a serializer that silently
4382
4383
  * dropped the edge would emit a flow whose handler goes nowhere, which is the
4383
4384
  * exact class of failure `.onError` used to have (an error path that vanishes in
@@ -4577,10 +4578,11 @@ function isTypeDesc(type) {
4577
4578
  *
4578
4579
  * Emitting the full set is not cosmetic. The product's output-mapping rule warns
4579
4580
  * `MISSING_OUTPUT_MAPPING` once per (End node × unmapped output variable), and
4580
- * the SDK used to leave three whole classes unmapped: the implicit End node a
4581
- * flow falls off the bottom into, every `.var(…)` anywhere, and any output a
4582
- * `.return(…)` didn't name. A four-output flow with two End nodes could collect
4583
- * eight warnings for a graph that was completely correct.
4581
+ * an End with no map answers the run with nothing. So every End site goes
4582
+ * through `emitEnd`, which calls this: the implicit End a flow falls off the
4583
+ * bottom into, a `.return(…)`'s End, the End a `.stepToList` / `.onError` body
4584
+ * gets when it runs out of steps, and the End for a `.stepSwitch` exit with no
4585
+ * arm (or a human task's `action: 'End'` outcome).
4584
4586
  */
4585
4587
  function endOutputs(scope, explicit = {}) {
4586
4588
  const outs = {};
@@ -4636,8 +4638,8 @@ function renameRefs(text, rename) {
4636
4638
  /** Every step name a flow declares in its own scope (not its children's). */
4637
4639
  function stepNamesOf(steps, out = []) {
4638
4640
  for (const s of steps) {
4639
- // `return` and `rejoin` carry a synthetic `name` and create no node named
4640
- // after it (an End id is claimed as `end`; a rejoin is one edge).
4641
+ // `return` carries a synthetic `name` but creates no node named after it (an
4642
+ // End id is claimed as `end`); `stepToRef` has no `name` at all (it is one edge).
4641
4643
  if ('name' in s && s.kind !== 'return')
4642
4644
  out.push(s.name);
4643
4645
  if (s.kind === 'branch') {
@@ -4679,6 +4681,18 @@ export function serialize(built, opts = {}) {
4679
4681
  * A bindings entry the node does not reference is not a bound resource.
4680
4682
  */
4681
4683
  const resourceBindings = new Map();
4684
+ /**
4685
+ * The resolved definition of every contribution-lowered node, by node id, so an
4686
+ * `error` edge from one can be checked against its definition (#943). Typed
4687
+ * families are not recorded: their definitions are the SDK's own.
4688
+ */
4689
+ const contributionDefs = new Map();
4690
+ /** Refuse an `error` edge from a contribution-lowered node whose definition has no error port. */
4691
+ function refuseErrorEdgeWithoutPort(nodeId, stepName) {
4692
+ const def = contributionDefs.get(nodeId);
4693
+ if (def !== undefined && !definitionHasErrorPort(def))
4694
+ throw new Error(errorPortRefusal(stepName, def));
4695
+ }
4682
4696
  /**
4683
4697
  * Hoist a contribution's own bindings, then close every `<bindings.NAME>` its
4684
4698
  * definition references and it did not supply, from the definition's own
@@ -4785,7 +4799,7 @@ export function serialize(built, opts = {}) {
4785
4799
  /**
4786
4800
  * `uid` for a node that IS an author's step, recording which id that step got.
4787
4801
  *
4788
- * `.rejoin('<step>')` names a step, and the id it was emitted under is not
4802
+ * `.stepToRef('<step>')` names a step, and the id it was emitted under is not
4789
4803
  * derivable from the name: `uid` sanitizes, de-duplicates against every id in
4790
4804
  * the file, and hands a subflow COPY a prefixed one from the rename map. So
4791
4805
  * the mapping is recorded where it is decided rather than reconstructed later
@@ -4868,6 +4882,32 @@ export function serialize(built, opts = {}) {
4868
4882
  node.parentId = o.parentId;
4869
4883
  return node;
4870
4884
  }
4885
+ /**
4886
+ * Emit an End node, map the flow's outputs on it, and wire `tails` into it.
4887
+ *
4888
+ * Every End goes through here: the `.return()` End, the End a root or
4889
+ * subflow path falls off into, a `.stepToList` / `.onError` body that runs
4890
+ * out of steps, a `.stepSwitch` exit with no arm, and a fan-out human task's
4891
+ * `action: 'End'` outcome. An End finishes the run, and the run's outputs are
4892
+ * whatever its `outputs` map says, so an End built without that map answers
4893
+ * with nothing at all, even on a path that set every output variable
4894
+ * (flow-builder-sdk#742). Building the node and its mappings in one place
4895
+ * makes that impossible to forget.
4896
+ *
4897
+ * `explicit` holds the sources a `.return({…})` named; every other output
4898
+ * maps to its own variable (`endOutputs`).
4899
+ */
4900
+ function emitEnd(scope, id, tails, o = {}) {
4901
+ const node = makeNode(NODE_TYPE.end, id, o.label ?? 'End', { parentId: o.parentId, requestedVersion: o.requestedVersion });
4902
+ node.inputs = {};
4903
+ const outs = endOutputs(scope, o.explicit);
4904
+ if (Object.keys(outs).length)
4905
+ node.outputs = outs;
4906
+ scope.nodes.push(node);
4907
+ for (const t of tails)
4908
+ addEdge(scope, t, id);
4909
+ return node;
4910
+ }
4871
4911
  /**
4872
4912
  * Emit a child flow into its own scope and register it under the calling
4873
4913
  * node's id — that key is how the format names a subflow body, and how
@@ -4908,19 +4948,10 @@ export function serialize(built, opts = {}) {
4908
4948
  start.inputs = {};
4909
4949
  scope.nodes.push(start);
4910
4950
  const tails = emit(child.steps, [{ node: startId, port: 'output' }], scope);
4911
- if (tails.length) {
4912
- const endId = uid(scope, 'end');
4913
- const end = makeNode(NODE_TYPE.end, endId, 'End');
4914
- end.inputs = {};
4915
- // Same as the root's implicit End — mapped against the SUBFLOW's own
4916
- // outputs, which is the scope the product validates a subflow body in.
4917
- const outs = endOutputs(scope);
4918
- if (Object.keys(outs).length)
4919
- end.outputs = outs;
4920
- scope.nodes.push(end);
4921
- for (const t of tails)
4922
- addEdge(scope, t, endId);
4923
- }
4951
+ // Same as the root's implicit End — mapped against the SUBFLOW's own
4952
+ // outputs, which is the scope the product validates a subflow body in.
4953
+ if (tails.length)
4954
+ emitEnd(scope, uid(scope, 'end'), tails);
4924
4955
  const globals = [];
4925
4956
  for (const inp of child.inputs)
4926
4957
  globals.push({ id: inp.name, name: inp.name, direction: 'in', type: inp.type, triggerNodeId: startId, ...varMeta(inp) });
@@ -5120,6 +5151,7 @@ export function serialize(built, opts = {}) {
5120
5151
  node.outputs = { ...(node.outputs ?? {}), ...subNodeId(clone(contribution.outputs), id) };
5121
5152
  }
5122
5153
  hoistContributionBindings(def, contribution.bindings, where);
5154
+ contributionDefs.set(id, def);
5123
5155
  scope.nodes.push(node);
5124
5156
  for (const t of tails)
5125
5157
  addEdge(scope, t, id);
@@ -6457,7 +6489,7 @@ export function serialize(built, opts = {}) {
6457
6489
  * The handler is its OWN path, and it still is: its leftover tails get their
6458
6490
  * own End rather than rejoining the chain, because `.onError` must not
6459
6491
  * SILENTLY change what runs after the step. What changed is that an author can
6460
- * now ASK — `h.rejoin('<step>')` emits an edge into a step the success path
6492
+ * now ASK — `h.stepToRef('<step>')` emits an edge into a step the success path
6461
6493
  * also reaches, and returns no tail, so nothing is capped. Never inferred:
6462
6494
  * a handler that just runs out of steps ends exactly as it always did.
6463
6495
  *
@@ -6477,11 +6509,7 @@ export function serialize(built, opts = {}) {
6477
6509
  * outcome.
6478
6510
  */
6479
6511
  function endExit(scope, nodeId, port, stepName, parentId) {
6480
- const endId = uid(scope, `${stepName}${port.charAt(0).toUpperCase()}${port.slice(1)}End`);
6481
- const end = makeNode(NODE_TYPE.end, endId, 'End', parentId ? { parentId } : {});
6482
- end.inputs = {};
6483
- scope.nodes.push(end);
6484
- addEdge(scope, { node: nodeId, port }, endId);
6512
+ emitEnd(scope, uid(scope, `${stepName}${port.charAt(0).toUpperCase()}${port.slice(1)}End`), [{ node: nodeId, port }], { parentId });
6485
6513
  }
6486
6514
  /**
6487
6515
  * Refuse a side exit on a human task's port that the step's tails already
@@ -6522,16 +6550,17 @@ export function serialize(built, opts = {}) {
6522
6550
  throw new Error(`.stepToList('${step.port}', …) after step "${source}": emitted node "${nodeId}" not found in scope`);
6523
6551
  // Only the error port carries this: without it the platform routes but Studio
6524
6552
  // Web HIDES the handle, so no human can see the handler.
6525
- if (step.port === 'error')
6553
+ if (step.port === 'error') {
6554
+ refuseErrorEdgeWithoutPort(nodeId, source);
6526
6555
  node.inputs = { ...(node.inputs ?? {}), errorHandlingEnabled: true };
6556
+ }
6527
6557
  const handlerTails = emit(handler, [{ node: nodeId, port: step.port }], scope, parentId, breakTarget);
6558
+ // A body that runs out of steps ends the run on an End of its own (no
6559
+ // merge-back is inferred). That End maps the outputs like any other, so a
6560
+ // body that set them with `{ updates }` still answers (#742).
6528
6561
  if (handlerTails.length) {
6529
6562
  const endId = uid(scope, `${source}${step.port === 'error' ? 'Error' : step.port.charAt(0).toUpperCase() + step.port.slice(1)}End`);
6530
- const end = makeNode(NODE_TYPE.end, endId, 'End', parentId ? { parentId } : {});
6531
- end.inputs = {};
6532
- scope.nodes.push(end);
6533
- for (const t of handlerTails)
6534
- addEdge(scope, t, endId);
6563
+ emitEnd(scope, endId, handlerTails, { parentId });
6535
6564
  }
6536
6565
  return tails;
6537
6566
  }
@@ -6653,8 +6682,8 @@ export function serialize(built, opts = {}) {
6653
6682
  routed.add(exit.port);
6654
6683
  // Arms CONVERGE, exactly as `.switch()`'s do: every arm that does not
6655
6684
  // end in `.return()` contributes its tail, so the next step fans in
6656
- // from all of them. (`.stepToList` instead gives each arm a private
6657
- // End, which silently drops the flow's outputs on that path —
6685
+ // from all of them. (`.stepToList` instead gives an arm that runs out
6686
+ // of steps an End of its own; `emitEnd` maps the outputs on it,
6658
6687
  // flow-builder-sdk#742.)
6659
6688
  const caseTails = emit(c.body, [{ node: nodeId, port: exit.port }], scope, parentId, breakTarget);
6660
6689
  if (caseTails.length === 0 && stopsAtEnd(c.body))
@@ -6924,6 +6953,7 @@ export function serialize(built, opts = {}) {
6924
6953
  throw new Error(`.stepToRef('${step.port}', '${step.target}') has no step to leave from.`);
6925
6954
  refuseRewire('.stepToRef', step.port, lastAction ?? '?', tails);
6926
6955
  if (step.port === 'error') {
6956
+ refuseErrorEdgeWithoutPort(from.node, lastAction ?? from.node);
6927
6957
  const node = scope.nodes.find((n) => n.id === from.node);
6928
6958
  if (node)
6929
6959
  node.inputs = { ...(node.inputs ?? {}), errorHandlingEnabled: true };
@@ -6936,8 +6966,6 @@ export function serialize(built, opts = {}) {
6936
6966
  // source must be able to ask for the id the original file had, or the
6937
6967
  // round trip cannot reproduce its own layout block (ReturnOptions.name).
6938
6968
  const id = uid(scope, step.options?.name ?? 'end');
6939
- const node = makeNode(NODE_TYPE.end, id, step.options?.label ?? 'End', { parentId, requestedVersion: step.options?.version });
6940
- node.inputs = {};
6941
6969
  const explicit = {};
6942
6970
  const updates = [];
6943
6971
  for (const [name, expr] of Object.entries(step.values)) {
@@ -6948,12 +6976,7 @@ export function serialize(built, opts = {}) {
6948
6976
  // variable; a variableUpdate for one would be a no-op assignment.
6949
6977
  updates.push({ variableId: name, expression: src });
6950
6978
  }
6951
- const outs = endOutputs(scope, explicit);
6952
- if (Object.keys(outs).length)
6953
- node.outputs = outs;
6954
- scope.nodes.push(node);
6955
- for (const t of tails)
6956
- addEdge(scope, t, id);
6979
+ emitEnd(scope, id, tails, { label: step.options?.label, parentId, requestedVersion: step.options?.version, explicit });
6957
6980
  const extra = step.options?.updates ?? {};
6958
6981
  for (const name of Object.keys(extra)) {
6959
6982
  if (name in step.values) {
@@ -6969,20 +6992,11 @@ export function serialize(built, opts = {}) {
6969
6992
  return tails;
6970
6993
  }
6971
6994
  const finalTails = emit(built.steps, [{ node: built.triggerId ?? 'start', port: 'output' }], root);
6972
- if (finalTails.length) {
6973
- const id = uid(root, 'end');
6974
- const node = makeNode(NODE_TYPE.end, id, 'End');
6975
- node.inputs = {};
6976
- // The implicit End a flow falls off the bottom into still terminates the
6977
- // flow, so it maps the flow's outputs — each to itself, since no `.return`
6978
- // named a value here.
6979
- const outs = endOutputs(root);
6980
- if (Object.keys(outs).length)
6981
- node.outputs = outs;
6982
- root.nodes.push(node);
6983
- for (const t of finalTails)
6984
- addEdge(root, t, id);
6985
- }
6995
+ // The implicit End a flow falls off the bottom into still terminates the
6996
+ // flow, so it maps the flow's outputs — each to itself, since no `.return`
6997
+ // named a value here.
6998
+ if (finalTails.length)
6999
+ emitEnd(root, uid(root, 'end'), finalTails);
6986
7000
  // ── Additional entry points (design §5.1) ────────────────────────────────
6987
7001
  // Emitted AFTER the shared body so their prefixes can join it by name. Once a
6988
7002
  // flow has more than one root, every root carries the platform's entry-point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.17.0",
3
+ "version": "6.17.2",
4
4
  "description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uipath.com/maestro",
@@ -92,5 +92,5 @@
92
92
  "@types/node": "^22.7.0",
93
93
  "esbuild": "^0.28.1"
94
94
  },
95
- "gitref": "c6e35983af98d4dc81b45aaee8a9bb0419bc1cc2"
95
+ "gitref": "629eba0e7332894902dec4cd1ef5c7072c02adf6"
96
96
  }