@uipath/maestro-builder-sdk 6.29.1 → 6.30.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.
package/dist/api-index.md CHANGED
@@ -150,7 +150,7 @@ could hand you spans for a version you do not have.
150
150
  | `HitlAssigneeType` | type | `dist/nodes/hitl/flow/spec.d.ts:16-23` | Supporting types | How the task is assigned, the platform's own vocabulary (canvas `HITL_ASSIGNEE_TYPE`): `'user'` a single user, `'group'`… |
151
151
  | `HitlChannel` | type | `dist/nodes/hitl/flow/spec.d.ts:10-15` | Supporting types | Where the task is delivered. |
152
152
  | `HitlConnection` | interface | `dist/nodes/hitl/flow/spec.d.ts:24-32` | Supporting types | A per-channel Integration Service connection (Slack / Teams delivery). |
153
- | `HitlInputs` | interface | `dist/nodes/hitl/flow/spec.d.ts:186-257` | Option shapes | 11 field(s) |
153
+ | `HitlInputs` | interface | `dist/nodes/hitl/flow/spec.d.ts:186-290` | Option shapes | 12 field(s) |
154
154
  | `HitlRecipient` | interface | `dist/nodes/hitl/flow/spec.d.ts:33-69` | Supporting types | Who gets the task and over which channels. |
155
155
  | `HitlVariant` | type | `dist/nodes/hitl/flow/spec.d.ts:114-139` | Supporting types | WHICH human-task node type to emit. |
156
156
  | `http` | function | `dist/nodes/http/flow/factories.d.ts:7-14` | Actions | Declare an HTTP request action, explicitly choosing standalone or managed. |
@@ -5,12 +5,12 @@
5
5
 
6
6
  # Maestro Builder SDK — member index
7
7
 
8
- **A lookup table of 1118 rows — match one name in it, do not read it.**
8
+ **A lookup table of 1119 rows — match one name in it, do not read it.**
9
9
  Grep or search for the member you have; the row it lands on is the whole
10
10
  answer. Reading top to bottom costs ~20x what the one row does and tells you
11
11
  nothing you asked for.
12
12
 
13
- Every field and method of every exported shape — 1118 across
13
+ Every field and method of every exported shape — 1119 across
14
14
  200 interfaces and classes — keyed by the MEMBER's own name.
15
15
 
16
16
  **Use this when the name you have is a field or a method**, which is usually
@@ -28,7 +28,7 @@ truncated summary would, for less. Read the span.
28
28
  Member names repeat across shapes (`name` is declared on 69 of them), so the
29
29
  `Declared on` column is what makes a match actionable. For the whole shape
30
30
  rather than the one field, look that name up in [`api-index.md`](api-index.md) —
31
- which is why its span is not repeated on all 1118 rows here.
31
+ which is why its span is not repeated on all 1119 rows here.
32
32
 
33
33
  **A name that misses both indexes is probably not part of the API.** The one
34
34
  that comes up is a RUNTIME output key — a human task's `Action`, a step's
@@ -264,7 +264,7 @@ not find it either.
264
264
  | `displayName` | property | `RegistryInputField` | `dist/bpmn/typed-node.d.ts:58` |
265
265
  | `displayName` | property | `SlaOpts` | `dist/case/case-sdk.d.ts:170-175` |
266
266
  | `displayName` | property | `TriggerMeta` | `dist/core/connectors.d.ts:127-128` |
267
- | `document` | property | `HitlInputs` | `dist/nodes/hitl/flow/spec.d.ts:252-256` |
267
+ | `document` | property | `HitlInputs` | `dist/nodes/hitl/flow/spec.d.ts:285-289` |
268
268
  | `documentClassify` | property | `FlowActionSpecs` | `dist/nodes/document/flow/spec.d.ts:56-59` |
269
269
  | `documentId` | property | `DocValidationInputs` | `dist/nodes/hitl/flow/spec.d.ts:84-85` |
270
270
  | `doWhile` | method | `StepList` | `dist/flow-sdk.d.ts:1089-1113` |
@@ -420,7 +420,7 @@ not find it either.
420
420
  | `height` | property | `StickyNote` | `dist/flow-sdk.d.ts:702-703` |
421
421
  | `hidden` | property | `RegistryContextField` | `dist/bpmn/typed-node.d.ts:27` |
422
422
  | `hidden` | property | `RegistryInputField` | `dist/bpmn/typed-node.d.ts:61` |
423
- | `hitl` | property | `FlowActionSpecs` | `dist/nodes/hitl/flow/spec.d.ts:260-263` |
423
+ | `hitl` | property | `FlowActionSpecs` | `dist/nodes/hitl/flow/spec.d.ts:293-296` |
424
424
  | `http` | property | `FlowActionSpecs` | `dist/nodes/http/flow/spec.d.ts:145-148` |
425
425
  | `http` | method | `ScopeBuilder` | `dist/bpmn/bpmn-sdk.d.ts:2366-2400` |
426
426
  | `humanTask` | method | `ScopeBuilder` | `dist/bpmn/bpmn-sdk.d.ts:2595-2646` |
@@ -549,6 +549,7 @@ not find it either.
549
549
  | `layout` | property | `BuiltFlow` | `dist/flow-sdk.d.ts:754-755` |
550
550
  | `layout` | method | `CaseBuilder` | `dist/case/case-sdk.d.ts:1139-1159` |
551
551
  | `layout` | method | `FlowBuilder` | `dist/flow-sdk.d.ts:1288-1298` |
