@uipath/maestro-builder-sdk 6.18.10 → 6.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/api-index.md +185 -184
  2. package/dist/api-members.md +506 -473
  3. package/dist/check.js +358 -695
  4. package/dist/cli/commands/flow.js +3 -0
  5. package/dist/compile-cli.js +51 -0
  6. package/dist/core/actions.d.ts +132 -765
  7. package/dist/core/actions.js +1 -307
  8. package/dist/core/binding-identity.d.ts +22 -0
  9. package/dist/core/binding-identity.js +28 -0
  10. package/dist/core/context-index.d.ts +34 -0
  11. package/dist/core/context-index.js +38 -0
  12. package/dist/core/definitionless-node-types.d.ts +16 -0
  13. package/dist/core/definitionless-node-types.js +16 -0
  14. package/dist/core/error-port.d.ts +6 -0
  15. package/dist/core/error-port.js +9 -0
  16. package/dist/core/form-field-type.d.ts +33 -0
  17. package/dist/core/form-field-type.js +30 -0
  18. package/dist/core/hitl-routing.d.ts +8 -0
  19. package/dist/core/hitl-routing.js +17 -1
  20. package/dist/core/node-classes.d.ts +4 -63
  21. package/dist/core/node-classes.js +0 -112
  22. package/dist/core/nodes/family.d.ts +36 -0
  23. package/dist/core/nodes/family.js +1 -0
  24. package/dist/core/nodes/node-type.d.ts +136 -0
  25. package/dist/core/nodes/node-type.js +1 -0
  26. package/dist/core/nodes/registry.d.ts +57 -0
  27. package/dist/core/nodes/registry.js +122 -0
  28. package/dist/core/nodes/versioned.d.ts +50 -0
  29. package/dist/core/nodes/versioned.js +46 -0
  30. package/dist/core/process-resource-sort-order.d.ts +40 -0
  31. package/dist/core/process-resource-sort-order.js +44 -0
  32. package/dist/core/typed-raw-node.d.ts +13 -62
  33. package/dist/core/typed-raw-node.js +40 -154
  34. package/dist/core/wire-expr.d.ts +27 -0
  35. package/dist/core/wire-expr.js +34 -0
  36. package/dist/decompile-cli.js +11 -1
  37. package/dist/decompile.d.ts +8 -3
  38. package/dist/decompile.js +665 -354
  39. package/dist/flow-definitions.d.ts +10 -0
  40. package/dist/flow-definitions.js +35 -0
  41. package/dist/flow-expr-check.js +1 -1
  42. package/dist/flow-node-registry.d.ts +13 -0
  43. package/dist/flow-node-registry.js +19 -0
  44. package/dist/flow-node-type.d.ts +192 -0
  45. package/dist/flow-node-type.js +1 -0
  46. package/dist/flow-sdk.d.ts +40 -1
  47. package/dist/flow-sdk.js +29 -2
  48. package/dist/flow-step-tree.js +2 -1
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.js +1 -0
  51. package/dist/merge-cli.js +25 -8
  52. package/dist/merge-roundtrip.d.ts +81 -0
  53. package/dist/merge-roundtrip.js +404 -0
  54. package/dist/merge.d.ts +35 -6
  55. package/dist/merge.js +124 -24
  56. package/dist/node-placement.d.ts +93 -0
  57. package/dist/node-placement.js +163 -0
  58. package/dist/nodes/data-fabric/flow/check.d.ts +13 -0
  59. package/dist/nodes/data-fabric/flow/check.js +164 -0
  60. package/dist/nodes/data-fabric/flow/factories.d.ts +113 -0
  61. package/dist/nodes/data-fabric/flow/factories.js +134 -0
  62. package/dist/nodes/data-fabric/flow/family.d.ts +7 -0
  63. package/dist/nodes/data-fabric/flow/family.js +9 -0
  64. package/dist/nodes/data-fabric/flow/index.d.ts +8 -0
  65. package/dist/nodes/data-fabric/flow/index.js +8 -0
  66. package/dist/nodes/data-fabric/flow/raw.d.ts +32 -0
  67. package/dist/nodes/data-fabric/flow/raw.js +100 -0
  68. package/dist/nodes/data-fabric/flow/shared.d.ts +77 -0
  69. package/dist/nodes/data-fabric/flow/shared.js +174 -0
  70. package/dist/nodes/data-fabric/flow/spec.d.ts +238 -0
  71. package/dist/nodes/data-fabric/flow/spec.js +1 -0
  72. package/dist/nodes/data-fabric/flow/verbs.d.ts +29 -0
  73. package/dist/nodes/data-fabric/flow/verbs.js +239 -0
  74. package/dist/nodes/delay/flow/check.d.ts +16 -0
  75. package/dist/nodes/delay/flow/check.js +86 -0
  76. package/dist/nodes/delay/flow/factories.d.ts +28 -0
  77. package/dist/nodes/delay/flow/factories.js +38 -0
  78. package/dist/nodes/delay/flow/family.d.ts +3 -0
  79. package/dist/nodes/delay/flow/family.js +8 -0
  80. package/dist/nodes/delay/flow/index.d.ts +3 -0
  81. package/dist/nodes/delay/flow/index.js +3 -0
  82. package/dist/nodes/delay/flow/node-type.d.ts +13 -0
  83. package/dist/nodes/delay/flow/node-type.js +71 -0
  84. package/dist/nodes/delay/flow/spec.d.ts +43 -0
  85. package/dist/nodes/delay/flow/spec.js +16 -0
  86. package/dist/nodes/document/flow/factories.d.ts +27 -0
  87. package/dist/nodes/document/flow/factories.js +30 -0
  88. package/dist/nodes/document/flow/family.d.ts +3 -0
  89. package/dist/nodes/document/flow/family.js +7 -0
  90. package/dist/nodes/document/flow/index.d.ts +3 -0
  91. package/dist/nodes/document/flow/index.js +3 -0
  92. package/dist/nodes/document/flow/node-type.d.ts +14 -0
  93. package/dist/nodes/document/flow/node-type.js +150 -0
  94. package/dist/nodes/document/flow/spec.d.ts +65 -0
  95. package/dist/nodes/document/flow/spec.js +1 -0
  96. package/dist/nodes/mock/flow/factories.d.ts +51 -0
  97. package/dist/nodes/mock/flow/factories.js +47 -0
  98. package/dist/nodes/mock/flow/family.d.ts +3 -0
  99. package/dist/nodes/mock/flow/family.js +7 -0
  100. package/dist/nodes/mock/flow/index.d.ts +2 -0
  101. package/dist/nodes/mock/flow/index.js +2 -0
  102. package/dist/nodes/mock/flow/node-type.d.ts +11 -0
  103. package/dist/nodes/mock/flow/node-type.js +51 -0
  104. package/dist/nodes/script/flow/check.d.ts +19 -0
  105. package/dist/nodes/script/flow/check.js +49 -0
  106. package/dist/nodes/script/flow/factories.d.ts +14 -0
  107. package/dist/nodes/script/flow/factories.js +15 -0
  108. package/dist/nodes/script/flow/family.d.ts +3 -0
  109. package/dist/nodes/script/flow/family.js +8 -0
  110. package/dist/nodes/script/flow/index.d.ts +3 -0
  111. package/dist/nodes/script/flow/index.js +3 -0
  112. package/dist/nodes/script/flow/node-type.d.ts +7 -0
  113. package/dist/nodes/script/flow/node-type.js +125 -0
  114. package/dist/nodes/script/flow/spec.d.ts +52 -0
  115. package/dist/nodes/script/flow/spec.js +1 -0
  116. package/dist/nodes/transform/flow/check.d.ts +6 -0
  117. package/dist/nodes/transform/flow/check.js +229 -0
  118. package/dist/nodes/transform/flow/factories.d.ts +52 -0
  119. package/dist/nodes/transform/flow/factories.js +53 -0
  120. package/dist/nodes/transform/flow/family.d.ts +3 -0
  121. package/dist/nodes/transform/flow/family.js +8 -0
  122. package/dist/nodes/transform/flow/index.d.ts +3 -0
  123. package/dist/nodes/transform/flow/index.js +3 -0
  124. package/dist/nodes/transform/flow/node-type.d.ts +7 -0
  125. package/dist/nodes/transform/flow/node-type.js +245 -0
  126. package/dist/nodes/transform/flow/spec.d.ts +116 -0
  127. package/dist/nodes/transform/flow/spec.js +10 -0
  128. package/dist/raw-node-advice.d.ts +16 -0
  129. package/dist/raw-node-advice.js +36 -0
  130. package/dist/raw-node.d.ts +71 -0
  131. package/dist/raw-node.js +116 -0
  132. package/dist/registry/uip/pipeline-front-door.js +5 -3
  133. package/dist/serialize.js +246 -549
  134. package/package.json +2 -2
package/dist/serialize.js CHANGED
@@ -10,18 +10,24 @@ import { readFileSync } from 'node:fs';
10
10
  import { createHash } from 'node:crypto';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { dirname, join } from 'node:path';
13
- import { Expr, toExpr, SCHEDULE_PRESETS, DELAY_PRESETS, parseIxpProjectType, flatNameOf, flatNameSegmentsOf } from './flow-sdk.js';
13
+ import { BUNDLED_DEFINITIONS, bundledVersions } from './flow-definitions.js';
14
+ import { Expr, toExpr, SCHEDULE_PRESETS, parseIxpProjectType, flatNameOf, flatNameSegmentsOf } from './flow-sdk.js';
14
15
  import { FlowAction, FlowTrigger } from './core/node-classes.js';
16
+ import { contextIndexType } from './core/context-index.js';
15
17
  import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
16
- import { dataFabricUnknownInputs } from './core/typed-raw-node.js';
18
+ import { rawNodeInputProblem, registeredRawType } from './raw-node-advice.js';
17
19
  import { planContributionBindings } from './core/contribution-bindings.js';
