@uipath/maestro-builder-sdk 6.8.3 → 6.8.5

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/serialize.js CHANGED
@@ -14,8 +14,8 @@ import { Expr, toExpr, SCHEDULE_PRESETS, DELAY_PRESETS, parseIxpProjectType, fla
14
14
  import { FlowAction, FlowTrigger } from './core/node-classes.js';
15
15
  import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
16
16
  import { bindingSelfNameMessage } from './core/binding-messages.js';
17
- import { hitlRoutesPerOutcome } from './core/hitl-routing.js';
18
- import { declaredExits, hasExits } from './core/step-ports.js';
17
+ import { OUTCOME_HANDLE_NODE_TYPES, assertHitlExitInvariant, hitlExitModel, hitlExitViolation, outcomeSlug } from './core/hitl-routing.js';
18
+ import { declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
19
19
  import { buildConfiguration, transportHttpMethod } from './config.js';
20
20
  import { stableId } from './core/stable-id.js';
21
21
  import { readEventFilter, eventFilterProblem, eventFilterJmes, eventFilterTreeLeaf } from './event-filters.js';
@@ -2793,14 +2793,13 @@ function bindingPath(value, rename) {
2793
2793
  // `__hitlInputBindings` the designer reads back stores the binding verbatim.
2794
2794
  return path.startsWith('vars.') ? path : `vars.${path}`;
2795
2795
  }
2796
- /** `Approve` → `approve`; a name that slugifies to nothing falls back to its index. */
2796
+ /**
2797
+ * `Approve` → `approve`; a name that slugifies to nothing falls back to its index.
2798
+ * The same rule that names the `outcome-<id>` ports (`outcomeSlug`), so an id
2799
+ * this writes into `schema.outcomes` is always one a port can name.
2800
+ */
2797
2801
  function outcomeId(name, index) {
2798
- const slug = String(name ?? '')
2799
- .trim()
2800
- .toLowerCase()
2801
- .replace(/[^a-z0-9]+/g, '-')
2802
- .replace(/^-+|-+$/g, '');
2803
- return slug || `outcome-${index + 1}`;
2802
+ return outcomeSlug(name, index);
2804
2803
  }
2805
2804
  /**
2806
2805
  * A deterministic UUID for the form schema, derived from the flow and node ids.
@@ -3475,8 +3474,18 @@ function eventDetail(rc, sub, nodeId, opts) {
3475
3474
  errorState: { issues: [] },
3476
3475
  };
3477
3476
  }
3478
- /** Recursively render Exprs nested inside objects/arrays (for connector inputs). */
3477
+ /**
3478
+ * Recursively render Exprs nested inside objects/arrays (for connector inputs).
3479
+ *
3480
+ * A lookup token is REFUSED, never rendered. `buildConnectorInputs` swaps every
3481
+ * token it can place — a field's whole value, or an element of an array — so a
3482
+ * token that reaches here sits where nothing resolves it: inside an object value,
3483
+ * or in a custom node's inputs. Recursing into it would put `{"__lookup": …}` on
3484
+ * the wire where the connector expects an id.
3485
+ */
3479
3486
  function renderDeep(val, rename = NO_RENAME) {
3487
+ if (isLookupToken(val))
3488
+ throw new Error(misplacedLookupMessage(val));
3480
3489
  if (val instanceof Expr)
3481
3490
  return renderValue(val, rename);
3482
3491
  if (Array.isArray(val))
@@ -3489,6 +3498,13 @@ function renderDeep(val, rename = NO_RENAME) {
3489
3498
  }
3490
3499
  return val;
3491
3500
  }
3501
+ /** Why {@link renderDeep} refused a token: where a lookup CAN go, and what it would have emitted. */
3502
+ function misplacedLookupMessage(token) {
3503
+ const { field, by, value } = token.__lookup;
3504
+ return (`lookup('${field}') (${by}=${JSON.stringify(value)}) is in a position no lookup is resolved from: ` +
3505
+ `a lookup() must be the whole value of a connector input, or an element of an array input. ` +
3506
+ `Emitting it here would send a raw {"__lookup": …} object where the connector expects an id.`);
3507
+ }
3492
3508
  function newScope(outputs, vars = []) {
3493
3509
  return {
3494
3510
  nodes: [],
@@ -3859,7 +3875,28 @@ export function serialize(built, opts = {}) {
3859
3875
  scope.stepNodeId.set(stepName, id);
3860
3876
  return id;
3861
3877
  }
3878
+ /**
3879
+ * Base and quick-form human tasks emitted so far, by node id. `addEdge` holds
3880
+ * every edge out of one to the product's handle rule (`hitlExitViolation`), so
3881
+ * `completed` on a task with outcomes fails at the edge that would write it;
3882
+ * `assertHitlExitInvariant` repeats the check over each finished scope for the
3883
+ * edges that do not come through here (parked `.stepToRef` rejoins).
3884
+ */
3885
+ const hitlNodes = new Map();
3886
+ /**
3887
+ * Human tasks with an outcome whose path stops at an End while another
3888
+ * outcome continues — read by `.parallel`, whose join would wait forever for
3889
+ * the continuing path (check's HITL_END_OUTCOME_IN_PARALLEL). Two ways in: a
3890
+ * fan-out task's `action: 'End'` outcome, and a `.stepSwitch` outcome with no
3891
+ * arm (it gets an End) or whose arm ends in `.return()`. `why` finishes the
3892
+ * sentence "human task X …" in the refusal.
3893
+ */
3894
+ const hitlEndExits = [];
3862
3895
  function addEdge(scope, from, toNode, toPort = 'input') {
3896
+ const hitlNode = hitlNodes.get(from.node);
3897
+ const refused = hitlNode ? hitlExitViolation(hitlNode, from.port) : undefined;
3898
+ if (refused)
3899
+ throw new Error(`serialize: ${refused}`);
3863
3900
  scope.edges.push({ id: `edge_${edgeSeq++}`, sourceNodeId: from.node, sourcePort: from.port, targetNodeId: toNode, targetPort: toPort });
3864
3901
  }
3865
3902
  let col = 0;
@@ -3959,6 +3996,7 @@ export function serialize(built, opts = {}) {
3959
3996
  for (const vd of child.vars)
3960
3997
  globals.push({ id: vd.name, name: vd.name, direction: 'inout', type: vd.type, ...varMeta(vd) });
3961
3998
  resolveRejoins(scope);
3999
+ assertHitlExitInvariant(scope.nodes, scope.edges);
3962
4000
  typeAgentInputSchemas(scope, globals);
3963
4001
  emitStickyNotes(child, scope);
3964
4002
  const layoutEdges = applyAuthorLayout(child.layout, scope, `subflow "${nodeId}"`);
@@ -4077,13 +4115,11 @@ export function serialize(built, opts = {}) {
4077
4115
  // the shipped `sample-flows/queue-operations.flow` wires its edges to it.
4078
4116
  if (type === NODE_TYPE.queueCreate || type === NODE_TYPE.queueCreateAndWait)
4079
4117
  return 'success';
4080
- // The human task's definition declares exactly one source handle, `completed`
4081
- // — it fires when the person submits, whichever outcome they chose. (Per-outcome
4082
- // handles exist only from typeVersion 1.1 of the node, which this SDK does not
4083
- // emit; routing on the outcome is a decision downstream, reading `output.Action`,
4084
- // which is also how flow-v1's own outcome conditions are written.)
4085
- // — and all three human-task node types declare the same one, so the port is
4086
- // the same whichever `variant` was named.
4118
+ // Every human-task definition declares one static source handle, `completed`.
4119
+ // It is the real exit only where the product draws it: action-app,
4120
+ // document-validation, and a task with no outcomes. A base or quick-form task
4121
+ // with outcomes leaves on `outcome-<id>` instead (`hitlExitModel`), and never
4122
+ // reaches this function for its tail.
4087
4123
  if (type === NODE_TYPE.hitl || type === NODE_TYPE.hitlQuickForm || type === NODE_TYPE.hitlActionApp
4088
4124
  || type === NODE_TYPE.hitlDocValidation)
4089
4125
  return 'completed';
@@ -5094,28 +5130,31 @@ export function serialize(built, opts = {}) {
5094
5130
  if (spec.kind === 'hitl') {
5095
5131
  const id = stepUid(scope, step.name);
5096
5132
  const hitlType = hitlNodeType(spec.inputs);
5097
- // Rich options select a newer definition, same rule as the scheduled
5098
- // trigger's cron: per-outcome routing needs 1.1 (per-outcome
5099
- // `outcome-<id>` exits) and `exposeError` needs 1.2 (adds the `error`
5100
- // output; its exits are the same per-outcome handles). A zero- or
5101
- // one-outcome task stays on the pinned default, byte-identically.
5133
+ // How the task leaves its node — `hitlExitModel` (core/hitl-routing.ts)
5134
+ // has the product rule and what `.step()` means after each shape.
5102
5135
  //
5103
- // More than one outcome INFERS the routing — see `hitlRoutesPerOutcome`
5104
- // for the four things that turn that off, one of which is an explicit
5105
- // `{ version }`, which is what keeps decompile→compile exact.
5136
+ // Only the BASE node has definition versions to select: routing per
5137
+ // outcome picks 1.1 and `exposeError` picks 1.2 (which adds the `error`
5138
+ // output). quick-form draws the same per-outcome handles on the one
5139
+ // definition it has, so it routes per outcome without moving. An explicit
5140
+ // pin wins over the inferred 1.1 — `flow validate` accepts `outcome-<id>`
5141
+ // edges on base@1.0 — but not over `exposeError`, whose output only 1.2
5142
+ // declares. A zero- or one-outcome task stays on the pinned default.
5106
5143
  const pinned = step.options?.version;
5107
- const outcomeRouted = hitlRoutesPerOutcome(spec.inputs, pinned);
5108
- const neededVersion = spec.inputs?.exposeError ? '1.2' : outcomeRouted ? '1.1' : undefined;
5109
- if (neededVersion !== undefined && hitlType !== NODE_TYPE.hitl) {
5110
- throw new Error(`"${step.name}": outcomePorts/exposeError need the base human task's ${neededVersion} `
5111
- + `definition — the '${spec.inputs?.variant}' variant has no per-outcome version. `
5112
- + `Drop the variant, or route on out('${step.name}', 'Action') downstream.`);
5144
+ const exitModel = hitlExitModel(spec.inputs, pinned);
5145
+ if (hitlType !== NODE_TYPE.hitl && spec.inputs?.exposeError === true) {
5146
+ throw new Error(`"${step.name}": exposeError needs the base human task's 1.2 definition, which adds the `
5147
+ + `error output — the '${spec.inputs?.variant}' variant has no such version. Drop the variant or exposeError.`);
5113
5148
  }
5114
- // Both refusals below can only be reached by an EXPLICIT flag: the
5115
- // inference already declines on a variant and on a pinned version.
5116
- if (neededVersion !== undefined && pinned !== undefined && pinned !== neededVersion) {
5117
- throw new Error(`"${step.name}": ${spec.inputs?.exposeError ? 'exposeError' : 'outcomePorts'} needs the task's ${neededVersion} `
5118
- + `definition, but { version: '${pinned}' } pins it. Drop the pin or match it.`);
5149
+ if (spec.inputs?.outcomePorts === true && !OUTCOME_HANDLE_NODE_TYPES.has(hitlType)) {
5150
+ throw new Error(`"${step.name}": outcomePorts: true, but the '${spec.inputs?.variant}' variant reads its outcomes `
5151
+ + `from its app and leaves on its single 'completed' exit. Route on out('${step.name}', 'Action') downstream.`);
5152
+ }
5153
+ const neededVersion = hitlType !== NODE_TYPE.hitl ? undefined
5154
+ : spec.inputs?.exposeError ? '1.2' : exitModel.shape === 'per-outcome' ? '1.1' : undefined;
5155
+ if (spec.inputs?.exposeError === true && pinned !== undefined && pinned !== '1.2') {
5156
+ throw new Error(`"${step.name}": exposeError needs the task's 1.2 definition, but { version: '${pinned}' } pins it. `
5157
+ + `Drop the pin or match it.`);
5119
5158
  }
5120
5159
  const node = makeNode(hitlType, id, step.name, { parentId, requestedVersion: pinned ?? neededVersion });
5121
5160
  const { inputs, outputs } = hitlInputs(spec.inputs, id, built.id, step.name, scope.rename);
@@ -5124,15 +5163,19 @@ export function serialize(built, opts = {}) {
5124
5163
  // gives their shape (see hitlInputs), so the instance's version wins.
5125
5164
  node.outputs = outputs;
5126
5165
  scope.nodes.push(node);
5166
+ if (OUTCOME_HANDLE_NODE_TYPES.has(hitlType))
5167
+ hitlNodes.set(id, node);
5127
5168
  for (const t of tails)
5128
5169
  addEdge(scope, t, id);
5129
- if (outcomeRouted) {
5130
- // 1.1+ has no `completed` handle — each outcome is its own exit. The
5131
- // PRIMARY (first) outcome continues the main path; the author routes
5132
- // the rest with .stepToList('outcome-<id>', …).
5133
- const first = spec.inputs.outcomes?.[0];
5134
- const firstName = typeof first === 'string' ? first : first?.name;
5135
- return [{ node: id, port: `outcome-${outcomeId(firstName, 0)}` }];
5170
+ if (exitModel.shape === 'per-outcome') {
5171
+ // The PRIMARY (first) outcome continues the main path; the author routes
5172
+ // the rest with .stepToList('outcome-<id>', …) or as .stepSwitch arms.
5173
+ return [{ node: id, port: exitModel.exits[0]?.port ?? `outcome-${outcomeSlug(undefined, 0)}` }];
5174
+ }
5175
+ if (exitModel.shape === 'every-outcome') {
5176
+ // Fan-out: every outcome continues to the next step, each on its own
5177
+ // handle. End outcomes are marked, and `emit` ends them.
5178
+ return exitModel.exits.map((e) => ({ node: id, port: e.port, ...(e.endsProcess ? { endsRun: true } : {}) }));
5136
5179
  }
5137
5180
  return [{ node: id, port: successPort(hitlType) }];
5138
5181
  }
@@ -5370,6 +5413,34 @@ export function serialize(built, opts = {}) {
5370
5413
  * are backward — every deployed rejoin is forward convergence, which is why
5371
5414
  * `check` refuses a backward one outright.
5372
5415
  */
5416
+ /**
5417
+ * An End node on one exit of `nodeId`, so the run FINISHES there — a
5418
+ * `.stepSwitch` exit with no arm, and a fan-out human task's `action: 'End'`
5419
+ * outcome.
5420
+ */
5421
+ function endExit(scope, nodeId, port, stepName, parentId) {
5422
+ const endId = uid(scope, `${stepName}${port.charAt(0).toUpperCase()}${port.slice(1)}End`);
5423
+ const end = makeNode(NODE_TYPE.end, endId, 'End', parentId ? { parentId } : {});
5424
+ end.inputs = {};
5425
+ scope.nodes.push(end);
5426
+ addEdge(scope, { node: nodeId, port }, endId);
5427
+ }
5428
+ /**
5429
+ * Refuse a side exit on a human task's port that the step's tails already
5430
+ * continue from: that would put two edges on one port (flow-builder-sdk#741).
5431
+ * After a fan-out task that is EVERY outcome port. `check` reports the same
5432
+ * as HITL_OUTCOME_PORTS_OFF; this is the compile-side half.
5433
+ */
5434
+ function refuseRewire(method, port, source, tails) {
5435
+ if (!hitlNodes.has(tails[0]?.node ?? '') || !tails.some((t) => t.port === port))
5436
+ return;
5437
+ throw new Error(tails.length > 1
5438
+ ? `${method}('${port}', …) after human task "${source}": every outcome already continues to the next `
5439
+ + `step, '${port}' included, because this task does not route per outcome. Add outcomePorts: true `
5440
+ + `(the first outcome then continues and the others are routed), or route every outcome with .stepSwitch.`
5441
+ : `${method}('${port}', …) after human task "${source}": '${port}' already continues the main path, so `
5442
+ + `routing it here too would put two edges on one port (flow-builder-sdk#741).`);
5443
+ }
5373
5444
  /**
5374
5445
  * A step list hanging off a named PORT of the step just emitted.
5375
5446
  *
@@ -5379,12 +5450,14 @@ export function serialize(built, opts = {}) {
5379
5450
  */
5380
5451
  function emitPortList(step, source, tails, scope, parentId, breakTarget) {
5381
5452
  const handler = step.body;
5382
- // `emitAction` returns exactly one tail for every action kind — the node's
5383
- // own success port. Anything else means a new action shape reached here
5384
- // without being taught about error handling; say so rather than guess.
5385
- if (tails.length !== 1) {
5386
- throw new Error(`.stepToList('${step.port}', …) after step "${source}": expected the step to leave exactly one outgoing port, got ${tails.length}.`);
5453
+ // `emitAction` returns one tail for every action kind — the node's own
5454
+ // success port — except a fan-out human task, which returns one per outcome
5455
+ // from the SAME node. Tails from two nodes mean a new action shape reached
5456
+ // here without being taught about side exits; say so rather than guess.
5457
+ if (tails.length === 0 || tails.some((t) => t.node !== tails[0].node)) {
5458
+ throw new Error(`.stepToList('${step.port}', …) after step "${source}": expected the step to leave from exactly one node, got ${tails.length} tail(s).`);
5387
5459
  }
5460
+ refuseRewire('.stepToList', step.port, source, tails);
5388
5461
  const nodeId = tails[0].node;
5389
5462
  const node = scope.nodes.find((n) => n.id === nodeId);
5390
5463
  if (!node)
@@ -5451,12 +5524,33 @@ export function serialize(built, opts = {}) {
5451
5524
  throw new Error(`label on "${step.name}": emitted node not found.`);
5452
5525
  node.display = { ...(node.display ?? {}), label };
5453
5526
  };
5454
- for (const step of steps) {
5527
+ for (const [stepIndex, step] of steps.entries()) {
5455
5528
  if (step.kind === 'action') {
5456
5529
  tails = emitAction(step, tails, scope, parentId);
5457
5530
  applyUpdates(step);
5458
5531
  applyLabel(step);
5459
5532
  lastAction = step.name;
5533
+ // A fan-out human task's `action: 'End'` outcomes end the run here: the
5534
+ // flow runtime never reads `action`, so only an End node can. When the
5535
+ // very next step is a `.return()` they join it instead — it is an End
5536
+ // already, and it writes the flow's return values on that path too.
5537
+ if (tails.some((t) => t.endsRun)) {
5538
+ // Side exits attach to the task; the step that CONTINUES it is the
5539
+ // first one after them (check's HITL_UNREACHABLE_AFTER reads the same).
5540
+ const joinsReturn = steps.slice(stepIndex + 1)
5541
+ .find((x) => x.kind !== 'stepToList' && x.kind !== 'stepToRef')?.kind === 'return';
5542
+ const continuing = [];
5543
+ for (const t of tails) {
5544
+ if (t.endsRun && !joinsReturn)
5545
+ endExit(scope, t.node, t.port, step.name, parentId);
5546
+ else
5547
+ continuing.push({ node: t.node, port: t.port });
5548
+ }
5549
+ if (!joinsReturn && continuing.length > 0) {
5550
+ hitlEndExits.push({ scope, parentId, step: step.name, why: "ends the run on an action: 'End' outcome" });
5551
+ }
5552
+ tails = continuing;
5553
+ }
5460
5554
  }
5461
5555
  else if (step.kind === 'stepToList') {
5462
5556
  tails = emitPortList(step, lastAction ?? '?', tails, scope, parentId, breakTarget);
@@ -5489,6 +5583,9 @@ export function serialize(built, opts = {}) {
5489
5583
  throw new Error(`.stepSwitch("${step.name}"): emitted node not found.`);
5490
5584
  const armTails = [];
5491
5585
  const routed = new Set();
5586
+ // Outcomes of a human task whose path stops at an End: an arm that ends
5587
+ // in `.return()` here, an outcome with no arm below.
5588
+ const stopped = [];
5492
5589
  for (const c of step.cases) {
5493
5590
  const exit = exits.find((e) => e.value === c.value);
5494
5591
  if (!exit) {
@@ -5501,7 +5598,12 @@ export function serialize(built, opts = {}) {
5501
5598
  // from all of them. (`.stepToList` instead gives each arm a private
5502
5599
  // End, which silently drops the flow's outputs on that path —
5503
5600
  // flow-builder-sdk#742.)
5504
- armTails.push(...emit(c.body, [{ node: nodeId, port: exit.port }], scope, parentId, breakTarget));
5601
+ const caseTails = emit(c.body, [{ node: nodeId, port: exit.port }], scope, parentId, breakTarget);
5602
+ if (caseTails.length === 0 && stopsAtEnd(c.body))
5603
+ stopped.push(String(c.value));
5604
+ armTails.push(...(hitlNodes.has(nodeId)
5605
+ ? caseTails.map((t) => (t.humanSwitch ? t : { ...t, humanSwitch: step.name }))
5606
+ : caseTails));
5505
5607
  }
5506
5608
  // A declared exit with no arm gets an End, so the run FINISHES there
5507
5609
  // rather than reaching a port with no edge and stalling. `check` warns
@@ -5516,13 +5618,20 @@ export function serialize(built, opts = {}) {
5516
5618
  // `End`. What the action changes is the WARNING, not the edge: nobody
5517
5619
  // needs to be told to write an arm for an outcome that ends the run.
5518
5620
  for (const e of exits) {
5519
- if (routed.has(e.port))
5520
- continue;
5521
- const endId = uid(scope, `${step.name}${e.port.charAt(0).toUpperCase()}${e.port.slice(1)}End`);
5522
- const end = makeNode(NODE_TYPE.end, endId, 'End', parentId ? { parentId } : {});
5523
- end.inputs = {};
5524
- scope.nodes.push(end);
5525
- addEdge(scope, { node: nodeId, port: e.port }, endId);
5621
+ if (!routed.has(e.port)) {
5622
+ endExit(scope, nodeId, e.port, step.name, parentId);
5623
+ stopped.push(String(e.value));
5624
+ }
5625
+ }
5626
+ // On a human task those Ends sit beside the arms that continue, and only
5627
+ // one outcome fires. Inside a `.parallel` arm the join then waits
5628
+ // forever for the continuing path, as for an `action: 'End'` outcome.
5629
+ if (hitlNodes.has(nodeId) && stopped.length > 0 && armTails.length > 0) {
5630
+ hitlEndExits.push({
5631
+ scope, parentId, step: step.name,
5632
+ why: `stops at an End on outcome${stopped.length === 1 ? '' : 's'} ${stopped.join(', ')} of its `
5633
+ + `.stepSwitch (no arm, or an arm that ends in .return())`,
5634
+ });
5526
5635
  }
5527
5636
  tails = armTails;
5528
5637
  lastAction = step.name;
@@ -5590,9 +5699,49 @@ export function serialize(built, opts = {}) {
5590
5699
  // tail and so contributes no incoming edge; that is rejected at rung 1
5591
5700
  // too (PARALLEL_ARM_RETURNS), since a BPMN join waits for every branch —
5592
5701
  // UNLESS no arm reaches the join at all, which is the next paragraph.
5702
+ // The join waits for a token on EVERY incoming edge, and a fan-out human
5703
+ // task's outcome edges are mutually exclusive — only one fires. So two
5704
+ // of them straight into the join, or an End outcome beside a continuing
5705
+ // one, hang the run with no incident. check reports both
5706
+ // (HITL_FAN_OUT_INTO_JOIN, HITL_END_OUTCOME_IN_PARALLEL); this is the
5707
+ // compile-side half, and it fails closed.
5593
5708
  const armTails = [];
5594
- for (const arm of step.arms)
5595
- armTails.push(...emit(arm, tails, scope, parentId, breakTarget));
5709
+ const endsBefore = hitlEndExits.length;
5710
+ for (const arm of step.arms) {
5711
+ const t = emit(arm, tails, scope, parentId, breakTarget);
5712
+ // An arm hands the join ONE edge, or several that are alternatives —
5713
+ // `.branch` / `.switch` / `.stepSwitch` arms, a fan-out task's
5714
+ // outcomes — of which only one fires. Refused when any of them is a
5715
+ // human task's outcome edge: however the task got there (at the arm's
5716
+ // end, one empty `.stepSwitch` arm beside a body, one side of a
5717
+ // `.branch`), the join waits for all of them. Rejoining arms with no
5718
+ // human task in them are the older, general case (still emitted).
5719
+ const outcomeTail = t.find((x) => hitlNodes.has(x.node) && x.port.startsWith('outcome-'));
5720
+ // The same holds when each outcome's arm ended on a step of its own:
5721
+ // a complete `.stepSwitch` with nothing after it hands the join one
5722
+ // edge per arm, and still only one arm runs.
5723
+ const switchTail = outcomeTail ? undefined : t.find((x) => x.humanSwitch !== undefined);
5724
+ if (t.length >= 2 && switchTail) {
5725
+ throw new Error(`.parallel("${step.name}"): an arm sends ${t.length} alternative edges into join "${step.name}" — `
5726
+ + `among them the end of an arm of human task "${switchTail.humanSwitch}"'s .stepSwitch — and only one `
5727
+ + `of them fires, while the join waits for all of them. Add a step at the end of the arm, after the `
5728
+ + `.stepSwitch, so its arms meet there first, or move the task out of the .parallel.`);
5729
+ }
5730
+ if (t.length >= 2 && outcomeTail) {
5731
+ throw new Error(`.parallel("${step.name}"): an arm sends ${t.length} alternative edges into join "${step.name}" — `
5732
+ + `among them outcome edge "${outcomeTail.port}" of human task "${outcomeTail.node}" — and only one `
5733
+ + `of them fires, while the join waits for all of them. Add a step at the end of the arm so its paths `
5734
+ + `meet there first, or move the task out of the .parallel.`);
5735
+ }
5736
+ armTails.push(...t);
5737
+ }
5738
+ const starved = hitlEndExits.slice(endsBefore).find((x) => x.scope === scope && x.parentId === parentId);
5739
+ if (starved && armTails.length > 0) {
5740
+ throw new Error(`.parallel("${step.name}"): human task "${starved.step}" ${starved.why} inside an arm, and join `
5741
+ + `"${step.name}" would wait forever for the path that continues. Give every outcome a path that `
5742
+ + `continues to the end of the arm (drop action: 'End', add the missing .stepSwitch arm, move the `
5743
+ + `.return() after the .parallel), or move the task out of the .parallel.`);
5744
+ }
5596
5745
  // When EVERY arm is terminal — a terminate, or an End — there is nothing
5597
5746
  // left to join, and emitting a Merge would add a node with no incoming
5598
5747
  // edge, which is unreachable. The platform's own flows agree:
@@ -5714,6 +5863,7 @@ export function serialize(built, opts = {}) {
5714
5863
  const from = tails[0];
5715
5864
  if (!from)
5716
5865
  throw new Error(`.stepToRef('${step.port}', '${step.target}') has no step to leave from.`);
5866
+ refuseRewire('.stepToRef', step.port, lastAction ?? '?', tails);
5717
5867
  if (step.port === 'error') {
5718
5868
  const node = scope.nodes.find((n) => n.id === from.node);
5719
5869
  if (node)
@@ -5866,6 +6016,7 @@ export function serialize(built, opts = {}) {
5866
6016
  }
5867
6017
  }
5868
6018
  resolveRejoins(root);
6019
+ assertHitlExitInvariant(root.nodes, root.edges);
5869
6020
  typeAgentInputSchemas(root, globals);
5870
6021
  emitStickyNotes(built, root);
5871
6022
  const rootLayoutEdges = applyAuthorLayout(built.layout, root, `flow "${built.id}"`);
@@ -5984,16 +6135,35 @@ function buildConnectorInputs(rc, inputs, rename = NO_RENAME, resolutions = {},
5984
6135
  // NOT rendered as a placeholder: emitting something for a value nobody
5985
6136
  // resolved would produce a flow that compiles and then talks to the wrong
5986
6137
  // record, which is exactly the failure the lookup surface exists to remove.
5987
- if (isLookupToken(nested.value)) {
5988
- const resolved = resolvedValue(nested.value, resolutions);
5989
- if (resolved === undefined) {
5990
- if (policy.unresolvedLookups !== 'ignore')
5991
- errs.push(unresolvedLookupMessage(nested.value));
6138
+ //
6139
+ // A token is swapped wherever the field's value can hold one: the whole
6140
+ // value, or an element of an array at any depth. A multiselect is
6141
+ // `users: [lookup(…), lookup(…)]` — the form `ConnectorLookupValue`'s array
6142
+ // branch types and the generated `## Example` prints — and until this swapped
6143
+ // only the whole-value case, so every token inside the array reached
6144
+ // validation as an object ("expected string, got object") with every lookup
6145
+ // recorded. An unresolved element under 'ignore' becomes `null`, which
6146
+ // validation skips, so `check` still type-checks the resolved siblings.
6147
+ const unresolved = [];
6148
+ const swapped = swapLookupTokens(nested.value, resolutions, unresolved);
6149
+ if (unresolved.length) {
6150
+ if (policy.unresolvedLookups !== 'ignore') {
6151
+ for (const token of unresolved)
6152
+ errs.push(unresolvedLookupMessage(token));
5992
6153
  continue;
5993
6154
  }
5994
- nested = { ...nested, value: resolved };
6155
+ if (isLookupToken(nested.value))
6156
+ continue;
5995
6157
  }
5996
- validateConnectorFieldValue(f, nested.value, flat ? 0 : nested.projectedArrays, errs);
6158
+ nested = { ...nested, value: swapped };
6159
+ // A FLAT key whose wire name ends in `[*]` names one array: an array value
6160
+ // there is checked element by element, as the nested form is. A flat scalar
6161
+ // keeps depth 0 and is emitted as the scalar it is — the product wraps it
6162
+ // into a one-element list (flow-v1 `convertDotNotationToNested`).
6163
+ const depth = flat
6164
+ ? (f.name.endsWith('[*]') && Array.isArray(nested.value) ? 1 : 0)
6165
+ : nested.projectedArrays;
6166
+ validateConnectorFieldValue(f, nested.value, depth, errs);
5997
6167
  if (f.location === 'multipart') {
5998
6168
  partValues.set(f.name, renderDeep(nested.value, rename));
5999
6169
  continue;
@@ -6061,6 +6231,29 @@ function buildConnectorInputs(rc, inputs, rename = NO_RENAME, resolutions = {},
6061
6231
  }
6062
6232
  return slots;
6063
6233
  }
6234
+ /**
6235
+ * `value` with every lookup token swapped for its recorded id: the value itself,
6236
+ * or the elements of an array at any depth. An unrecorded token is pushed to
6237
+ * `unresolved` and stands as `null`, so an array keeps its length.
6238
+ *
6239
+ * Objects are NOT descended: a lookup's place is a field's value or an element
6240
+ * of an array field (`ConnectorLookupValue`), and a nested path such as
6241
+ * `contacts[*].id` reaches here already projected to its leaf array. A token
6242
+ * anywhere else is refused by {@link renderDeep}.
6243
+ */
6244
+ function swapLookupTokens(value, resolutions, unresolved) {
6245
+ if (isLookupToken(value)) {
6246
+ const resolved = resolvedValue(value, resolutions);
6247
+ if (resolved === undefined) {
6248
+ unresolved.push(value);
6249
+ return null;
6250
+ }
6251
+ return resolved;
6252
+ }
6253
+ if (Array.isArray(value))
6254
+ return value.map((item) => swapLookupTokens(item, resolutions, unresolved));
6255
+ return value;
6256
+ }
6064
6257
  /**
6065
6258
  * `inputMetadata.multipart.bodyFieldName` names the CONTAINER part — the one the
6066
6259
  * runtime composes from `bodyParameters`. Measured on the designer's own nodes in
@@ -100,6 +100,15 @@
100
100
  "objectName": "create_issue",
101
101
  "path": "uipath-atlassian-jira/generic/create_issue/create@1.0.0.json"
102
102
  },
103
+ {
104
+ "nodeType": "uipath.connector.uipath-getresponse-getresponse.send-newsletter",
105
+ "version": "1.0.0",
106
+ "connectorKey": "uipath-getresponse-getresponse",
107
+ "label": "Send Newsletter",
108
+ "activityType": "Curated",
109
+ "objectName": "NewslettersSend-draft",
110
+ "path": "uipath-getresponse-getresponse/send-newsletter@1.0.0.json"
111
+ },
103
112
  {
104
113
  "nodeType": "uipath.connector.uipath-google-vertex.execute-google-vertex-agent",
105
114
  "version": "1.0.0",
@@ -136,6 +145,15 @@
136
145
  "objectName": "triggerFlow",
137
146
  "path": "uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.json"
138
147
  },
148
+ {
149
+ "nodeType": "uipath.connector.uipath-salesforce-slack.create-group-direct-message",
150
+ "version": "1.0.0",
151
+ "connectorKey": "uipath-salesforce-slack",
152
+ "label": "Create Group Direct Message",
153
+ "activityType": "Curated",
154
+ "objectName": "conversations_open",
155
+ "path": "uipath-salesforce-slack/create-group-direct-message@1.0.0.json"
156
+ },
139
157
  {
140
158
  "nodeType": "uipath.connector.uipath-salesforce-slack.send-message-to-user",
141
159
  "version": "1.0.0",