552
+ | `legacyCompleted` | property | `HitlInputs` | `dist/nodes/hitl/flow/spec.d.ts:252-284` |
552
553
  | `limit` | property | `DataFabricReadInputs` | `dist/nodes/data-fabric/flow/spec.d.ts:95-100` |
553
554
  | `limit` | property | `DoWhileOptions` | `dist/flow-sdk.d.ts:503-504` |
554
555
  | `literal` | property | `Expr` | `dist/core/expr.d.ts:18` |
package/dist/check.js CHANGED
@@ -342,9 +342,12 @@ function checkFlow(built, ancestors, opts = {}) {
342
342
  level: 'error', code: 'HITL_OUTCOME_PORTS_OFF', step: prevAction?.name,
343
343
  message: `Port "${s.port}" routes a per-outcome exit, but human task "${prevAction?.name}" `
344
344
  + `has none — ${ids.length === 0 ? 'it declares no outcomes'
345
- : `the '${prevSpec.inputs?.variant}' variant reads its outcomes from its app`}, so its single `
345
+ : model.legacyCompleted ? 'it sets legacyCompleted: true'
346
+ : `the '${prevSpec.inputs?.variant}' variant reads its outcomes from its app`}, so its single `
346
347
  + `exit is 'completed'.`,
347
- suggestion: `route on out('${prevAction?.name}', 'Action') downstream`,
348
+ suggestion: model.legacyCompleted
349
+ ? 'drop `legacyCompleted` (and set outcomePorts: true to route each outcome)'
350
+ : `route on out('${prevAction?.name}', 'Action') downstream`,
348
351
  });
349
352
  }
350
353
  else if (!ids.includes(want)) {
@@ -425,7 +428,11 @@ function checkFlow(built, ancestors, opts = {}) {
425
428
  const allEnd = outcomes.length > 0
426
429
  && outcomes.every((o) => typeof o !== 'string' && o?.action === 'End');
427
430
  // `.stepSwitch` routes every outcome as an arm, so it never belongs here.
431
+ // Nor does a `legacyCompleted` task: only its primary outcome continues,
432
+ // so no downstream switch can reach the others, and HITL_LEGACY_COMPLETED
433
+ // already says so.
428
434
  if (outcomes.length > 1 && !allEnd && s0.kind !== 'stepSwitch'
435
+ && s.spec.inputs?.legacyCompleted !== true
429
436
  && !hitlRoutesPerOutcome(s.spec.inputs, s.options?.version)) {
430
437
  hitlChoiceNoPorts.set(s.name, {
431
438
  names: outcomes.map((o) => String((typeof o === 'string' ? o : o?.name) ?? '').trim()),
@@ -35,4 +35,17 @@ export declare function expandSourceHandles(def: Def, inputs: unknown): string[]
35
35
  * whose handles cannot be known: such a node has no fan-out to route.
36
36
  */
37
37
  export declare function forwardFanOut(def: Def, inputs: unknown): string[] | undefined;
38
+ /**
39
+ * The port a node's main path leaves on — the edge a plain `.step()` writes
40
+ * and a following step continues from: the first concrete forward source
41
+ * handle (`isDefaultForType` wins), else `output`. `undefined` when every
42
+ * forward source handle is templated (`case-{item.id}`), so no main path leaves
43
+ * the node.
44
+ *
45
+ * `serialize` writes the main-path edge on this port, and `decompile` reads the
46
+ * main path back off it, so the two must agree: a `core.logic.agentic-switch`
47
+ * (`case-{item.id}` + `default`) continues on `default`, and each other wired
48
+ * exit is a `.stepToList('case-<id>', …)` (#1214).
49
+ */
50
+ export declare function contributionPort(def: Def): string | undefined;
38
51
  export {};
@@ -72,3 +72,24 @@ export function forwardFanOut(def, inputs) {
72
72
  const handles = expandSourceHandles(def, inputs)?.filter((h) => !NOT_FORWARD.has(h));
73
73
  return handles !== undefined && handles.length > 1 ? handles : undefined;
74
74
  }
75
+ /**
76
+ * The port a node's main path leaves on — the edge a plain `.step()` writes
77
+ * and a following step continues from: the first concrete forward source
78
+ * handle (`isDefaultForType` wins), else `output`. `undefined` when every
79
+ * forward source handle is templated (`case-{item.id}`), so no main path leaves
80
+ * the node.
81
+ *
82
+ * `serialize` writes the main-path edge on this port, and `decompile` reads the
83
+ * main path back off it, so the two must agree: a `core.logic.agentic-switch`
84
+ * (`case-{item.id}` + `default`) continues on `default`, and each other wired
85
+ * exit is a `.stepToList('case-<id>', …)` (#1214).
86
+ */
87
+ export function contributionPort(def) {
88
+ const groups = Array.isArray(def?.handleConfiguration) ? def.handleConfiguration : [];
89
+ const handles = groups.flatMap((g) => (g?.handles ?? []));
90
+ const sources = handles.filter((h) => h?.type === 'source' && h?.id !== 'error');
91
+ const concrete = sources.filter((h) => typeof h?.id === 'string' && !h.id.includes('{'));
92
+ if (concrete.length === 0 && sources.length > 0)
93
+ return undefined;
94
+ return concrete.find((h) => h?.isDefaultForType)?.id ?? concrete[0]?.id ?? 'output';
95
+ }
@@ -68,6 +68,15 @@
68
68
  * `exposeError: true` or `outcomePorts: true` select `per-outcome` on every node
69
69
  * type, including the two app-backed variants, so that `serialize` and `check`
70
70
  * refuse them there with their own message rather than silently ignoring them.
71
+ *
72
+ * **`legacyCompleted: true`** is the one way to keep a task with outcomes on the
73
+ * `completed` handle: the `placeholder` shape, marked `legacyCompleted`. It
74
+ * exists so a file saved before flow-workbench #3512, whose task leaves on one
75
+ * `completed` edge, round-trips through decompile and compile unchanged
76
+ * (flow-builder-sdk#1197). It is the file's shape, not one to author: the
77
+ * product runs that edge as the PRIMARY outcome only, and `check` says so
78
+ * (HITL_LEGACY_COMPLETED). `serialize`'s edge guard below exempts exactly the
79
+ * nodes that asked for it.
71
80
  */
72
81
  /**
73
82
  * The fields of a human task's inputs the routing model reads — structural, so
@@ -83,6 +92,7 @@ export interface HitlRoutingInputs {
83
92
  }>;
84
93
  readonly outcomePorts?: boolean;
85
94
  readonly exposeError?: boolean;
95
+ readonly legacyCompleted?: boolean;
86
96
  }
87
97
  /**
88
98
  * An outcome's stable id — the slug that names its `outcome-<id>` port.
@@ -114,6 +124,7 @@ export interface HitlOutcomeExit {
114
124
  export type HitlFanOutReason = 'outcomePorts: false' | 'quick-form' | 'pinned' | 'one outcome' | 'every outcome ends';
115
125
  export type HitlExitModel = {
116
126
  shape: 'placeholder';
127
+ legacyCompleted?: true;
117
128
  } | {
118
129
  shape: 'every-outcome';
119
130
  exits: HitlOutcomeExit[];
@@ -151,14 +162,17 @@ type EdgeLike = {
151
162
  * the product does not draw, and an `outcome-<id>` naming no declared outcome is
152
163
  * one `flow validate` refuses ("Edge references undeclared source handle"). A
153
164
  * real outcome named "Completed" is `outcome-completed`, which is fine — the
154
- * bare `completed` never is.
165
+ * bare `completed` never is, unless the task asked for it with
166
+ * `legacyCompleted: true` (`legacyCompleted` here).
155
167
  */
156
- export declare function hitlExitViolation(node: NodeLike, port: string): string | undefined;
168
+ export declare function hitlExitViolation(node: NodeLike, port: string, legacyCompleted?: boolean): string | undefined;
157
169
  /**
158
170
  * The post-emit half of the guard: every edge of an emitted scope, whatever
159
171
  * path produced it (a tail, a `.stepToList` seed, a parked `.stepToRef`), is held
160
172
  * to {@link hitlExitViolation}. Fails closed — a serializer that emitted one of
161
173
  * these would ship a flow that validates and then drops outcomes at run time.
174
+ * `legacyCompleted` holds the ids of the tasks that set `legacyCompleted: true`,
175
+ * whose `completed` edge is the author's explicit choice.
162
176
  */
163
- export declare function assertHitlExitInvariant(nodes: readonly NodeLike[], edges: readonly EdgeLike[]): void;
177
+ export declare function assertHitlExitInvariant(nodes: readonly NodeLike[], edges: readonly EdgeLike[], legacyCompleted?: ReadonlySet<string>): void;
164
178
  export {};
@@ -57,6 +57,8 @@ export function hitlExitModel(inputs, pinnedVersion) {
57
57
  return { shape: 'per-outcome', exits };
58
58
  if (!hitlDerivesOutcomeHandles(inputs?.variant) || exits.length === 0)
59
59
  return { shape: 'placeholder' };
60
+ if (inputs?.legacyCompleted === true)
61
+ return { shape: 'placeholder', legacyCompleted: true };
60
62
  const every = (why) => ({ shape: 'every-outcome', exits, why });
61
63
  if (inputs?.outcomePorts === false)
62
64
  return every('outcomePorts: false');
@@ -95,9 +97,10 @@ function outcomeIdsOf(node) {
95
97
  * the product does not draw, and an `outcome-<id>` naming no declared outcome is
96
98
  * one `flow validate` refuses ("Edge references undeclared source handle"). A
97
99
  * real outcome named "Completed" is `outcome-completed`, which is fine — the
98
- * bare `completed` never is.
100
+ * bare `completed` never is, unless the task asked for it with
101
+ * `legacyCompleted: true` (`legacyCompleted` here).
99
102
  */
100
- export function hitlExitViolation(node, port) {
103
+ export function hitlExitViolation(node, port, legacyCompleted = false) {
101
104
  if (!OUTCOME_HANDLE_NODE_TYPES.has(String(node.type)))
102
105
  return undefined;
103
106
  const ids = outcomeIdsOf(node);
@@ -105,6 +108,8 @@ export function hitlExitViolation(node, port) {
105
108
  return undefined;
106
109
  const declared = ids.map((i) => `outcome-${i}`).join(', ');
107
110
  if (port === 'completed') {
111
+ if (legacyCompleted)
112
+ return undefined;
108
113
  return `human task "${String(node.id)}" has outcomes (${declared}), so the product draws no `
109
114
  + `'completed' handle on it — an edge there is remapped to the primary outcome and every other `
110
115
  + `outcome loses its path.`;
@@ -119,14 +124,16 @@ export function hitlExitViolation(node, port) {
119
124
  * path produced it (a tail, a `.stepToList` seed, a parked `.stepToRef`), is held
120
125
  * to {@link hitlExitViolation}. Fails closed — a serializer that emitted one of
121
126
  * these would ship a flow that validates and then drops outcomes at run time.
127
+ * `legacyCompleted` holds the ids of the tasks that set `legacyCompleted: true`,
128
+ * whose `completed` edge is the author's explicit choice.
122
129
  */
123
- export function assertHitlExitInvariant(nodes, edges) {
130
+ export function assertHitlExitInvariant(nodes, edges, legacyCompleted = new Set()) {
124
131
  const byId = new Map(nodes.map((n) => [String(n.id), n]));
125
132
  for (const e of edges) {
126
133
  const node = byId.get(String(e.sourceNodeId));
127
134
  if (!node)
128
135
  continue;
129
- const why = hitlExitViolation(node, String(e.sourcePort));
136
+ const why = hitlExitViolation(node, String(e.sourcePort), legacyCompleted.has(String(e.sourceNodeId)));
130
137
  if (why)
131
138
  throw new Error(`serialize: ${why}`);
132
139
  }
package/dist/decompile.js CHANGED
@@ -46,7 +46,7 @@ import { mapJsCode } from './core/js-code-spans.js';
46
46
  import { missingBindingLabels } from './core/binding-messages.js';
47
47
  import { prepareCommand } from './core/cli-spelling.js';
48
48
  import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
49
- import { forwardFanOut } from './core/definition-handles.js';
49
+ import { contributionPort, forwardFanOut } from './core/definition-handles.js';
50
50
  import { STICKY_NOTE_TYPE } from './core/definitionless-node-types.js';
51
51
  import { eventFilterFromTreeLeaf } from './core/event-filters.js';
52
52
  import { sdkFormFieldType } from './core/form-field-type.js';
@@ -532,11 +532,34 @@ class Graph {
532
532
  successorEdges(id) {
533
533
  const primary = this.primaryOutcomePort(id);
534
534
  const forked = this.fullyWiredExits(id);
535
+ const side = forked ? undefined : this.rawSidePorts(id);
535
536
  return this.outEdges(id).filter((e) => e.sourcePort !== 'error'
536
537
  && !ARTIFACT_PORTS.has(e.sourcePort)
537
538
  && (forked?.has(e.sourcePort)
538
539
  || (!e.sourcePort.startsWith('branch-')
539
- && (!e.sourcePort.startsWith('outcome-') || e.sourcePort === primary))));
540
+ && (!e.sourcePort.startsWith('outcome-') || e.sourcePort === primary)
541
+ && !side?.has(e.sourcePort))));
542
+ }
543
+ /**
544
+ * A multi-exit `rawNode`'s SIDE exits when not every exit is wired: each
545
+ * declared exit except the one its main path leaves on (`contributionPort`,
546
+ * the port serialize writes a plain `.step()`'s edge on — `default` for a
547
+ * `core.logic.agentic-switch`). Each is recovered as a
548
+ * `.stepToList('<port>', …)` after the `.step`, as `branch-*` and non-primary
549
+ * `outcome-*` exits are. `undefined` for every other node, and for a raw node
550
+ * whose exits are all wired (that one is a `.stepSwitch`).
551
+ *
552
+ * Without this, a partially wired agentic switch had two successors and no
553
+ * merge join, so the walk emitted an unstructured-fan-out TODO and dropped
554
+ * every arm (#1214).
555
+ */
556
+ rawSidePorts(id) {
557
+ const node = this.byId.get(id);
558
+ const exits = node ? rawNodeExits(node) : undefined;
559
+ if (!node || exits === undefined || this.fullyWiredExits(id) !== undefined)
560
+ return undefined;
561
+ const main = contributionPort(rawNodeDefinition(node));
562
+ return new Set(exits.map((e) => e.port).filter((p) => p !== main));
540
563
  }
541
564
  }
542
565
  // ─── Node → step-spec source ──────────────────────────────────────────────────
@@ -2397,6 +2420,17 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
2397
2420
  segs.push(`.stepToList(${tsLiteral(oe.sourcePort)}, ${armCb('p', outSegs)})`);
2398
2421
  }
2399
2422
  }
2423
+ // A partially wired multi-exit raw node (an agentic switch with one choice
2424
+ // unwired): every wired exit but the main path's is its own port list, and
2425
+ // an unwired one stays unwired (#1214).
2426
+ const rawSide = ctx.graph.rawSidePorts(node.id);
2427
+ for (const re of rawSide ? ctx.graph.outEdges(node.id).filter((e) => rawSide.has(e.sourcePort)) : []) {
2428
+ const mainReach = forwardReach(succ, scope, ctx.graph);
2429
+ const sideSegs = edgeRegion(re, EXIT, scope, ipdom, ctx, mainReach);
2430
+ if (sideSegs.length === 0)
2431
+ sideSegs.push('.return({})');
2432
+ segs.push(`.stepToList(${tsLiteral(re.sourcePort)}, ${armCb('p', sideSegs)})`);
2433
+ }
2400
2434
  // A body edge into the enclosing container's break handle: the path exits
2401
2435
  // the loop here. Emit `.break()` and end the region — recompiling recreates
2402
2436
  // exactly this edge and adds no continue back-edge for the path.
@@ -2565,13 +2599,20 @@ function declaredExitsOfNode(node) {
2565
2599
  */
2566
2600
  function rawNodeExits(node) {
2567
2601
  const type = String(node.type);
2602
+ // A registered type (an HTTP call, say) answers from its own `exits`.
2603
+ if (registeredImpl(node) !== undefined)
2604
+ return undefined;
2568
2605
  if (Object.values(T).includes(type) || isTrigger(type))
2569
2606
  return undefined;
2570
2607
  if (['uipath.connector.', 'uipath.core.', 'uipath.ixp.', 'uipath.agent.', 'uipath.conversational.']
2571
2608
  .some((p) => type.startsWith(p)))
2572
2609
  return undefined;
2573
- const def = runDefs.get(`${type}@${node.typeVersion}`) ?? runDefs.get(type);
2574
- return forwardFanOut(def, node.inputs)?.map((port) => ({ value: port, port }));
2610
+ return forwardFanOut(rawNodeDefinition(node), node.inputs)?.map((port) => ({ value: port, port }));
2611
+ }
2612
+ /** The definition the file carries for `node`'s type (its version's, else the type's). */
2613
+ function rawNodeDefinition(node) {
2614
+ const type = String(node.type);
2615
+ return runDefs.get(`${type}@${node.typeVersion}`) ?? runDefs.get(type);
2575
2616
  }
2576
2617
  /**
2577
2618
  * Split a switch case expression into `<discriminant> === <literal>`, the only
@@ -52,6 +52,12 @@ export interface FlowNodeContribution extends Omit<NodeContribution, 'definition
52
52
  readonly port: string;
53
53
  readonly endsRun?: boolean;
54
54
  }>;
55
+ /**
56
+ * The node is a human task with outcomes that leaves on its legacy `completed`
57
+ * handle, because its author set `legacyCompleted: true`. Exempts it from the
58
+ * guard that refuses a `completed` edge on such a task (`hitlExitViolation`).
59
+ */
60
+ legacyCompleted?: boolean;
55
61
  /**
56
62
  * `outputs` REPLACES the definition's `outputDefinition` on the node instead of
57
63
  * being merged over it — for an instance whose output slots are its own (a
@@ -6,6 +6,8 @@ import { hitlDerivesOutcomeHandles, outcomeIdOf } from '../../../core/hitl-routi
6
6
  export const HITL_DIAGNOSTICS = {
7
7
  HITL_VARIANT_UNKNOWN: { severity: 'error', summary: 'a variant that names no human-task node type' },
8
8
  HITL_OUTCOME_PORTS_VARIANT: { severity: 'error', summary: 'exposeError or outcomePorts on a variant that cannot honour it' },
9
+ HITL_LEGACY_COMPLETED: { severity: 'warning', summary: "a task with outcomes that leaves on the legacy 'completed' handle" },
10
+ HITL_LEGACY_COMPLETED_CONFLICT: { severity: 'error', summary: 'legacyCompleted with outcomePorts or exposeError, or where it changes nothing' },
9
11
  HITL_DOCUMENT_WITHOUT_VARIANT: { severity: 'error', summary: 'a document on a task that is not a validation station' },
10
12
  HITL_DOCVAL_WITH_FIELDS: { severity: 'error', summary: 'form fields on a validation station' },
11
13
  HITL_DOCVAL_NO_DOCUMENT: { severity: 'error', summary: 'a validation station with no document' },
@@ -86,6 +88,28 @@ export function checkHitl(inputs, ctx) {
86
88
  : `Route on out('${step}', 'Action') downstream.`);
87
89
  return;
88
90
  }
91
+ // `legacyCompleted: true` keeps a pre-#3512 file's `completed` edge (#1197).
92
+ // It is the file's shape, so it compiles, but what it does at run time is
93
+ // rarely what the author wants, and `flow validate` says nothing about it.
94
+ if (inputs?.legacyCompleted === true) {
95
+ const outcomes = Array.isArray(inputs?.outcomes) ? inputs.outcomes : [];
96
+ if (inputs?.outcomePorts !== undefined || inputs?.exposeError === true) {
97
+ at('HITL_LEGACY_COMPLETED_CONFLICT', `Human task "${step}" sets legacyCompleted: true, which leaves it on its single 'completed' handle, and also `
98
+ + `${inputs?.exposeError === true ? 'exposeError' : 'outcomePorts'}, which asks for per-outcome exits.`, 'Drop `legacyCompleted`.');
99
+ return;
100
+ }
101
+ if (!hitlDerivesOutcomeHandles(variant) || outcomes.length === 0) {
102
+ at('HITL_LEGACY_COMPLETED_CONFLICT', `Human task "${step}" sets legacyCompleted: true, but ${outcomes.length === 0 ? 'it has no outcomes'
103
+ : `its variant is "${variant}", which reads its outcomes from its app`}, so 'completed' is already its only exit.`, 'Drop `legacyCompleted`.');
104
+ return;
105
+ }
106
+ const first = typeof outcomes[0] === 'string' ? outcomes[0] : outcomes[0]?.name;
107
+ at('HITL_LEGACY_COMPLETED', `Human task "${step}" leaves on the legacy 'completed' handle (legacyCompleted: true). Studio Web and the `
108
+ + `runtime read that edge as the primary outcome, ${JSON.stringify(String(first ?? ''))}, only`
109
+ + (outcomes.length > 1 ? ': when the reviewer picks any other outcome, the run stops at the task.' : '.'), outcomes.length > 1
110
+ ? 'outcomePorts: false (every outcome continues to the next step), or outcomePorts: true / .stepSwitch (route each one)'
111
+ : 'Drop `legacyCompleted`: one outcome continues to the next step on its own handle.');
112
+ }
89
113
  if (inputs?.document !== undefined && !isDocval) {
90
114
  at('HITL_DOCUMENT_WITHOUT_VARIANT', `Human task "${step}" names a document to validate, but its variant is `
91
115
  + `${variant === undefined ? 'the default node type' : `"${variant}"`} — only the validation `
@@ -252,6 +252,12 @@ function hitlExits({ inputs }, pinnedVersion) {
252
252
  if (model.shape === 'placeholder') {
253
253
  // WHY it has no outcome exits decides what the author can do about it, so
254
254
  // the caller gets the specific one rather than "ports are off".
255
+ if (model.legacyCompleted) {
256
+ return {
257
+ reason: "legacyCompleted: true leaves it on its single 'completed' exit",
258
+ suggestion: 'Drop `legacyCompleted` to route its outcomes.',
259
+ };
260
+ }
255
261
  if ((inputs?.outcomes ?? []).length === 0) {
256
262
  return { reason: 'it declares no outcomes', suggestion: `outcomes: ['Approve', 'Reject']` };
257
263
  }
@@ -413,24 +419,24 @@ function liftHitl(node, typeName, ctx) {
413
419
  const outcomePorts = (typeName === BASE && version === '1.1')
414
420
  || (derivesHandles && !versionRoutes && !ctx.convergedFanOut()
415
421
  && outPorts.some((p) => p.startsWith('outcome-')));
416
- // A lone `completed` edge on a task with several outcomes is the shape the
417
- // designer stopped drawing: before flow-workbench #3512 (09-08) it was
418
- // unconditioned, so every outcome continued down it; since then Studio Web and
419
- // the converter remap it to the primary outcome only. Recompiling writes one
420
- // `outcome-<id>` edge per outcome to the same step — the first meaning — so
421
- // this is the one place decompile→compile is deliberately not exact. Say so.
422
- const legacyCompleted = derivesHandles && outcomeIdsOfNode(n).length > 1 && outPorts.includes('completed');
423
- // Both a converged fan-out and a legacy lone `completed` mean "every outcome
424
- // continues to the next step". Say so in the source with `outcomePorts: false`
425
- // rather than leaving it to the `{ version }` pin, so the intent survives an
426
- // author dropping the pin.
427
- const fanOut = !outcomePorts && derivesHandles && !versionRoutes && outcomeIdsOfNode(n).length > 1
428
- && (legacyCompleted || ctx.convergedFanOut());
422
+ // A `completed` edge on a task with outcomes is the shape the designer
423
+ // stopped drawing: before flow-workbench #3512 (09-08) it was unconditioned,
424
+ // so every outcome continued down it; since then Studio Web and the converter
425
+ // remap it to the primary outcome only. The source keeps it as it is,
426
+ // `legacyCompleted: true`, so a round trip with no edits writes the same edge
427
+ // back (#1197) and the author decides what it should mean.
428
+ const legacyCompleted = derivesHandles && !versionRoutes && outcomeIdsOfNode(n).length > 0
429
+ && outPorts.includes('completed');
430
+ // A converged fan-out means "every outcome continues to the next step". Say so
431
+ // in the source with `outcomePorts: false` rather than leaving it to the
432
+ // `{ version }` pin, so the intent survives an author dropping the pin.
433
+ const fanOut = !outcomePorts && !legacyCompleted && derivesHandles && !versionRoutes
434
+ && outcomeIdsOfNode(n).length > 1 && ctx.convergedFanOut();
429
435
  if (legacyCompleted) {
430
436
  ctx.warn(`human task ${JSON.stringify(n.id)} leaves on the legacy 'completed' handle, which Studio Web no longer draws `
431
- + `once a task has outcomes (it reads the edge as the primary outcome only). Recompiling emits one `
432
- + `outcome-<id> edge per outcome to the same step, so every outcome continues — what the edge meant `
433
- + `before flow-workbench #3512.`);
437
+ + `once a task has outcomes: it runs that edge as the primary outcome only, so any other outcome stops the run. `
438
+ + `The source keeps it (legacyCompleted: true), so recompiling writes the same edge. To continue every outcome, `
439
+ + `replace it with outcomePorts: false; to route each one, outcomePorts: true or .stepSwitch.`);
434
440
  }