18
- import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
20
+ import { declaredErrorPortRefusal, definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
21
+ import { FLOW_NODE_TYPES } from './flow-node-registry.js';
22
+ import { resolveVersion } from './core/nodes/versioned.js';
19
23
  import { isAnswerField } from './core/hitl-answers.js';
20
24
  import { connectionConnectors } from './bindings-v2.js';
21
25
  import { BindingRefusal, GUID_RE, bindingLabelRefusal, bindingMissingMessage, missingBindingLabels } from './core/binding-messages.js';
22
26
  import { ByoaRefusal, byoaUnverified } from './core/byoa.js';
23
27
  import { genericEventObjectRefusal } from './core/event-checks.js';
24
- import { OUTCOME_HANDLE_NODE_TYPES, assertHitlExitInvariant, hitlExitModel, hitlExitViolation, outcomeSlug } from './core/hitl-routing.js';
28
+ import { OUTCOME_HANDLE_NODE_TYPES, assertHitlExitInvariant, hitlExitModel, hitlExitViolation, outcomeIdOf, outcomeSlug } from './core/hitl-routing.js';
29
+ import { STICKY_NOTE_TYPE, STICKY_NOTE_TYPE_VERSION } from './core/definitionless-node-types.js';
30
+ import { placeholderPosition } from './node-placement.js';
25
31
  import { declaredExits, hasExits } from './core/step-ports.js';
26
32
  import { stopsAtEnd } from './flow-step-tree.js';
27
33
  import { assertNodeEnvelope } from './core/node-envelope.js';
@@ -45,7 +51,6 @@ export function connectorInputErrors(rc, inputs, resolutions = {}, lookups = {})
45
51
  });
46
52
  }
47
53
  import { isLookupToken, resolveEventWhere, } from './core/lookups.js';
48
- import { declaredScriptShape, inferScriptShape } from './core/script-return.js';
49
54
  import { migrateToSchemaVersion } from './migrate.js';
50
55
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
51
56
  import { resolveSchemaVersion } from './schema-version.js';
@@ -53,12 +58,13 @@ import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-vari
53
58
  import { prepareCommand } from './core/cli-spelling.js';
54
59
  import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
55
60
  import { AGENTIC_PROCESS_FORMS } from './core/agentic-process-forms.js';
61
+ import { resourceNodeSortOrder } from './core/process-resource-sort-order.js';
56
62
  import { types } from './core/expr.js';
57
63
  import { ownEntry } from './core/own-entry.js';
58
64
  import { jsonClone } from './core/json-file.js';
59
65
  const HERE = dirname(fileURLToPath(import.meta.url));
60
66
  // core-definitions.json is an object keyed by "<nodeType>@<version>".
61
- const DEFS = JSON.parse(readFileSync(join(HERE, 'core-definitions.json'), 'utf8'));
67
+ const DEFS = BUNDLED_DEFINITIONS;
62
68
  // FIRST occurrence wins: with multiple bundled versions of one family (HITL
63
69
  // 1.0 / 1.1 / 1.2), the unversioned lookup must stay the pinned default, not
64
70
  // whichever entry happens to be last in the JSON. (`.loop()` always passes its