435
441
  const title = plain(i.title);
436
442
  const priority = plain(i.priority);
@@ -447,7 +453,8 @@ function liftHitl(node, typeName, ctx) {
447
453
  // there is serialize's own, not the author's.
448
454
  ...(fields.length || (variant !== 'action-app' && variant !== 'document-validation') ? { fields } : {}),
449
455
  outcomes,
450
- ...(outcomePorts ? { outcomePorts: true } : fanOut ? { outcomePorts: false } : {}),
456
+ ...(legacyCompleted ? { legacyCompleted: true }
457
+ : outcomePorts ? { outcomePorts: true } : fanOut ? { outcomePorts: false } : {}),
451
458
  ...(version === '1.2' ? { exposeError: true } : {}),
452
459
  };
453
460
  return { kind: 'hitl', inputs: lifted };
@@ -511,6 +518,7 @@ function printHitl({ inputs }, ctx) {
511
518
  ['outcomes', `[${outcomes.join(', ')}]`],
512
519
  ['outcomePorts', h.outcomePorts === true ? 'true' : h.outcomePorts === false ? 'false' : undefined],
513
520
  ['exposeError', h.exposeError ? 'true' : undefined],
521
+ ['legacyCompleted', h.legacyCompleted ? 'true' : undefined],
514
522
  ],
515
523
  };
516
524
  }
@@ -535,6 +543,14 @@ function hitlAt(typeName, version, factory) {
535
543
  throw new Error(`"${ctx.step}": outcomePorts: true, but the '${inputs?.variant}' variant reads its outcomes `
536
544
  + `from its app and leaves on its single 'completed' exit. Route on out('${ctx.step}', 'Action') downstream.`);
537
545
  }
546
+ if (inputs?.legacyCompleted === true && (inputs?.outcomePorts !== undefined || inputs?.exposeError === true)) {
547
+ throw new Error(`"${ctx.step}": legacyCompleted: true leaves the task on its single 'completed' handle, so it cannot also `
548
+ + `${inputs?.exposeError === true ? 'expose the error output (exposeError)' : 'set outcomePorts'}. Drop one.`);
549
+ }
550
+ if (inputs?.legacyCompleted === true && typeName === BASE && (pinned === '1.1' || pinned === '1.2')) {
551
+ throw new Error(`"${ctx.step}": legacyCompleted: true, but { version: '${pinned}' } pins the task's per-outcome definition, `
552
+ + `which has no 'completed' handle (flow validate refuses the edge). Drop the pin or legacyCompleted.`);
553
+ }
538
554
  if (inputs?.exposeError === true && pinned !== undefined && pinned !== '1.2') {
539
555
  throw new Error(`"${ctx.step}": exposeError needs the task's 1.2 definition, but { version: '${pinned}' } pins it. `
540
556
  + `Drop the pin or match it.`);
@@ -554,8 +570,9 @@ function hitlAt(typeName, version, factory) {
554
570
  return { ...contribution, fanOut: exitModel.exits.map((e) => ({ port: e.port, ...(e.endsProcess ? { endsRun: true } : {}) })) };
555
571
  }
556
572
  // Every human-task definition declares one static source handle,
557
- // `completed`: the real exit on an app-backed task and on one with no outcomes.
558
- return { ...contribution, port: 'completed' };
573
+ // `completed`: the real exit on an app-backed task and on one with no
574
+ // outcomes, and the author's explicit choice with `legacyCompleted: true`.
575
+ return { ...contribution, port: 'completed', ...(exitModel.legacyCompleted ? { legacyCompleted: true } : {}) };
559
576
  },