@@ -91,8 +97,6 @@ const NODE_TYPE = {
91
97
  endCall: 'uipath.conversational.voice.end-call',
92
98
  http: 'core.action.http',
93
99
  httpV2: 'core.action.http.v2',
94
- script: 'core.action.script',
95
- transform: 'core.action.transform',
96
100
  hitl: 'uipath.human-in-the-loop',
97
101
  // The two other spellings of the SAME human task, both resolvable on this
98
102
  // tenant. `hitl()` emits the base type unless `variant` names one of these;
@@ -103,23 +107,15 @@ const NODE_TYPE = {
103
107
  hitlQuickForm: 'uipath.human-in-the-loop.quick-form',
104
108
  hitlActionApp: 'uipath.human-in-the-loop.coded-action-app',
105
109
  hitlDocValidation: 'uipath.human-in-the-loop.document-validation',
106
- delay: 'core.logic.delay',
107
110
  // The PLATFORM's placeholder — "a real node goes here later". Not the engine's
108
111
  // fixture-returning `kind: 'mock'` test construct, which shares only the name
109
112
  // (#125). The definition declares no `inputDefinition` and one output whose
110
113
  // `source` is the literal `"null"`.
111
- mock: 'core.logic.mock',
112
114
  decision: 'core.logic.decision',
113
115
  switch: 'core.logic.switch',
114
116
  merge: 'core.logic.merge',
115
117
  loop: 'core.logic.loop',
116
118
  doWhile: 'core.logic.dowhile',
117
- dataFabricRead: 'core.datafabric.read',
118
- dataFabricCreate: 'core.datafabric.create',
119
- dataFabricUpdate: 'core.datafabric.update',
120
- dataFabricDelete: 'core.datafabric.delete',
121
- documentClassify: 'uipath.document.classify',
122
- dynamicExtract: 'uipath.ixp.extract-document-builder',
123
119
  subflow: 'core.subflow',
124
120
  terminate: 'core.logic.terminate',
125
121
  queueCreate: 'core.action.queue.create',
@@ -214,6 +210,9 @@ function processResourceLocation(spec) {
214
210
  * `icon: 'rpa'`, the `bpmn:ServiceTask` / `Orchestrator.StartJob` / `v2` model,
215
211
  * `section: 'Published'` (42/45), `debug.runtime: 'bpmnEngine'`, and the
216
212
  * `<bindings.name>` / `<bindings.folderPath>` context placeholders.
213
+ * `sortOrder` is not one of those constants any more: flow-core derives it from
214
+ * the category, and renumbered after 07-29, so it comes from
215
+ * `resourceNodeSortOrder` (#1045).
217
216
  *
218
217
  * Two fields are derived from the author's call rather than from the tenant, and
219
218
  * that limit is real:
@@ -244,7 +243,7 @@ function rpaWorkflowDef(spec) {
244
243
  runtimeConstraints: { exclude: ['api-function'] },
245
244
  description: loc.description,
246
245
  tags: loc.tags,
247
- sortOrder: 530,
246
+ sortOrder: resourceNodeSortOrder('rpa-workflow'),
248
247
  supportsErrorHandling: true,
249
248
  display: { label: spec.name, icon: 'rpa' },
250
249
  handleConfiguration: [
@@ -324,7 +323,7 @@ const API_WORKFLOW_PREFIX = 'uipath.core.api-workflow.';
324
323
  * AGREE on, not invented: 42 `uipath.core.api-workflow.*` instances across 42
325
324
  * dated `skill-flow-api-workflow` artifacts (2026-05-18 → 07-29), cross-checked
326
325
  * against `registry get` for today's workflow. Every one is unanimous —
327
- * `category: 'api-workflow'`, `sortOrder: 510`, `supportsErrorHandling`,
326
+ * `category: 'api-workflow'`, `sortOrder: 510` (now 640, see `resourceNodeSortOrder`, #1045), `supportsErrorHandling`,
328
327
  * `icon: 'api'`, the `bpmn:ServiceTask` / `Orchestrator.ExecuteApiWorkflowAsync` /
329
328
  * `v2` model, `section: 'Published'` (37/42; the 5 without are the in-solution
330
329
  * shape, a bare-GUID resourceKey — rpa's 3/45 split again), `resourceSubType:
@@ -362,7 +361,7 @@ function apiWorkflowDef(spec) {
362
361
  runtimeConstraints: { exclude: ['api-function'] },
363
362
  description: `(${spec.folderPath})`,
364
363
  tags: [],
365
- sortOrder: 510,
364
+ sortOrder: resourceNodeSortOrder('api-workflow'),
366
365
  supportsErrorHandling: true,
367
366
  display: { label: spec.name, icon: 'api' },
368
367
  handleConfiguration: [
@@ -443,7 +442,7 @@ function publishedFunctionDef(spec) {
443
442
  category: 'function',
444
443
  description: `(${spec.folderPath})`,
445
444
  tags: [],
446
- sortOrder: 512,
445
+ sortOrder: resourceNodeSortOrder('function'),
447
446
  supportsErrorHandling: true,
448
447
  display: { label: spec.name, icon: 'function' },
449
448
  handleConfiguration: [
@@ -521,7 +520,8 @@ function publishedFunctionDef(spec) {
521
520
  *
522
521
  * - `uip maestro flow registry get uipath.core.agentic-process.<key>` for ALL
523
522
  * THREE agentic processes on the tenant (2026-07-29): `category:
524
- * 'agentic-process'`, `sortOrder: 507`, `supportsErrorHandling`, `icon:
523
+ * 'agentic-process'`, `sortOrder: 507` (now 610 for bpmn, 600 flow, 620 case:
524
+ * `resourceNodeSortOrder`, #1045), `supportsErrorHandling`, `icon:
525
525
  * 'agentic-process'`, `version: '1.0.0'`, the `bpmn:ServiceTask` /
526
526
  * `Orchestrator.StartAgenticProcess` / `v2` model, `section: 'Published'`,
527
527
  * `resourceSubType: 'ProcessOrchestration'`, `orchestratorType:
@@ -564,7 +564,7 @@ function agenticProcessDef(spec) {
564
564
  runtimeConstraints: { exclude: ['api-function'] },
565
565
  description: loc.description,
566
566
  tags: loc.tags,
567
- sortOrder: 507,
567
+ sortOrder: resourceNodeSortOrder(form.category),
568
568
  supportsErrorHandling: true,
569
569
  display: { label: spec.name, icon: form.icon },
570
570
  handleConfiguration: [
@@ -645,7 +645,8 @@ const INLINE_AGENT_TYPE = 'uipath.agent.autonomous';
645
645
  * checkable rather than asserted:
646
646
  *
647
647
  * - `uip maestro flow registry get uipath.core.agent.<key>` for ALL NINE agents on
648
- * the tenant (2026-07-29): `category: 'agent'`, `sortOrder: 505`,
648
+ * the tenant (2026-07-29): `category: 'agent'`, `sortOrder: 505` (510 since flow-core's
649
+ * renumber: the 54 runs from 09-01 below carry 510; `resourceNodeSortOrder`, #1045),
649
650
  * `supportsErrorHandling: true`, `version: '1.0.0'`, the `bpmn:ServiceTask` /
650
651
  * `Orchestrator.StartAgentJob` / `v2` model, `section: 'Published'`,
651
652
  * `resourceSubType: 'Agent'`, `orchestratorType: 'agent'`,
@@ -699,7 +700,7 @@ function agentDef(spec) {
699
700
  runtimeConstraints: { exclude: ['api-function'] },
700
701
  description: loc.description,
701
702
  tags: loc.tags,
702
- sortOrder: 505,
703
+ sortOrder: resourceNodeSortOrder('agent'),
703
704
  supportsErrorHandling: true,
704
705
  display: {
705
706
  label: spec.name,
@@ -844,21 +845,6 @@ function contextIndexDef(spec) {
844
845
  },
845
846
  };
846
847
  }
847
- /**
848
- * The node type for a context index: `…context.index.<slug>.<uuid>`.
849
- *
850
- * The slug is the index's display name lower-cased with every run of
851
- * non-alphanumerics collapsed to a single `-`. Measured against the platform's own
852
- * type for "Billing Dispute SOP Index" → `billing-dispute-sop-index`, and pinned
853
- * by a test — because a derived identifier that disagrees with the registry's is a
854
- * node type the tenant has never heard of.
855
- */
856
- function contextIndexSlug(name) {
857
- return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
858
- }
859
- function contextIndexType(spec) {
860
- return `uipath.agent.resource.context.index.${contextIndexSlug(spec.name)}.${spec.id}`;
861
- }
862
848
  /**
863
849
  * Render an index's retrieval query into the platform's MODE-TAGGED object.
864
850
  *
@@ -2966,9 +2952,14 @@ function connectionResourceBindings(spec, existing) {
2966
2952
  * create, or the author's `returns` for a create-and-wait. Without a schema a
2967
2953
  * downstream read is unresolvable.
2968
2954
  */
2969
- function queueItemDef(spec, stepName, ids) {
2955
+ function queueItemDef(spec, stepName, ids, version) {
2970
2956
  const nodeType = spec.wait ? NODE_TYPE.queueCreateAndWait : NODE_TYPE.queueCreate;
2971
- const base = defByType.get(nodeType);
2957
+ // The template at the AUTHOR'S version, when the bundle has it. Starting from
2958
+ // the default and handing it to makeNode with the pin refused every other
2959
+ // bundled version — the bug #988 fixed for inline agents (#1128). An
2960
+ // unbundled pin falls back to the default, and makeNode's exact-version
2961
+ // refusal names what is bundled.
2962
+ const base = (version !== undefined ? DEFS[`${nodeType}@${version}`] : undefined) ?? defByType.get(nodeType);
2972
2963
  if (!base)
2973
2964
  throw new Error(`no bundled definition for "${nodeType}"`);
2974
2965
  const def = jsonClone(base);
@@ -3048,32 +3039,6 @@ function queueItemBindings(spec, existing) {
3048
3039
  ],
3049
3040
  };
3050
3041
  }
3051
- /**
3052
- * The three single-purpose Transform node types. Same runtime as the generic
3053
- * (the platform dispatches on the OPERATION's type, not the node's) — the node
3054
- * type only says what the node is for, and picks its editor in the designer.
3055
- */
3056
- const TRANSFORM_VARIANT_TYPE = {
3057
- map: 'core.action.transform.map',
3058
- filter: 'core.action.transform.filter',
3059
- 'group-by': 'core.action.transform.group-by',
3060
- };
3061
- /**
3062
- * Pick the node type for a transform step: the generic unless a variant is named.
3063
- *
3064
- * An unrecognized variant is rejected by `check` (TRANSFORM_VARIANT_UNKNOWN)
3065
- * before serialization, and compile-cli refuses to emit on errors. Reaching
3066
- * here with one means compile ran without a check — so emit the type the author
3067
- * asked for, keeping the failure loud downstream rather than quietly falling
3068
- * back to the generic (which would produce a flow that runs and is the wrong
3069
- * node) — the same principle as `collectionPath`.
3070
- */
3071
- function transformNodeType(inputs) {
3072
- const variant = inputs?.variant;
3073
- if (!variant)
3074
- return NODE_TYPE.transform;
3075
- return ownEntry(TRANSFORM_VARIANT_TYPE, variant) ?? `core.action.transform.${String(variant)}`;
3076
- }
3077
3042
  /**
3078
3043
  * Encode a scheduled trigger's single `every` interval into the three inputs the
3079
3044
  * platform's node carries.
@@ -3115,93 +3080,6 @@ function scheduledVersion(inputs, explicit) {
3115
3080
  }
3116
3081
  return cron ? '1.2' : undefined;
3117
3082
  }
3118
- /**
3119
- * Lower filter rows the way the platform's own serializer reads them.
3120
- *
3121
- * `_filters` is an ARRAY, and the join is a `join: 'OR'` key on the row that
3122
- * STARTS a new alternative — `entity-vdo.ts`'s `buildQesFromEntityConfig`
3123
- * splits the array into runs on that key, ANDs within a run, and ORs the runs
3124
- * together. So the query model is disjunctive: `alt1 OR alt2 OR …`, each `alt`
3125
- * an AND of its rows, which is why `[A, B(or), C]` is `A OR (B AND C)` and not
3126
- * `(A OR B) AND C`.
3127
- *
3128
- * The grouped `{ logicalOperator, rows, groups }` object the authoring guide
3129
- * shows is a NEWER shape: the vendored converter the local engine runs throws
3130
- * `(filters ?? []).filter is not a function` on it, so emitting it would trade
3131
- * a working offline rung for a shape only the current platform can read. The
3132
- * array is the form both accept.
3133
- */
3134
- function dataFabricFilterRows(filters, render) {
3135
- return filters.map((f, i) => ({
3136
- field: f.field,
3137
- operator: f.operator ?? '=',
3138
- value: render(f.value),
3139
- // Never on the first row: a leading `join` starts an empty run, which the
3140
- // platform's own loop guards against, and which means nothing anyway.
3141
- ...(i > 0 && f.or ? { join: 'OR' } : {}),
3142
- }));
3143
- }
3144
- /**
3145
- * The two `bindings[]` rows a FOLDER-SCOPED entity needs to survive export to
3146
- * another org, and the `_resourceKey` that ties them to the node.
3147
- *
3148
- * Nothing else in this file emits `resource: 'Entity'`: the kind is matched by
3149
- * `(resourceKey, propertyAttribute)` rather than by name, carries no
3150
- * `resourceSubType`, spells the folder attribute `folderKey` (never
3151
- * `folderPath` — a folderPath row receives the Orchestrator FQN, which the
3152
- * Data Fabric query rejects even in the source org), and puts a DISPLAY label
3153
- * in `name`. The capital `E` is load-bearing: packaging ignores a lowercase
3154
- * row, so deploy emits no override at all.
3155
- *
3156
- * A tenant-scoped entity needs none of this — it serializes as a dotted
3157
- * literal — which is why the pair is emitted only when the author supplied
3158
- * both `folderKey` and `resourceKey`. `check` refuses the half-authored case,
3159
- * because `_folderKey` with no binding falls back to a source-org literal that
3160
- * looks portable and breaks on deploy.
3161
- */
3162
- function entityBindings(entity, folderKey, resourceKey) {
3163
- const base = `b${entity.replace(/[^A-Za-z0-9]/g, '')}Entity`;
3164
- return [
3165
- {
3166
- id: `${base}Name`,
3167
- name: entity,
3168
- type: 'string',
3169
- resource: 'Entity',
3170
- resourceKey,
3171
- default: entity,
3172
- propertyAttribute: 'name',
3173
- },
3174
- {
3175
- id: `${base}Folder`,
3176
- name: `${entity}Folder`,
3177
- type: 'string',
3178
- resource: 'Entity',
3179
- resourceKey,
3180
- default: folderKey,
3181
- propertyAttribute: 'folderKey',
3182
- },
3183
- ];
3184
- }
3185
- /**
3186
- * Which `core.datafabric.read` definition a step needs.
3187
- *
3188
- * The family's read node grew from "Read entity" into "Query entity records" at
3189
- * 1.4, and `entityConfig.resultMode` is the input that expresses the difference.
3190
- * The SDK keeps 1.0 pinned as the default, the same way every other family here
3191
- * keeps its older default — a flow that reads one record must not change
3192
- * version because the platform moved. So `resultMode` SELECTS 1.4, exactly as a
3193
- * Quartz `every` selects the scheduled trigger's 1.2 (see `scheduledVersion`).
3194
- */
3195
- function dataFabricReadVersion(inputs, explicit) {
3196
- if (explicit !== undefined) {
3197
- if (inputs?.resultMode !== undefined && explicit === '1.0') {
3198
- throw new Error("dataFabricRead(): { resultMode } needs the node's 1.4 definition, but { version: '1.0' } was "
3199
- + 'requested. Drop the version, request 1.4, or drop resultMode.');
3200
- }
3201
- return explicit;
3202
- }
3203
- return inputs?.resultMode !== undefined ? '1.4' : undefined;
3204
- }
3205
3083
  /**
3206
3084
  * The form-trigger schema: one field per flow input, the same derivation the
3207
3085
  * designer's form editor applies to a flow's arguments (`argument-fields.ts` /
@@ -3246,42 +3124,6 @@ function scheduleInputs(inputs, nodeId) {
3246
3124
  ...(isPreset ? {} : { timerValue: every }),
3247
3125
  };
3248
3126
  }
3249
- /**
3250
- * Encode a delay's single `duration` into the two inputs the platform's node
3251
- * carries — the SAME split as the scheduled trigger above, on the same field
3252
- * names, because it is the same designer control: `timerPreset` holds the
3253
- * duration when it is one of the dropdown's twelve, and otherwise holds the
3254
- * literal `"custom"` with the real duration in `timerValue`. flow-v1's timer
3255
- * serializer collapses them straight back (`event-definition.ts:31`), so the
3256
- * two encodings are the same wait.
3257
- *
3258
- * Both are shapes the platform actually deploys — 26 of 26 archived
3259
- * `skill-flow-delay` flows carry the preset form, 35 of 38 archived
3260
- * `skill-flow-terminate` flows the custom form — so this is a choice of
3261
- * encoding, not of correctness. Emitting the preset when one fits keeps the
3262
- * node's Duration dropdown showing a value instead of "Custom".
3263
- *
3264
- * `timerType` is `timeDuration` for a `duration` and `timeDate` for `until`,
3265
- * because the platform's timer serializer resolves a body as
3266
- * "preset, else timerValue" for EVERY kind and so never reads `timerDate` while
3267
- * a duration is present (flow-builder-sdk#70).
3268
- */
3269
- function delayInputs(inputs) {
3270
- // An absolute-date wait (`until`) is the definition's timeDate arm: it
3271
- // requires timerDate, while the top-level required timerPreset is satisfied
3272
- // with the literal 'custom' (the timeDuration-only allOf never fires).
3273
- const until = String(inputs?.until ?? '').trim();
3274
- if (until) {
3275
- return { timerType: 'timeDate', timerPreset: 'custom', timerDate: until };
3276
- }
3277
- const duration = String(inputs?.duration ?? '').trim();
3278
- const isPreset = DELAY_PRESETS.includes(duration);
3279
- return {
3280
- timerType: 'timeDuration',
3281
- timerPreset: isPreset ? duration : 'custom',
3282
- ...(isPreset ? {} : { timerValue: duration }),
3283
- };
3284
- }
3285
3127
  /** JSON-Schema type per form-field type, as flow-v1's form serializer maps them (quick-form escalations and human tasks). */
3286
3128
  const FORM_FIELD_JSON_TYPE = {
3287
3129
  text: { type: 'string' },
@@ -3356,7 +3198,7 @@ function hitlInputs(inputs, nodeId, flowId, label, rename) {
3356
3198
  const outcomes = (inputs?.outcomes ?? []).map((o, i) => {
3357
3199
  const name = typeof o === 'string' ? o : o?.name;
3358
3200
  const action = typeof o === 'string' ? 'Continue' : (o?.action ?? 'Continue');
3359
- return { id: outcomeId(name, i), name, type: 'string', isPrimary: i === 0, action };
3201
+ return { id: outcomeIdOf(o, i), name, type: 'string', isPrimary: i === 0, action };
3360
3202
  });
3361
3203
  const properties = {};
3362
3204
  for (const f of inputs?.fields ?? []) {
@@ -3632,8 +3474,6 @@ const NO_RENAME = { nodeIds: new Map(), errorEnvelopes: new Map() };
3632
3474
  const ERROR_ENVELOPE_VAR = {
3633
3475
  [NODE_TYPE.httpV2]: 'output',
3634
3476
  };
3635
- /** The `typeVersion` Studio Web stamps on a sticky note. The type has no manifest to take it from. */
3636
- const STICKY_NOTE_TYPE_VERSION = '1.0.0';
3637
3477
  /**
3638
3478
  * Append the flow's `.stickyNotes()` as real `stickyNote` nodes.
3639
3479
  *
@@ -3684,7 +3524,7 @@ function emitStickyNotes(built, scope, claimNoteId) {
3684
3524
  // `flow validate`, manifest or not (#869).
3685
3525
  scope.nodes.push({
3686
3526
  id: emittedId,
3687
- type: 'stickyNote',
3527
+ type: STICKY_NOTE_TYPE,
3688
3528
  typeVersion: STICKY_NOTE_TYPE_VERSION,
3689
3529
  ui: {
3690
3530
  position: { x: note.x, y: note.y },
@@ -3936,22 +3776,12 @@ function describeDeclared(bindings) {
3936
3776
  export function bundledActionNodeType(spec) {
3937
3777
  switch (spec?.kind) {
3938
3778
  case 'http': return spec.inputs?.managed ? NODE_TYPE.httpV2 : NODE_TYPE.http;
3939
- case 'transform': return transformNodeType(spec.inputs);
3940
3779
  case 'hitl': return hitlNodeType(spec.inputs);
3941
3780
  case 'queueItem': return spec.inputs?.wait ? NODE_TYPE.queueCreateAndWait : NODE_TYPE.queueCreate;
3942
3781
  case 'inlineAgent': return INLINE_AGENT_TYPE;
3943
- case 'delay':
3944
- case 'script':
3945
- case 'mock':
3946
3782
  case 'subflow':
3947
3783
  case 'summarize':
3948
3784
  case 'batchTransform':
3949
- case 'documentClassify':
3950
- case 'dynamicExtract':
3951
- case 'dataFabricRead':
3952
- case 'dataFabricCreate':
3953
- case 'dataFabricUpdate':
3954
- case 'dataFabricDelete':
3955
3785
  case 'sendMessage':
3956
3786
  case 'waitForMessage':
3957
3787
  case 'conversationContext':
@@ -4017,14 +3847,35 @@ export function resolveContributionDefinition(ref, requestedVersion, where) {
4017
3847
  throw new Error(`${where}: unknown DefinitionReference source "${ref.source}".`);
4018
3848
  }
4019
3849
  /**
4020
- * The forward port of a contribution-lowered node, derived from its
4021
- * definition's handles: the source handle marked `isDefaultForType`, else the
4022
- * first non-error source handle, else the conventional `output`.
3850
+ * Whether `def` declares `port` as a forward source handle: an exact id, or a
3851
+ * templated one (`outcome-{item.id}` declares every `outcome-<slug>`). `error` is
3852
+ * not a forward port. A definition that declares no handles cannot say, so it
3853
+ * accepts any port.
3854
+ */
3855
+ function declaresForwardPort(def, port) {
3856
+ const handles = (def?.handleConfiguration ?? []).flatMap((g) => g?.handles ?? []);
3857
+ if (handles.length === 0)
3858
+ return true;
3859
+ return handles.some((h) => {
3860
+ if (h?.type !== 'source' || typeof h.id !== 'string' || h.id === 'error')
3861
+ return false;
3862
+ if (!h.id.includes('{'))
3863
+ return h.id === port;
3864
+ const [head, tail] = [h.id.slice(0, h.id.indexOf('{')), h.id.slice(h.id.lastIndexOf('}') + 1)];
3865
+ return port.length > head.length + tail.length && port.startsWith(head) && port.endsWith(tail);
3866
+ });
3867
+ }
3868
+ /**
3869
+ * Forward port: the first concrete source handle (`isDefaultForType` wins), else `output`;
3870
+ * `undefined` when every forward source handle is templated (`case-{item.id}`).
4023
3871
  */
4024
3872
  function contributionPort(def) {
4025
3873
  const handles = (def?.handleConfiguration ?? []).flatMap((g) => g?.handles ?? []);
4026
3874
  const sources = handles.filter((h) => h?.type === 'source' && h?.id !== 'error');
4027
- return sources.find((h) => h?.isDefaultForType)?.id ?? sources[0]?.id ?? 'output';
3875
+ const concrete = sources.filter((h) => typeof h?.id === 'string' && !h.id.includes('{'));
3876
+ if (concrete.length === 0 && sources.length > 0)
3877
+ return undefined;
3878
+ return concrete.find((h) => h?.isDefaultForType)?.id ?? concrete[0]?.id ?? 'output';
4028
3879
  }
4029
3880
  /**
4030
3881
  * Render a switch case value as a JS literal for the case expression.
@@ -4726,12 +4577,21 @@ export function serialize(built, opts = {}) {
4726
4577
  * recorded: `check` answers for those from its per-tenant kind list.
4727
4578
  */
4728
4579
  const bundledNodeDefs = new Map();
4580
+ /**
4581
+ * Node id → whether a registered PER-TENANT node type has an error port, as it
4582
+ * declares (its definition is built per call, so there is nothing else to ask).
4583
+ * A registered bundled type is in `bundledNodeDefs` like any typed family.
4584
+ */
4585
+ const declaredErrorPorts = new Map();
4729
4586
  /**
4730
4587
  * Refuse an `error` edge from a node whose definition has no error port: a
4731
4588
  * contribution-lowered node (#943) or a typed node with a bundled definition
4732
4589
  * (#1011). Same predicate as `check`, so `compile --no-check` agrees with it.
4733
4590
  */
4734
4591
  function refuseErrorEdgeWithoutPort(nodeId, stepName) {
4592
+ const declared = declaredErrorPorts.get(nodeId);
4593
+ if (declared?.port === false)
4594
+ throw new Error(declaredErrorPortRefusal(stepName, declared.typeName, declared.version));
4735
4595
  const contributed = contributionDefs.get(nodeId);
4736
4596
  const def = contributed ?? bundledNodeDefs.get(nodeId);
4737
4597
  if (def !== undefined && !definitionHasErrorPort(def)) {
@@ -4865,6 +4725,8 @@ export function serialize(built, opts = {}) {
4865
4725
  * `.stepToRef` rejoins now do come through here, #938).
4866
4726
  */
4867
4727
  const hitlNodes = new Map();
4728
+ /** Step name → node id of a step whose every forward exit is templated (see `contributionPort`). */
4729
+ const routerNodes = new Map();
4868
4730
  /**
4869
4731
  * Human tasks with an outcome whose path stops at an End while another
4870
4732
  * outcome continues — read by `.parallel`, whose join would wait forever for
@@ -4896,8 +4758,16 @@ export function serialize(built, opts = {}) {
4896
4758
  if (o.requestedVersion !== undefined && def?.version !== o.requestedVersion) {
4897
4759
  const exact = o.def ? undefined : DEFS[`${type}@${o.requestedVersion}`];
4898
4760
  if (!exact) {
4761
+ // Name every version the SDK could have resolved: every bundled
4762
+ // version of the type, else (a type built per resource, a connector)
4763
+ // the definition in hand. Naming only the default sent the reader
4764
+ // looking for a version that was shipped all along (#1128:
4765
+ // quick-form bundles 1.0 AND 1.1, and the message said 1.0).
4766
+ const bundled = bundledVersions(type);
4767
+ const resolvable = bundled.length > 0 ? bundled
4768
+ : [def?.version].filter((v) => typeof v === 'string');
4899
4769
  throw new Error(`"${label ?? id}" requests ${type}@${o.requestedVersion}, but this SDK resolves only `
4900
- + `${type}@${def?.version ?? '<none>'}. Versions are exact — omit { version } to use `
4770
+ + `${type}@${resolvable.join(', ') || '<none>'}. Versions are exact — omit { version } to use `
4901
4771
  + `the pinned default, or request a version this SDK ships.`);
4902
4772
  }
4903
4773
  def = exact;
@@ -4910,18 +4780,15 @@ export function serialize(built, opts = {}) {
4910
4780
  id,
4911
4781
  type,
4912
4782
  typeVersion: def?.version ?? '1.0.0',
4913
- ui: { position: { x: 256 + col * 288, y: 144 + (o.parentId ? 160 : 0) }, size: { width: 96, height: 96 }, collapsed: false },
4783
+ ui: { position: placeholderPosition(col, Boolean(o.parentId)), size: { width: 96, height: 96 }, collapsed: false },
4914
4784
  display: { label: label ?? def?.display?.label ?? id, ...(def?.display?.icon ? { icon: def.display.icon } : {}) },
4915
4785
  };
4916
4786
  col++;
4917
- // A Data Fabric node gets no node-level `model`, whichever path emits it
4918
- // (the typed verbs, or a rawNode through emitContribution). Studio Web
4919
- // never writes one (flow-core node.ts: "`node.model` is never written";
4920
- // canvas `nodeToInstance` omits it), and export seeds it from the
4921
- // definition (services bpmn-to-xml.ts), so a copy here can only go stale.
4922
- // `outputs` stay: the canvas persists them (`source: '=response'`), and
4923
- // `variables.nodes[]` is derived from them.
4924
- if (def?.model && !type.startsWith('core.datafabric.'))
4787
+ // A registered node type may carry no node-level `model`, whichever path
4788
+ // emits it (its own serialize, or a rawNode through emitContribution) — the
4789
+ // Data Fabric verbs, whose reasons are in their family (`nodeModel`).
4790
+ const omitModel = registeredRawType(type, String(def?.version ?? ''))?.impl.nodeModel === 'omit';
4791
+ if (def?.model && !omitModel)
4925
4792
  node.model = subNodeId(jsonClone(def.model), id);
4926
4793
  if (def?.outputDefinition)
4927
4794
  node.outputs = subNodeId(jsonClone(def.outputDefinition), id);
@@ -5124,7 +4991,7 @@ export function serialize(built, opts = {}) {
5124
4991
  function successPort(type) {
5125
4992
  if (type === NODE_TYPE.http || type === NODE_TYPE.httpV2)
5126
4993
  return 'default';
5127
- if (type === NODE_TYPE.script || type === NODE_TYPE.loop)
4994
+ if (type === NODE_TYPE.loop)
5128
4995
  return 'success';
5129
4996
  // Both queue definitions declare exactly one source handle, `success` — and
5130
4997
  // the shipped `sample-flows/queue-operations.flow` wires its edges to it.
@@ -5156,9 +5023,6 @@ export function serialize(built, opts = {}) {
5156
5023
  // as it is in all 151 deployed IxP instances.
5157
5024
  if (type.startsWith('uipath.ixp.'))
5158
5025
  return 'success';
5159
- // Classify declares the same single `success` source handle as the extractors.
5160
- if (type === NODE_TYPE.documentClassify)
5161
- return 'success';
5162
5026
  // connectors, the trigger, the merge join and the mock placeholder all expose
5163
5027
  // their forward edge on `output` (merge's definition declares exactly one
5164
5028
  // source handle, `output`; so does `core.logic.mock`, and all 74 archived
@@ -5185,27 +5049,119 @@ export function serialize(built, opts = {}) {
5185
5049
  const connectorRefusal = connectorRawNodeRefusal(def.nodeType);
5186
5050
  if (connectorRefusal)
5187
5051
  throw new Error(`${where}: ${connectorRefusal}`);
5188
- // A Data Fabric key the product never reads (`_sortOptions`) is refused for
5189
- // every contribution, whichever class or SDK copy made it: `validate`
5190
- // cannot see it, and the node would ignore it at run time.
5191
- const unknownInput = dataFabricUnknownInputs(def.nodeType, def.version, contribution.inputs);
5052
+ // An input a registered node type never reads (a Data Fabric `_sortOptions`)
5053
+ // is refused for every contribution, whichever class or SDK copy made it:
5054
+ // `validate` cannot see it, and the node would ignore it at run time.
5055
+ const unknownInput = rawNodeInputProblem(def.nodeType, def.version, contribution.inputs);
5192
5056
  if (unknownInput?.severity === 'error')
5193
5057
  throw new Error(`${where}: ${unknownInput.message}`);
5194
5058
  const id = stepUid(scope, stepName);
5195
- const node = makeNode(def.nodeType, id, stepName, { parentId, def, requestedVersion });
5059
+ contributionDefs.set(id, def);
5060
+ return placeStepNode({ where, stepName, id, typeName: def.nodeType, requestedVersion, def, contribution, tails, scope, parentId });
5061
+ }
5062
+ /**
5063
+ * Place one step node from a contribution — the one path a custom FlowAction
5064
+ * (emitContribution) and a registered node type (emitRegistered) share
5065
+ * (docs/NODE_TYPE_ARCHITECTURE_DESIGN.md, Phase 3). The serializer owns the
5066
+ * rest: the node envelope from the definition, rendering, bindings, edges, and
5067
+ * the forward port. `def` is undefined only for a registered bundled type,
5068
+ * whose definition is `<typeName>@<version>` from the bundle.
5069
+ */
5070
+ function placeStepNode(o) {
5071
+ const { id, contribution, scope } = o;
5072
+ const node = makeNode(o.typeName, id, o.stepName, { parentId: o.parentId, def: o.def, requestedVersion: o.requestedVersion });
5073
+ if (contribution.model === 'omit')
5074
+ delete node.model;
5196
5075
  node.inputs = renderDeep(contribution.inputs ?? {}, scope.rename);
5197
5076
  if (contribution.outputs) {
5198
5077
  node.outputs = { ...(node.outputs ?? {}), ...subNodeId(jsonClone(contribution.outputs), id) };
5199
5078
  }
5200
- hoistContributionBindings(def, contribution.bindings, where);
5201
- contributionDefs.set(id, def);
5079
+ // A bundled registered type returns no definition; its bindings still close
5080
+ // against the bundled one (#942), counting what it bound through the context
5081
+ // as supplied (first, in the order it bound them, as when `bind` hoisted directly).
5082
+ const def = o.def ?? bundledDefinition(o.typeName, o.requestedVersion);
5083
+ hoistContributionBindings(def, [...(o.bound ?? []), ...(contribution.bindings ?? [])], o.where);
5202
5084
  scope.nodes.push(node);
5203
- for (const t of tails)
5085
+ for (const t of o.tails)
5204
5086
  addEdge(scope, t, id);
5205
- return [{ node: id, port: contributionPort(def) }];
5087
+ const port = contribution.port ?? contributionPort(def);
5088
+ if (port === undefined) {
5089
+ // Only templated exits: no main path leaves this node. Remember it so a
5090
+ // following `.stepToList` / side-exit `.stepToRef` can still attach.
5091
+ routerNodes.set(o.stepName, id);
5092
+ return [];
5093
+ }
5094
+ return [{ node: id, port }];
5095
+ }
5096
+ /**
5097
+ * Lower a step whose kind is a registered node type (flow-node-registry.ts):
5098
+ * the node type serializes its spec through a context, and this owns the rest —
5099
+ * id, label, `typeVersion`, definition, rendering, bindings, edges — exactly as
5100
+ * for a custom contribution. docs/NODE_TYPE_ARCHITECTURE_DESIGN.md §4.
5101
+ */
5102
+ function emitRegistered(entry, step, tails, scope, parentId) {
5103
+ const where = `step "${step.name}"`;
5104
+ const spec = step.spec;
5105
+ const impl = resolveVersion(entry.type, spec, step.options?.version, where);
5106
+ const id = stepUid(scope, step.name);
5107
+ // As for every typed family: record the failure-envelope boundary before any
5108
+ // expression renders, so a handler reading this step resolves correctly.
5109
+ const envelope = ownEntry(ERROR_ENVELOPE_VAR, impl.typeName);
5110
+ if (envelope)
5111
+ scope.rename.errorEnvelopes.set(step.name, envelope);
5112
+ const ctx = {
5113
+ step: step.name,
5114
+ version: impl.version,
5115
+ render: (value) => renderValue(value, scope.rename),
5116
+ renameRefs: (js) => renameRefs(js, scope.rename),
5117
+ stepNodeId: (name) => scope.stepNodeId.get(name) ?? name,
5118
+ mint: (seed) => stableId(`${built.id}:${id}:${seed}`),
5119
+ bind: (b) => { bound.push(b); },
5120
+ definition: (typeName, version) => bundledDefinition(typeName, version),
5121
+ };
5122
+ const bound = [];
5123
+ const contribution = impl.serialize(spec, ctx);
5124
+ // The definition contract, both halves: a per-tenant type supplies its
5125
+ // definition, and a bundled type does not (its definition is the bundle's, and
5126
+ // makeNode records it, so the error-edge refusal and check ask the same one).
5127
+ let def;
5128
+ if (impl.definition === 'bundled' && contribution.definition !== undefined) {
5129
+ throw new Error(`internal SDK error: ${where}: bundled node type ${impl.typeName}@${impl.version} returned a definition; `
5130
+ + 'the bundle owns it — omit `definition`, or declare the type per-tenant.');
5131
+ }
5132
+ if (contribution.definition !== undefined) {
5133
+ def = resolveContributionDefinition(contribution.definition, impl.version, where);
5134
+ }
5135
+ else if (impl.definition === 'per-tenant') {
5136
+ throw new Error(`internal SDK error: ${where}: per-tenant node type ${impl.typeName}@${impl.version} returned no definition.`);
5137
+ }
5138
+ if (impl.definition === 'per-tenant') {
5139
+ const port = typeof impl.errorPort === 'function' ? impl.errorPort(spec) : impl.errorPort === true;
5140
+ // The declaration answers for check, which has no definition; here the
5141
+ // definition is in hand, and the two must agree (#1011).
5142
+ if (port && def !== undefined && !definitionHasErrorPort(def)) {
5143
+ throw new Error(`internal SDK error: ${where}: node type ${impl.typeName}@${impl.version} declares an error port, `
5144
+ + 'but the definition it supplied has no error handle and no supportsErrorHandling.');
5145
+ }
5146
+ declaredErrorPorts.set(id, { port, typeName: impl.typeName, version: impl.version });
5147
+ }
5148
+ if (contribution.port !== undefined && !declaresForwardPort(def ?? bundledDefinition(impl.typeName, impl.version), contribution.port)) {
5149
+ throw new Error(`internal SDK error: ${where}: node type ${impl.typeName}@${impl.version} leaves by port "${contribution.port}", `
5150
+ + 'which its definition declares no source handle for.');
5151
+ }
5152
+ return placeStepNode({
5153
+ where, stepName: step.name, id, typeName: impl.typeName, requestedVersion: impl.version, def, contribution, bound, tails, scope, parentId,
5154
+ });
5206
5155
  }
5207
5156
  function emitAction(step, tails, scope, parentId) {
5208
5157
  const spec = step.spec;
5158
+ // A registered node type serializes itself; every other kind falls through to
5159
+ // the branches below until its family is ported (FB014's pins are the list).
5160
+ const registered = typeof spec.kind === 'string'
5161
+ ? FLOW_NODE_TYPES.byKind(spec.kind, spec)
5162
+ : undefined;
5163
+ if (registered)
5164
+ return emitRegistered(registered, step, tails, scope, parentId);
5209
5165
  if (spec.kind === 'subflow') {
5210
5166
  const id = stepUid(scope, step.name);
5211
5167
  // `subflows` is ONE flat map for the whole file, keyed by the calling
@@ -5473,6 +5429,13 @@ export function serialize(built, opts = {}) {
5473
5429
  const contexts = s.context === undefined ? []
5474
5430
  : Array.isArray(s.context) ? s.context : [s.context];
5475
5431
  for (const cx of contexts) {
5432
+ // `check`'s AGENT_CONTEXT_NO_ID, again for `--no-check`: a blank id builds
5433
+ // the type `…context.index.<slug>.`, which no tenant serves, and nothing
5434
+ // after this point would say so.
5435
+ if (typeof cx?.id !== 'string' || !GUID_RE.test(cx.id)) {
5436
+ throw Object.assign(new Error(`inline agent "${step.name}": context index "${cx?.name ?? ''}" needs its index id (a GUID) as \`id\`; `
5437
+ + `got ${JSON.stringify(cx?.id)}. \`uip solution resources list --kind Index --source remote --search "<name>"\` returns it as \`Key\`.`), { code: 'AGENT_CONTEXT_NO_ID' });
5438
+ }
5476
5439
  const cdef = contextIndexDef({ name: cx.name, id: cx.id });
5477
5440
  // `<step>Context`, deduped by `uid` for a second index on the same agent.
5478
5441
  // Not slugged from the index NAME: an id is the author's handle on the node
@@ -5936,7 +5899,7 @@ export function serialize(built, opts = {}) {
5936
5899
  const { ids, entries } = queueItemBindings(s, resourceBindings);
5937
5900
  for (const b of entries)
5938
5901
  resourceBindings.set(b.id, b);
5939
- const def = queueItemDef(s, step.name, ids);
5902
+ const def = queueItemDef(s, step.name, ids, step.options?.version);
5940
5903
  const id = stepUid(scope, step.name);
5941
5904
  // The result is read as `$vars.<step>.output`, like every other family: the
5942
5905
  // node's own output block maps the runtime `response` onto the variable
@@ -6310,154 +6273,6 @@ export function serialize(built, opts = {}) {
6310
6273
  }
6311
6274
  return [{ node: id, port: successPort(hitlType) }];
6312
6275
  }
6313
- if (spec.kind === 'mock') {
6314
- // A placeholder is a pure `makeNode` node: the definition supplies `model`
6315
- // and the single `output` (`source: "null"`), and there is nothing else to
6316
- // write. `inputs: {}` rather than an omitted key because that is what the
6317
- // vendored Studio-Web golden writes (`shared/sample-flows/new.flow`, all
6318
- // three of its mock nodes) and what 36 of the 74 archived instances carry.
6319
- //
6320
- // Nothing here can open a fixture channel: a runner that honours one looks
6321
- // for `fixture`/`output`/`value` in the node's `inputs`, and the factory
6322
- // takes no arguments, so `inputs` is always the empty object. That
6323
- // is the #125 fence, made unrepresentable rather than documented.
6324
- const id = stepUid(scope, step.name);
6325
- const node = makeNode(NODE_TYPE.mock, id, step.name, { parentId, requestedVersion: step.options?.version });
6326
- node.inputs = {};
6327
- scope.nodes.push(node);
6328
- for (const t of tails)
6329
- addEdge(scope, t, id);
6330
- return [{ node: id, port: successPort(NODE_TYPE.mock) }];
6331
- }
6332
- if (spec.kind === 'documentClassify' || spec.kind === 'dynamicExtract') {
6333
- const type = spec.kind === 'documentClassify' ? NODE_TYPE.documentClassify : NODE_TYPE.dynamicExtract;
6334
- const id = stepUid(scope, step.name);
6335
- const node = makeNode(type, id, step.name, { parentId, requestedVersion: step.options?.version });
6336
- const def = defByType.get(type);
6337
- const defaults = jsonClone(def?.inputDefaults ?? {});
6338
- // A `uipath.ixp.*` fileRef must stay a PLAIN `=js:` string — the platform
6339
- // reads it unwrapped (see migrate's UNWRAPPED_INPUTS); classify follows
6340
- // the default envelope rules.
6341
- const fileRef = renderValue(toExpr(spec.inputs.fileRef), scope.rename);
6342
- if (spec.kind === 'documentClassify') {
6343
- node.inputs = {
6344
- ...defaults,
6345
- fileRef,
6346
- ...(spec.inputs.pageRange !== undefined ? { pageRange: spec.inputs.pageRange } : {}),
6347
- ...(spec.inputs.splitPages !== undefined ? { splitPages: spec.inputs.splitPages } : {}),
6348
- ...(spec.inputs.modelConfig !== undefined
6349
- ? { modelConfig: { ...(defaults.modelConfig ?? {}), ...spec.inputs.modelConfig } }
6350
- : {}),
6351
- };
6352
- }
6353
- else {
6354
- const m = spec.inputs.model;
6355
- node.inputs = {
6356
- ...defaults,
6357
- // The deployment identity Studio Web's panel destructures — both the
6358
- // nested `model` object and the flat mirrors the definition declares.
6359
- model: { modelName: m.modelName, folderKey: m.folderKey },
6360
- modelName: m.modelName,
6361
- folderKey: m.folderKey,
6362
- ...(m.projectId !== undefined ? { projectId: m.projectId } : {}),
6363
- ...(m.projectName !== undefined ? { projectName: m.projectName } : {}),
6364
- ...(m.folderName !== undefined ? { folderName: m.folderName } : {}),
6365
- ...(m.versionTag !== undefined ? { versionTag: m.versionTag } : {}),
6366
- fileRef,
6367
- ...(spec.inputs.pageRange !== undefined ? { pageRange: spec.inputs.pageRange } : {}),
6368
- ...(spec.inputs.modelConfig !== undefined
6369
- ? { modelConfig: { ...(defaults.modelConfig ?? {}), ...spec.inputs.modelConfig } }
6370
- : {}),
6371
- ...(spec.inputs.schema !== undefined ? { schemaDocument: spec.inputs.schema } : {}),
6372
- };
6373
- // The definition's model declares `source: true`: the platform mints a
6374
- // stable storage slot per instance; omit it and the node validates then
6375
- // faults. Deterministic, like every other minted identity here.
6376
- node.inputs.source = stableId(`${built.id}:${id}:source`);
6377
- // IxP instances must NOT carry the manifest's model block — the slim
6378
- // { type, serviceType } lives only in definitions[] (product validate
6379
- // rejects a node-level copy).
6380
- delete node.model;
6381
- }
6382
- scope.nodes.push(node);
6383
- for (const t of tails)
6384
- addEdge(scope, t, id);
6385
- return [{ node: id, port: successPort(type) }];
6386
- }
6387
- if (spec.kind === 'dataFabricRead' || spec.kind === 'dataFabricCreate'
6388
- || spec.kind === 'dataFabricUpdate' || spec.kind === 'dataFabricDelete') {
6389
- const type = NODE_TYPE[spec.kind];
6390
- const id = stepUid(scope, step.name);
6391
- const node = makeNode(type, id, step.name, {
6392
- parentId,
6393
- requestedVersion: spec.kind === 'dataFabricRead'
6394
- ? dataFabricReadVersion(spec.inputs, step.options?.version)
6395
- : step.options?.version,
6396
- });
6397
- const cfg = { entityName: spec.inputs.entity };
6398
- // `recordSource` is how update and delete both name their target; the
6399
- // read step's emitted node id is rename-aware, for subflow copies.
6400
- const targetRecord = (record) => {
6401
- if (typeof record.fromRead === 'string') {
6402
- cfg.recordSource = 'fromRead';
6403
- cfg.readEntityNodeId = scope.stepNodeId.get(record.fromRead) ?? record.fromRead;
6404
- }
6405
- else {
6406
- cfg.recordSource = 'byId';
6407
- cfg.recordId = String(renderValue(record.byId, scope.rename));
6408
- }
6409
- };
6410
- const fieldRows = (values) => Object.entries(values ?? {}).map(([field, value]) => ({
6411
- field,
6412
- value: String(renderValue(value, scope.rename)),
6413
- }));
6414
- if (spec.kind === 'dataFabricRead') {
6415
- // Only 1.4 declares it, and only 1.4 is emitted when it is set (see
6416
- // dataFabricReadVersion) — so writing it can never land on a 1.0 node,
6417
- // where it would be an input the definition does not know.
6418
- if (spec.inputs.resultMode !== undefined)
6419
- cfg.resultMode = spec.inputs.resultMode;
6420
- if (spec.inputs.limit !== undefined)
6421
- cfg._recordLimit = spec.inputs.limit;
6422
- if (spec.inputs.skip !== undefined)
6423
- cfg._skip = spec.inputs.skip;
6424
- if (spec.inputs.sort !== undefined) {
6425
- cfg._sort = { field: spec.inputs.sort.field, direction: spec.inputs.sort.direction ?? 'asc' };
6426
- }
6427
- if (spec.inputs.fields?.length)
6428
- cfg._selectedFieldNames = [...spec.inputs.fields];
6429
- if (spec.inputs.filters?.length) {
6430
- cfg._filters = dataFabricFilterRows(spec.inputs.filters, (value) => String(renderValue(value, scope.rename)));
6431
- }
6432
- }
6433
- else if (spec.kind === 'dataFabricCreate') {
6434
- cfg.fieldValues = fieldRows(spec.inputs.values);
6435
- }
6436
- else if (spec.kind === 'dataFabricUpdate') {
6437
- targetRecord(spec.inputs.record);
6438
- cfg.fieldUpdates = fieldRows(spec.inputs.set);
6439
- }
6440
- else {
6441
- targetRecord(spec.inputs.record);
6442
- }
6443
- if (spec.inputs.folderKey !== undefined)
6444
- cfg._folderKey = spec.inputs.folderKey;
6445
- // A folder-scoped entity is portable only with its `Entity` bindings; the
6446
- // half-authored form is refused by check(), so reaching here with both
6447
- // means emit both. Same id for the same (entity, resourceKey), so two
6448
- // steps on one entity share one pair rather than duplicating it.
6449
- if (spec.inputs.folderKey !== undefined && spec.inputs.resourceKey !== undefined) {
6450
- cfg._resourceKey = spec.inputs.resourceKey;
6451
- for (const entry of entityBindings(spec.inputs.entity, spec.inputs.folderKey, spec.inputs.resourceKey)) {
6452
- resourceBindings.set(entry.id, entry);
6453
- }
6454
- }
6455
- node.inputs = { entityConfig: cfg };
6456
- scope.nodes.push(node);
6457
- for (const t of tails)
6458
- addEdge(scope, t, id);
6459
- return [{ node: id, port: successPort(type) }];
6460
- }
6461
6276
  // A custom FlowAction subclass carries no `kind`; it compiles through its
6462
6277
  // own contribution (design §4). Anything else without a recognized kind is
6463
6278
  // an authoring error — it must NOT fall through into the script branch
@@ -6469,7 +6284,7 @@ export function serialize(built, opts = {}) {
6469
6284
  }
6470
6285
  throw new Error(`step "${step.name}": not an action spec — pass a factory result, subflow(), or a FlowAction subclass instance.`);
6471
6286
  }
6472
- if (!['http', 'transform', 'delay', 'script'].includes(spec.kind)) {
6287
+ if (spec.kind !== 'http') {
6473
6288
  throw new Error(`step "${step.name}": unknown action kind "${spec.kind}".`);
6474
6289
  }
6475
6290
  const type = bundledActionNodeType(spec);
@@ -6482,6 +6297,7 @@ export function serialize(built, opts = {}) {
6482
6297
  const envelope = ownEntry(ERROR_ENVELOPE_VAR, type);
6483
6298
  if (envelope)
6484
6299
  scope.rename.errorEnvelopes.set(step.name, envelope);
6300
+ // The guard above leaves http; registered kinds returned at the top.
6485
6301
  node.inputs = actionInputs(spec, scope.rename, opts, step.name);
6486
6302
  if (spec.kind === 'http') {
6487
6303
  node.outputs = httpOutputs(node.outputs, spec.inputs.returns, spec.inputs.managed);
@@ -6501,12 +6317,6 @@ export function serialize(built, opts = {}) {
6501
6317
  resourceBindings.set(entry.id, entry);
6502
6318
  }
6503
6319
  }
6504
- if (spec.kind === 'script') {
6505
- node.outputs = scriptOutputs(node.outputs, spec.inputs.returns, spec.inputs.code);
6506
- }
6507
- if (spec.kind === 'transform') {
6508
- node.outputs = transformOutputs(node.outputs);
6509
- }
6510
6320
  scope.nodes.push(node);
6511
6321
  for (const t of tails)
6512
6322
  addEdge(scope, t, id);
@@ -6572,8 +6382,13 @@ export function serialize(built, opts = {}) {
6572
6382
  * from a non-default port is a side exit. Only the seed differs from an ordinary
6573
6383
  * list — the same node, a different port.
6574
6384
  */
6575
- function emitPortList(step, source, tails, scope, parentId, breakTarget) {
6385
+ function emitPortList(step, source, tails, scope, parentId, loopExits) {
6576
6386
  const handler = step.body;
6387
+ // A node whose exits are all templated hands on no tail; its port lists
6388
+ // leave from the node itself.
6389
+ const router = tails.length === 0 ? routerNodes.get(source) : undefined;
6390
+ if (router !== undefined)
6391
+ tails = [{ node: router, port: step.port }];
6577
6392
  // `emitAction` returns one tail for every action kind — the node's own
6578
6393
  // success port — except a fan-out human task, which returns one per outcome
6579
6394
  // from the SAME node. Tails from two nodes mean a new action shape reached
@@ -6592,7 +6407,7 @@ export function serialize(built, opts = {}) {
6592
6407
  refuseErrorEdgeWithoutPort(nodeId, source);
6593
6408
  node.inputs = { ...(node.inputs ?? {}), errorHandlingEnabled: true };
6594
6409
  }
6595
- const handlerTails = emit(handler, [{ node: nodeId, port: step.port }], scope, parentId, breakTarget);
6410
+ const handlerTails = emit(handler, [{ node: nodeId, port: step.port }], scope, parentId, loopExits);
6596
6411
  // A body that runs out of steps ends the run on an End of its own (no
6597
6412
  // merge-back is inferred). That End maps the outputs like any other, so a
6598
6413
  // body that set them with `{ updates }` still answers (#742).
@@ -6600,13 +6415,13 @@ export function serialize(built, opts = {}) {
6600
6415
  const endId = uid(scope, `${source}${step.port === 'error' ? 'Error' : step.port.charAt(0).toUpperCase() + step.port.slice(1)}End`);
6601
6416
  emitEnd(scope, endId, handlerTails, { parentId });
6602
6417
  }
6603
- return tails;
6418
+ return router !== undefined ? [] : tails;
6604
6419
  }
6605
6420
  function emit(steps, incoming, scope, parentId,
6606
- // The enclosing container's break handle, when inside a loop/do-while body.
6607
- // Branch/switch/parallel arms and error handlers INHERIT it; a nested
6608
- // container replaces it for its own body.
6609
- breakTarget) {
6421
+ // The enclosing container's break and continue handles, when inside a
6422
+ // loop/do-while body. Branch/switch/parallel arms and error handlers INHERIT
6423
+ // them; a nested container replaces them for its own body.
6424
+ loopExits) {
6610
6425
  let tails = incoming;
6611
6426
  // The action a port-scoped edge leaves. `.stepToList('error', …)` and
6612
6427
  // `.stepToRef('error', …)` are list MEMBERS that follow the step they attach
@@ -6650,6 +6465,14 @@ export function serialize(built, opts = {}) {
6650
6465
  node.display = { ...(node.display ?? {}), label };
6651
6466
  };
6652
6467
  for (const [stepIndex, step] of steps.entries()) {
6468
+ // After a node whose exits are all templated, only its port lists may
6469
+ // follow: nothing continues from it, so a step here would hang off no edge.
6470
+ const router = lastAction !== undefined && tails.length === 0 ? routerNodes.get(lastAction) : undefined;
6471
+ if (router !== undefined && step.kind !== 'stepToList' && step.kind !== 'stepToRef') {
6472
+ throw new Error(`step "${lastAction}": its definition declares only templated exits (such as case-{item.id}), so no `
6473
+ + `path continues from it and ${'name' in step ? `"${step.name}"` : `.${step.kind}()`} after it would `
6474
+ + `have no incoming edge. Route each exit with .stepToList('<port>', …), e.g. .stepToList('case-<id>', …).`);
6475
+ }
6653
6476
  if (step.kind === 'action') {
6654
6477
  tails = emitAction(step, tails, scope, parentId);
6655
6478
  applyUpdates(step);
@@ -6678,7 +6501,7 @@ export function serialize(built, opts = {}) {
6678
6501
  }
6679
6502
  }
6680
6503
  else if (step.kind === 'stepToList') {
6681
- tails = emitPortList(step, lastAction ?? '?', tails, scope, parentId, breakTarget);
6504
+ tails = emitPortList(step, lastAction ?? '?', tails, scope, parentId, loopExits);
6682
6505
  }
6683
6506
  else if (step.kind === 'stepSwitch') {
6684
6507
  // The action, then EVERY exit it declares as an arm — no tacit tail.
@@ -6723,7 +6546,7 @@ export function serialize(built, opts = {}) {
6723
6546
  // from all of them. (`.stepToList` instead gives an arm that runs out
6724
6547
  // of steps an End of its own; `emitEnd` maps the outputs on it,
6725
6548
  // flow-builder-sdk#742.)
6726
- const caseTails = emit(c.body, [{ node: nodeId, port: exit.port }], scope, parentId, breakTarget);
6549
+ const caseTails = emit(c.body, [{ node: nodeId, port: exit.port }], scope, parentId, loopExits);
6727
6550
  if (caseTails.length === 0 && stopsAtEnd(c.body))
6728
6551
  stopped.push(String(c.value));
6729
6552
  armTails.push(...(hitlNodes.has(nodeId)
@@ -6768,8 +6591,8 @@ export function serialize(built, opts = {}) {
6768
6591
  scope.nodes.push(node);
6769
6592
  for (const t of tails)
6770
6593
  addEdge(scope, t, id);
6771
- const thenTails = emit(step.then, [{ node: id, port: 'true' }], scope, parentId, breakTarget);
6772
- const elseTails = emit(step.otherwise, [{ node: id, port: 'false' }], scope, parentId, breakTarget);
6594
+ const thenTails = emit(step.then, [{ node: id, port: 'true' }], scope, parentId, loopExits);
6595
+ const elseTails = emit(step.otherwise, [{ node: id, port: 'false' }], scope, parentId, loopExits);
6773
6596
  tails = [...thenTails, ...elseTails];
6774
6597
  }
6775
6598
  else if (step.kind === 'switch') {
@@ -6784,8 +6607,11 @@ export function serialize(built, opts = {}) {
6784
6607
  // the runtime — so no `=js:` prefix, same as a decision's `expression`.
6785
6608
  const id = stepUid(scope, step.name);
6786
6609
  const node = makeNode(NODE_TYPE.switch, id, step.options?.label ?? step.label ?? step.name, { parentId, requestedVersion: step.options?.version });
6610
+ // `id` is the author's when given (a decompiled switch keeps the
6611
+ // canvas's `default-1`, …: merge keeps the original node, so the edges
6612
+ // must leave from ITS handles), else positional (#1130).
6787
6613
  const cases = step.cases.map((c, i) => ({
6788
- id: `case_${i + 1}`,
6614
+ id: c.id ?? `case_${i + 1}`,
6789
6615
  label: c.label ?? `Case ${caseLiteral(c.value)}`,
6790
6616
  expression: `${renameRefs(step.on.js, scope.rename)} === ${caseLiteral(c.value)}`,
6791
6617
  }));
@@ -6798,9 +6624,9 @@ export function serialize(built, opts = {}) {
6798
6624
  // merge node; the convergence point is found by graph search).
6799
6625
  const armTails = [];
6800
6626
  for (const [i, c] of step.cases.entries())
6801
- armTails.push(...emit(c.body, [{ node: id, port: `case-${cases[i].id}` }], scope, parentId, breakTarget));
6627
+ armTails.push(...emit(c.body, [{ node: id, port: `case-${cases[i].id}` }], scope, parentId, loopExits));
6802
6628
  if (step.default !== undefined)
6803
- armTails.push(...emit(step.default, [{ node: id, port: 'default' }], scope, parentId));
6629
+ armTails.push(...emit(step.default, [{ node: id, port: 'default' }], scope, parentId, loopExits));
6804
6630
  tails = armTails;
6805
6631
  }
6806
6632
  else if (step.kind === 'parallel') {
@@ -6833,7 +6659,7 @@ export function serialize(built, opts = {}) {
6833
6659
  const armTails = [];
6834
6660
  const endsBefore = hitlEndExits.length;
6835
6661
  for (const arm of step.arms) {
6836
- const t = emit(arm, tails, scope, parentId, breakTarget);
6662
+ const t = emit(arm, tails, scope, parentId, loopExits);
6837
6663
  // An arm hands the join ONE edge, or several that are alternatives —
6838
6664
  // `.branch` / `.switch` / `.stepSwitch` arms, a fan-out task's
6839
6665
  // outcomes — of which only one fires. Refused when any of them is a
@@ -6918,7 +6744,7 @@ export function serialize(built, opts = {}) {
6918
6744
  addEdge(scope, t, id);
6919
6745
  const bodyPort = v24 ? 'start' : 'output';
6920
6746
  const backPort = v24 ? 'continue' : 'loopBack';
6921
- const bodyTails = emit(step.body, [{ node: id, port: bodyPort }], scope, id, v24 ? { node: id, port: 'break' } : undefined);
6747
+ const bodyTails = emit(step.body, [{ node: id, port: bodyPort }], scope, id, v24 ? { node: id, break: 'break', continue: backPort } : { node: id, continue: backPort });
6922
6748
  for (const bt of bodyTails)
6923
6749
  addEdge(scope, bt, id, backPort);
6924
6750
  applyUpdates(step);
@@ -6937,18 +6763,29 @@ export function serialize(built, opts = {}) {
6937
6763
  scope.nodes.push(node);
6938
6764
  for (const t of tails)
6939
6765
  addEdge(scope, t, id);
6940
- const bodyTails = emit(step.body, [{ node: id, port: 'start' }], scope, id, { node: id, port: 'break' });
6766
+ const bodyTails = emit(step.body, [{ node: id, port: 'start' }], scope, id, { node: id, break: 'break', continue: 'continue' });
6941
6767
  for (const bt of bodyTails)
6942
6768
  addEdge(scope, bt, id, 'continue');
6943
6769
  applyUpdates(step);
6944
6770
  tails = [{ node: id, port: 'success' }];
6945
6771
  }
6946
6772
  else if (step.kind === 'break') {
6947
- if (!breakTarget) {
6773
+ if (!loopExits?.break) {
6948
6774
  throw new Error('.break() has no enclosing .loop()/.doWhile() body to exit — it is only valid inside a container.');
6949
6775
  }
6950
6776
  for (const t of tails)
6951
- addEdge(scope, t, breakTarget.node, breakTarget.port);
6777
+ addEdge(scope, t, loopExits.node, loopExits.break);
6778
+ tails = [];
6779
+ }
6780
+ else if (step.kind === 'continue') {
6781
+ // The same edge a body's last step gets, from wherever this path has got
6782
+ // to — so a handler or an arm can go on to the next iteration instead of
6783
+ // ending the run on an End of its own (#1129).
6784
+ if (!loopExits) {
6785
+ throw new Error('.continue() has no enclosing .loop()/.doWhile() body to go back to — it is only valid inside a container.');
6786
+ }
6787
+ for (const t of tails)
6788
+ addEdge(scope, t, loopExits.node, loopExits.continue);
6952
6789
  tails = [];
6953
6790
  }
6954
6791
  else if (step.kind === 'terminate') {
@@ -6986,7 +6823,8 @@ export function serialize(built, opts = {}) {
6986
6823
  else {
6987
6824
  // Any other port is a SIDE EXIT from the step just emitted. The main
6988
6825
  // path is untouched, so `tails` survives and chaining continues.
6989
- const from = tails[0];
6826
+ const router = tails.length === 0 && lastAction !== undefined ? routerNodes.get(lastAction) : undefined;
6827
+ const from = tails[0] ?? (router !== undefined ? { node: router, port: step.port } : undefined);
6990
6828
  if (!from)
6991
6829
  throw new Error(`.stepToRef('${step.port}', '${step.target}') has no step to leave from.`);
6992
6830
  refuseRewire('.stepToRef', step.port, lastAction ?? '?', tails);
@@ -7364,88 +7202,7 @@ export function assertBindingPlaceholdersResolve(flow) {
7364
7202
  + `"invalid GUID <bindings.…>") and refuses to publish. This is a serializer defect, not an authoring `
7365
7203
  + `error; report it with the flow source.`);
7366
7204
  }
7367
- /**
7368
- * Emit a transform's `inputs` in the canonical shape the node's runtime reads:
7369
- * `collection` as a BARE variable path (not `=js:`-prefixed — it is a path
7370
- * field, and the runtime turns the string into a lookup), and `operations` as
7371
- * the array of `{ id, type, config }` descriptors the designer writes.
7372
- *
7373
- * Ids are synthesized here rather than asked of the author, the same way switch
7374
- * case ids are: they identify a row in the designer's editor and carry no
7375
- * meaning for the author. Defaults are made explicit for the same reason the
7376
- * HTTP case spells out `method`/`contentType` — the emitted file should say
7377
- * what it does rather than rely on the runtime's fallbacks.
7378
- */
7379
- function transformInputs(inputs, rename = NO_RENAME) {
7380
- const operations = (inputs.operations ?? []).map((op, i) => {
7381
- const id = `op${i + 1}`;
7382
- if (op.type === 'filter') {
7383
- return {
7384
- id,
7385
- type: 'filter',
7386
- config: {
7387
- operation: op.operation ?? 'and',
7388
- filters: (op.filters ?? []).map((f, j) => ({
7389
- id: `f${j + 1}`,
7390
- field: f.field,
7391
- condition: f.condition,
7392
- // Value-free conditions (is_null, is_empty, …) legitimately omit it.
7393
- ...(f.value !== undefined ? { value: f.value } : {}),
7394
- })),
7395
- },
7396
- };
7397
- }
7398
- if (op.type === 'map') {
7399
- return {
7400
- id,
7401
- type: 'map',
7402
- config: {
7403
- keepOriginalFields: op.keepOriginalFields ?? false,
7404
- mappings: (op.mappings ?? []).map((m, j) => ({
7405
- id: `m${j + 1}`,
7406
- field: m.field,
7407
- transformation: m.transformation ?? 'copy',
7408
- // '' is the runtime's "keep the original name" sentinel.
7409
- renameTo: m.renameTo ?? '',
7410
- })),
7411
- },
7412
- };
7413
- }
7414
- return {
7415
- id,
7416
- type: 'groupBy',
7417
- config: {
7418
- groupByField: op.groupByField,
7419
- aggregations: (op.aggregations ?? []).map((a, j) => ({
7420
- id: `a${j + 1}`,
7421
- // `count` needs no field; the runtime expects the key present anyway.
7422
- field: a.field ?? '',
7423
- operation: a.operation,
7424
- alias: a.alias,
7425
- })),
7426
- },
7427
- };
7428
- });
7429
- return { collection: renameRefs(collectionPath(inputs.collection), rename), operations };
7430
- }
7431
- /**
7432
- * The `collection` string. A bare reference (`v('x')` / `out('s','p')`) is
7433
- * already the path the runtime wants, so it is emitted verbatim. Anything else
7434
- * cannot be expressed as a path — `check` rejects it (TRANSFORM_COLLECTION_*)
7435
- * before serialization, so reaching here with one means `compile` was called
7436
- * without a check; emit the raw text so the failure stays visible downstream
7437
- * rather than becoming a silently empty collection.
7438
- */
7439
- function collectionPath(collection) {
7440
- if (collection instanceof Expr)
7441
- return collection.literal ? JSON.stringify(collection.value) : collection.js;
7442
- return String(collection);
7443
- }
7444
7205
  function actionInputs(spec, rename = NO_RENAME, opts = {}, stepName = '') {
7445
- if (spec.kind === 'transform')
7446
- return transformInputs(spec.inputs, rename);
7447
- if (spec.kind === 'delay')
7448
- return delayInputs(spec.inputs);
7449
7206
  if (spec.kind === 'http') {
7450
7207
  const i = spec.inputs;
7451
7208
  if (i.managed)
@@ -7468,10 +7225,7 @@ function actionInputs(spec, rename = NO_RENAME, opts = {}, stepName = '') {
7468
7225
  ...httpBranchRows(i, rename),
7469
7226
  };
7470
7227
  }
7471
- // A script BODY is a plain string, not an Expr, and it reads step outputs the
7472
- // same way everything else does — so it needs the rename too, or a reused
7473
- // child's script would read the first copy's node.
7474
- return { script: renameRefs(spec.inputs.code, rename) };
7228
+ throw new Error(`internal SDK error: no inputs for action kind "${spec.kind}".`);
7475
7229
  }
7476
7230
  /**
7477
7231
  * `timeout` / `retryCount` — the two request settings BOTH http node types carry.
@@ -7653,60 +7407,3 @@ function httpOutputs(base, returns, managed) {
7653
7407
  };
7654
7408
  return out;
7655
7409
  }
7656
- /**
7657
- * A transform node publishes a COLLECTION — always.
7658
- *
7659
- * All four transform definitions declare `output: {type: 'object', description:
7660
- * 'The transformed data result'}`, which makes the designer type
7661
- * `out('<step>')` as `Record<string, any>` and reject mapping it into an
7662
- * `array` flow output — the same defect as the script node's, one node type
7663
- * over, and it hits five of the shipped examples.
7664
- *
7665
- * Unlike a script this needs no inference and no author declaration, because it
7666
- * is not a property of the particular node: `filter` and `map` are array→array,
7667
- * and `group-by` is too — the engine's own implementation says so in as many
7668
- * words (`transform-operations.ts`: "Always return an array", returning
7669
- * `Array.from(groups.entries()).map(…)` rather than an object keyed by group).
7670
- * So the type is knowable from the node TYPE alone, and is simply written down.
7671
- */
7672
- function transformOutputs(base) {
7673
- // An unrecognized `variant` resolves to no definition at all, so there are no
7674
- // outputs to refine — and `jsonClone(undefined)` throws ("undefined" is not valid
7675
- // JSON) rather than passing the node through.
7676
- if (!base || typeof base !== 'object')
7677
- return base;
7678
- const out = jsonClone(base);
7679
- if (out.output)
7680
- out.output.type = 'array';
7681
- return out;
7682
- }
7683
- /**
7684
- * Declare what a `core.action.script` node actually publishes.
7685
- *
7686
- * Same instance-vs-definition split as `httpOutputs` above, for the same reason
7687
- * one level over: the platform's definition says `output: {type: 'object'}`
7688
- * because a node TYPE cannot know what a particular body returns, so the
7689
- * designer types every script read as `Record<string, any>` and rejects mapping
7690
- * one into a `string` flow output — on a flow that runs. What the body returns
7691
- * is knowable here, at author time, and this is where it gets written down.
7692
- *
7693
- * `returns` wins; otherwise the body is read (see `inferScriptShape`, which
7694
- * answers only when every `return` agrees). When neither says anything the
7695
- * definition's open object stands, byte-identical to what this emitted before.
7696
- */
7697
- function scriptOutputs(base, returns, code) {
7698
- const shape = returns !== undefined ? declaredScriptShape(returns) : inferScriptShape(code);
7699
- // Nothing knowable, or knowably the open object the definition already
7700
- // declares — leave the node exactly as it was.
7701
- if (!shape || (shape.type === 'object' && !shape.properties))
7702
- return base;
7703
- if (!base || typeof base !== 'object')
7704
- return base;
7705
- const out = jsonClone(base);
7706
- if (!out.output)
7707
- return out;
7708
- out.output.type = shape.type;
7709
- if (shape.properties)
7710
- out.output.properties = shape.properties;
7711
- return out;
7712
- }