560
577
  deserialize: (node, ctx) => liftHitl(node, typeName, ctx),
561
578
  print: printHitl,
@@ -249,6 +249,39 @@ export interface HitlInputs {
249
249
  * {@link HitlInputs.outcomePorts | outcomePorts}. Default node only.
250
250
  */
251
251
  exposeError?: boolean;
252
+ /**
253
+ * Leave on the task's legacy `completed` handle, as a file saved before
254
+ * Studio Web drew one handle per outcome does. `decompile` writes this for
255
+ * such a file, so a round trip with no edits gives its `completed` edge back
256
+ * unchanged instead of rewriting it into one `outcome-<id>` edge per outcome.
257
+ *
258
+ * Not for new flows. Studio Web and the runtime read a `completed` edge on a
259
+ * task with outcomes as the PRIMARY (first) outcome only: when the reviewer
260
+ * picks any other outcome, that path has no edge and the run stops there.
261
+ * `check` warns (`HITL_LEGACY_COMPLETED`). To continue every outcome to the
262
+ * next step, drop this and set
263
+ * {@link HitlInputs.outcomePorts | outcomePorts}`: false`; to route each
264
+ * outcome, use `outcomePorts: true` or `.stepSwitch`.
265
+ *
266
+ * Only on the default node and `'quick-form'` with at least one outcome (the
267
+ * other variants always leave on `completed`), and not with `outcomePorts` or
268
+ * `exposeError`.
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * // Decompiled from a task whose one edge leaves on `completed`.
273
+ * export default flow('review')
274
+ * .step('review1', hitl({
275
+ * variant: 'quick-form',
276
+ * fields: [{ id: 'comments', type: 'text', direction: 'output' }],
277
+ * outcomes: ['Submit', 'Reject'],
278
+ * legacyCompleted: true,
279
+ * }))
280
+ * .step('approved1', script({ code: 'return 1;' }))
281
+ * .build();
282
+ * ```
283
+ */
284
+ legacyCompleted?: boolean;
252
285
  /**
253
286
  * The document to validate. Required by — and only legal with —
254
287
  * `variant: 'document-validation'`.
package/dist/serialize.js CHANGED
@@ -57,6 +57,7 @@ import { resolveSchemaVersion } from './schema-version.js';
57
57
  import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
58
58
  import { outputBackingGlobal, planOutputBacking } from './flow-output-backing.js';
59
59
  import { mapJsCode } from './core/js-code-spans.js';
60
+ import { contributionPort } from './core/definition-handles.js';
60
61
  import { prepareCommand } from './core/cli-spelling.js';
61
62
  import { AGENTIC_PROCESS_FORMS } from './core/agentic-process-forms.js';
62
63
  import { resourceNodeSortOrder } from './core/process-resource-sort-order.js';
@@ -3531,18 +3532,6 @@ function declaresForwardPort(def, port) {
3531
3532
  return port.length > head.length + tail.length && port.startsWith(head) && port.endsWith(tail);
3532
3533
  });
3533
3534
  }
3534
- /**
3535
- * Forward port: the first concrete source handle (`isDefaultForType` wins), else `output`;
3536
- * `undefined` when every forward source handle is templated (`case-{item.id}`).
3537
- */
3538
- function contributionPort(def) {
3539
- const handles = (def?.handleConfiguration ?? []).flatMap((g) => g?.handles ?? []);
3540
- const sources = handles.filter((h) => h?.type === 'source' && h?.id !== 'error');
3541
- const concrete = sources.filter((h) => typeof h?.id === 'string' && !h.id.includes('{'));
3542
- if (concrete.length === 0 && sources.length > 0)
3543
- return undefined;
3544
- return concrete.find((h) => h?.isDefaultForType)?.id ?? concrete[0]?.id ?? 'output';
3545
- }
3546
3535
  /**
3547
3536
  * Render a switch case value as a JS literal for the case expression.
3548
3537
  *
@@ -4434,6 +4423,13 @@ export function serialize(built, opts = {}) {
4434
4423
  * `.stepToRef` rejoins now do come through here, #938).
4435
4424
  */
4436
4425
  const hitlNodes = new Map();
4426
+ /**
4427
+ * Human tasks with outcomes that set `legacyCompleted: true`: they leave on
4428
+ * `completed` by the author's choice, so the guard exempts that one port
4429
+ * (flow-builder-sdk#1197). They are not tracked in `hitlNodes`, whose tails are
4430
+ * outcome edges; theirs is the one `completed` edge.
4431
+ */
4432
+ const legacyCompletedNodes = new Set();
4437
4433
  /** Step name → node id of a step whose every forward exit is templated (see `contributionPort`). */
4438
4434
  const routerNodes = new Map();
4439
4435
  /**
@@ -4447,7 +4443,7 @@ export function serialize(built, opts = {}) {
4447
4443
  const hitlEndExits = [];
4448
4444
  function addEdge(scope, from, toNode, toPort = 'input') {
4449
4445
  const hitlNode = hitlNodes.get(from.node);
4450
- const refused = hitlNode ? hitlExitViolation(hitlNode, from.port) : undefined;
4446
+ const refused = hitlNode ? hitlExitViolation(hitlNode, from.port, legacyCompletedNodes.has(from.node)) : undefined;
4451
4447
  if (refused)
4452
4448
  throw new Error(`serialize: ${refused}`);
4453
4449
  scope.edges.push({ id: `edge_${edgeSeq++}`, sourceNodeId: from.node, sourcePort: from.port, targetNodeId: toNode, targetPort: toPort });
@@ -4623,7 +4619,7 @@ export function serialize(built, opts = {}) {
4623
4619
  for (const vd of childGlobals.vars)
4624
4620
  globals.push({ id: vd.name, name: vd.name, direction: 'inout', type: vd.type, ...varMeta(vd) });
4625
4621
  resolveRejoins(scope, addEdge);
4626
- assertHitlExitInvariant(scope.nodes, scope.edges);
4622
+ assertHitlExitInvariant(scope.nodes, scope.edges, legacyCompletedNodes);
4627
4623
  typeAgentInputSchemas(scope, globals);
4628
4624
  emitStickyNotes(child, scope, claimNoteId);
4629
4625
  const layoutEdges = applyAuthorLayout(child.layout, scope, `subflow "${nodeId}"`);
@@ -4826,7 +4822,9 @@ export function serialize(built, opts = {}) {
4826
4822
  // edge out of it is held to that rule (`hitlExitViolation`, in addEdge), and
4827
4823
  // the step list treats its tails as outcome edges. A rawNode of the type is
4828
4824
  // not one: it routes by the port lists its author wrote.
4829
- if (o.trackOutcomes && OUTCOME_HANDLE_NODE_TYPES.has(o.typeName))
4825
+ if (o.trackOutcomes && contribution.legacyCompleted)
4826
+ legacyCompletedNodes.add(id);
4827
+ else if (o.trackOutcomes && OUTCOME_HANDLE_NODE_TYPES.has(o.typeName))
4830
4828
  hitlNodes.set(id, node);
4831
4829
  for (const t of o.tails)
4832
4830
  addEdge(scope, t, id);
@@ -5990,10 +5988,13 @@ export function serialize(built, opts = {}) {
5990
5988
  * Refuse a side exit on a human task's port that the step's tails already
5991
5989
  * continue from: that would put two edges on one port (flow-builder-sdk#741).
5992
5990
  * After a fan-out task that is EVERY outcome port. `check` reports the same
5993
- * as HITL_OUTCOME_PORTS_OFF; this is the compile-side half.
5991
+ * as HITL_OUTCOME_PORTS_OFF; this is the compile-side half. A
5992
+ * `legacyCompleted` task counts too: its main path leaves on `completed`, so a
5993
+ * side route there is the same second edge.
5994
5994
  */
5995
5995
  function refuseRewire(method, port, source, tails) {
5996
- if (!hitlNodes.has(tails[0]?.node ?? '') || !tails.some((t) => t.port === port))
5996
+ const node = tails[0]?.node ?? '';
5997
+ if (!(hitlNodes.has(node) || legacyCompletedNodes.has(node)) || !tails.some((t) => t.port === port))
5997
5998
  return;
5998
5999
  throw new Error(tails.length > 1
5999
6000
  ? `${method}('${port}', …) after human task "${source}": every outcome already continues to the next `
@@ -6652,7 +6653,7 @@ export function serialize(built, opts = {}) {
6652
6653
  }
6653
6654
  }
6654
6655
  resolveRejoins(root, addEdge);
6655
- assertHitlExitInvariant(root.nodes, root.edges);
6656
+ assertHitlExitInvariant(root.nodes, root.edges, legacyCompletedNodes);
6656
6657
  typeAgentInputSchemas(root, globals);
6657
6658
  emitStickyNotes(built, root, claimNoteId);
6658
6659
  const rootLayoutEdges = applyAuthorLayout(built.layout, root, `flow "${built.id}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.29.1",
3
+ "version": "6.30.0",
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": "5dee9e0ebdef1bca6c586f4802285a0f429ae432"
95
+ "gitref": "5b38f3fae80cbcb874c6d92afb49690b77656caf"
96
96
  }