@uipath/maestro-builder-sdk 6.56.0 → 6.58.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 (75) hide show
  1. package/dist/api-index.md +6 -2
  2. package/dist/api-members.md +1 -1
  3. package/dist/case/subscription.d.ts +4 -2
  4. package/dist/case/subscription.js +5 -2
  5. package/dist/cli/commands/flow.js +2 -0
  6. package/dist/core/actions.js +2 -2
  7. package/dist/core/binding-messages.d.ts +2 -1
  8. package/dist/core/binding-messages.js +3 -4
  9. package/dist/core/byoa.d.ts +3 -2
  10. package/dist/core/byoa.js +3 -3
  11. package/dist/core/carried-definitions.d.ts +0 -18
  12. package/dist/core/carried-definitions.js +3 -2
  13. package/dist/core/ceql.d.ts +2 -1
  14. package/dist/core/ceql.js +3 -2
  15. package/dist/core/compile-refusal.d.ts +21 -0
  16. package/dist/core/compile-refusal.js +26 -0
  17. package/dist/core/connector-inputs.js +2 -1
  18. package/dist/core/connector-resolve-error.d.ts +2 -1
  19. package/dist/core/connector-resolve-error.js +3 -4
  20. package/dist/core/connectors.js +2 -1
  21. package/dist/core/event-filters.js +2 -1
  22. package/dist/core/hitl-routing.d.ts +7 -82
  23. package/dist/core/hitl-routing.js +95 -6
  24. package/dist/core/library.js +4 -3
  25. package/dist/core/lookups.js +6 -5
  26. package/dist/core/nodes/registry.js +3 -2
  27. package/dist/core/nodes/versioned.js +3 -2
  28. package/dist/core/process-resource-sort-order.js +1 -1
  29. package/dist/core/read-text.d.ts +0 -28
  30. package/dist/core/read-text.js +3 -2
  31. package/dist/core/refusals.d.ts +465 -0
  32. package/dist/core/refusals.js +504 -0
  33. package/dist/core/script-function.d.ts +126 -0
  34. package/dist/core/script-function.js +511 -0
  35. package/dist/core/typescript-module.js +2 -1
  36. package/dist/decompile-cli.js +11 -3
  37. package/dist/decompile.d.ts +9 -0
  38. package/dist/decompile.js +76 -3
  39. package/dist/flow-expr-check.d.ts +2 -1
  40. package/dist/flow-expr-check.js +4 -2
  41. package/dist/flow-node-type.d.ts +21 -0
  42. package/dist/flow-sdk.js +17 -16
  43. package/dist/flow-step-tree.js +1 -1
  44. package/dist/flow-variables.js +3 -2
  45. package/dist/load.js +19 -5
  46. package/dist/nodes/agent/flow/inline-agent.js +7 -6
  47. package/dist/nodes/connector/flow/node-type.js +2 -1
  48. package/dist/nodes/data-fabric/flow/factories.js +2 -1
  49. package/dist/nodes/data-fabric/flow/verbs.js +2 -1
  50. package/dist/nodes/delay/flow/factories.js +2 -1
  51. package/dist/nodes/document/flow/ixp.js +5 -1
  52. package/dist/nodes/hitl/flow/check.js +4 -2
  53. package/dist/nodes/hitl/flow/node-type.js +6 -5
  54. package/dist/nodes/http/flow/factories.js +2 -1
  55. package/dist/nodes/http/flow/node-type.js +6 -5
  56. package/dist/nodes/process/flow/factories.js +2 -1
  57. package/dist/nodes/queue/flow/node-type.js +3 -2
  58. package/dist/nodes/script/flow/factories.d.ts +42 -1
  59. package/dist/nodes/script/flow/factories.js +8 -9
  60. package/dist/nodes/script/flow/node-type.js +71 -0
  61. package/dist/nodes/script/flow/runtime.d.ts +20 -0
  62. package/dist/nodes/script/flow/runtime.js +29 -0
  63. package/dist/nodes/script/flow/spec.d.ts +32 -1
  64. package/dist/nodes/transform/flow/node-type.js +2 -1
  65. package/dist/raw-node.js +3 -2
  66. package/dist/ref-resolve.d.ts +0 -36
  67. package/dist/ref-resolve.js +9 -8
  68. package/dist/schema-version.d.ts +0 -38
  69. package/dist/schema-version.js +3 -2
  70. package/dist/script-function-scope.d.ts +57 -0
  71. package/dist/script-function-scope.js +203 -0
  72. package/dist/sdk-resolve.js +2 -1
  73. package/dist/serialize.d.ts +5 -2
  74. package/dist/serialize.js +115 -83
  75. package/package.json +2 -2
package/dist/serialize.js CHANGED
@@ -11,6 +11,7 @@ import { LOOP_DEFAULT_VERSION, bodyUsesBreak, loopDeclaredInputs, loopHasInnerHa
11
11
  import { Expr, toExpr, SCHEDULE_PRESETS, flatNameSegmentsOf, parallelFork } from './flow-sdk.js';
12
12
  import { FlowAction, FlowTrigger } from './core/node-classes.js';
13
13
  import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
14
+ import { CompileRefusal } from './core/compile-refusal.js';
14
15
  import { rawNodeInputProblem, registeredRawType } from './raw-node-advice.js';
15
16
  import { planContributionBindings } from './core/contribution-bindings.js';
16
17
  import { declaredErrorPortRefusal, definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
@@ -219,7 +220,7 @@ function scheduledVersion(inputs, explicit) {
219
220
  const cron = isQuartz(String(inputs?.every ?? ''));
220
221
  if (explicit !== undefined) {
221
222
  if (cron && explicit === '1.1') {
222
- throw new Error("scheduled(): a Quartz cron expression needs the trigger's 1.2 definition, but { version: '1.1' } "
223
+ throw new CompileRefusal('SCHEDULE_CRON_VERSION', "scheduled(): a Quartz cron expression needs the trigger's 1.2 definition, but { version: '1.1' } "
223
224
  + 'was requested. Drop the version or request 1.2.');
224
225
  }
225
226
  return explicit;
@@ -271,6 +272,12 @@ function scheduleInputs(inputs, nodeId) {
271
272
  };
272
273
  }
273
274
  export { CONNECTOR_INPUT_MISSING } from './core/connector-inputs.js';
275
+ /**
276
+ * `DraftCode` and the table's `draft` entries are one list: this fails to
277
+ * type-check when a code is added to one and not the other.
278
+ */
279
+ const DRAFT_CODES_AGREE = true;
280
+ void DRAFT_CODES_AGREE;
274
281
  /**
275
282
  * `serialize`, plus the extra files the flow needs on disk beside it.
276
283
  *
@@ -356,20 +363,20 @@ function emitStickyNotes(built, scope, claimNoteId) {
356
363
  const authorId = note.id ?? `stickyNote_${i + 1}`;
357
364
  const id = authorId;
358
365
  if (taken.has(id)) {
359
- throw new Error(`.stickyNotes(): id "${id}" is already a node in this flow. A sticky note is a real node, `
366
+ throw new CompileRefusal('STICKY_NOTE_ID_TAKEN', `.stickyNotes(): id "${id}" is already a node in this flow. A sticky note is a real node, `
360
367
  + `so its id has to be unique — name it something else.`);
361
368
  }
362
369
  if (seen.has(id))
363
- throw new Error(`.stickyNotes(): duplicate id "${id}".`);
370
+ throw new CompileRefusal('STICKY_NOTE_DUP_ID', `.stickyNotes(): duplicate id "${id}".`);
364
371
  seen.add(id);
365
372
  if (typeof note.content !== 'string') {
366
- throw new Error(`.stickyNotes(): note "${id}" needs a string \`content\`.`);
373
+ throw new CompileRefusal('STICKY_NOTE_NO_CONTENT', `.stickyNotes(): note "${id}" needs a string \`content\`.`);
367
374
  }
368
375
  if (!Number.isFinite(note.x) || !Number.isFinite(note.y)) {
369
- throw new Error(`.stickyNotes(): note "${id}" needs finite x/y coordinates.`);
376
+ throw new CompileRefusal('STICKY_NOTE_POSITION', `.stickyNotes(): note "${id}" needs finite x/y coordinates.`);
370
377
  }
371
378
  if ((note.width === undefined) !== (note.height === undefined)) {
372
- throw new Error(`.stickyNotes(): note "${id}" needs both width and height, or neither.`);
379
+ throw new CompileRefusal('STICKY_NOTE_SIZE', `.stickyNotes(): note "${id}" needs both width and height, or neither.`);
373
380
  }
374
381
  // Node ids are one namespace over the whole file (#938): pack merges every
375
382
  // subflow into one BPMN document. The author's id is kept unless another
@@ -409,7 +416,7 @@ function applyAuthorLayout(layout, scope, where) {
409
416
  ?? scope.noteNodeId?.get(key)
410
417
  ?? (scope.nodes.some((n) => n.id === key) ? key : undefined);
411
418
  if (!id) {
412
- throw new Error(`${where}.layout: "${key}" names no step or node in this flow. Layout keys are the flow's own step names (plus the trigger id and "end").`);
419
+ throw new CompileRefusal('LAYOUT_UNKNOWN_KEY', `${where}.layout: "${key}" names no step or node in this flow. Layout keys are the flow's own step names (plus the trigger id and "end").`);
413
420
  }
414
421
  return id;
415
422
  };
@@ -428,11 +435,11 @@ function applyAuthorLayout(layout, scope, where) {
428
435
  for (const [key, route] of Object.entries(layout.edges ?? {})) {
429
436
  const m = /^(.+?)->(.+)$/.exec(key);
430
437
  if (!m)
431
- throw new Error(`${where}.layout: edge key "${key}" must be "<fromStep>-><toStep>".`);
438
+ throw new CompileRefusal('LAYOUT_EDGE_KEY_FORM', `${where}.layout: edge key "${key}" must be "<fromStep>-><toStep>".`);
432
439
  const [src, tgt] = [resolveId(m[1].trim()), resolveId(m[2].trim())];
433
440
  const matches = scope.edges.filter((e) => e.sourceNodeId === src && e.targetNodeId === tgt);
434
441
  if (matches.length !== 1) {
435
- throw new Error(`${where}.layout: edge "${key}" matches ${matches.length} emitted edges; expected exactly 1.`);
442
+ throw new CompileRefusal('LAYOUT_EDGE_AMBIGUOUS', `${where}.layout: edge "${key}" matches ${matches.length} emitted edges; expected exactly 1.`);
436
443
  }
437
444
  edgesOut[matches[0].id] = {
438
445
  ...(route.waypoints ? { waypoints: withIds(route.waypoints) } : {}),
@@ -668,16 +675,16 @@ export function bundledContainerDefinition(kind, requestedVersion) {
668
675
  */
669
676
  export function resolveContributionDefinition(ref, requestedVersion, where) {
670
677
  if (!ref || typeof ref.nodeType !== 'string' || !ref.nodeType || typeof ref.version !== 'string' || !ref.version) {
671
- throw new Error(`${where}: contribute() returned a DefinitionReference without an exact nodeType + version.`);
678
+ throw new CompileRefusal('CONTRIBUTION_INVALID', `${where}: contribute() returned a DefinitionReference without an exact nodeType + version.`);
672
679
  }
673
680
  if (requestedVersion !== undefined && ref.version !== requestedVersion) {
674
- throw new Error(`${where}: the author requested version ${requestedVersion}, but contribute() returned `
681
+ throw new CompileRefusal('CONTRIBUTION_INVALID', `${where}: the author requested version ${requestedVersion}, but contribute() returned `
675
682
  + `${ref.nodeType}@${ref.version}. A contributor must honor the requested version or throw.`);
676
683
  }
677
684
  if (ref.source === 'bundled') {
678
685
  const def = DEFS[`${ref.nodeType}@${ref.version}`];
679
686
  if (!def) {
680
- throw new Error(`${where}: no bundled definition ${ref.nodeType}@${ref.version}. Bundled references resolve `
687
+ throw new CompileRefusal('CONTRIBUTION_INVALID', `${where}: no bundled definition ${ref.nodeType}@${ref.version}. Bundled references resolve `
681
688
  + `from core-definitions.json only; use a snapshot reference for definitions the SDK does not ship.`);
682
689
  }
683
690
  return def;
@@ -685,22 +692,22 @@ export function resolveContributionDefinition(ref, requestedVersion, where) {
685
692
  if (ref.source === 'snapshot') {
686
693
  const m = ref.manifest;
687
694
  if (!m || typeof m !== 'object') {
688
- throw new Error(`${where}: snapshot reference carries no manifest object.`);
695
+ throw new CompileRefusal('CONTRIBUTION_INVALID', `${where}: snapshot reference carries no manifest object.`);
689
696
  }
690
697
  if (m.nodeType !== ref.nodeType || m.version !== ref.version) {
691
- throw new Error(`${where}: snapshot manifest says ${m.nodeType}@${m.version}, but the reference says `
698
+ throw new CompileRefusal('CONTRIBUTION_INVALID', `${where}: snapshot manifest says ${m.nodeType}@${m.version}, but the reference says `
692
699
  + `${ref.nodeType}@${ref.version}. A snapshot is an exact copy of a real definition.`);
693
700
  }
694
701
  if (typeof m.display?.label !== 'string' || !m.display.label) {
695
- throw new Error(`${where}: snapshot manifest for ${ref.nodeType} lacks display.label — not a valid node manifest.`);
702
+ throw new CompileRefusal('CONTRIBUTION_INVALID', `${where}: snapshot manifest for ${ref.nodeType} lacks display.label — not a valid node manifest.`);
696
703
  }
697
704
  return m;
698
705
  }
699
706
  if (ref.source === 'library') {
700
- throw new Error(`${where}: library-sourced DefinitionReference is not wired yet — it lands with the `
707
+ throw new CompileRefusal('CONTRIBUTION_INVALID', `${where}: library-sourced DefinitionReference is not wired yet — it lands with the `
701
708
  + `connector-tool base classes (coverage design, Phase 5). Use 'bundled' or 'snapshot'.`);
702
709
  }
703
- throw new Error(`${where}: unknown DefinitionReference source "${ref.source}".`);
710
+ throw new CompileRefusal('CONTRIBUTION_INVALID', `${where}: unknown DefinitionReference source "${ref.source}".`);
704
711
  }
705
712
  /**
706
713
  * Whether `def` declares `port` as a forward source handle: an exact id, or a
@@ -811,7 +818,7 @@ function eventDetail(rc, sub, nodeId, opts, site) {
811
818
  const objectName = String(op.objectName ?? '');
812
819
  const noObject = genericEventObjectRefusal(rc, sub, (o) => `onEvent(…, { object: '${o}' })`);
813
820
  if (noObject)
814
- throw new Error(`event node "${nodeId}": ${noObject}`);
821
+ throw new CompileRefusal('EVENT_GENERIC_NO_OBJECT', `event node "${nodeId}": ${noObject}`);
815
822
  // An unresolved `where` lookup is swapped for its recorded id — the id
816
823
  // `prepare --resolve` stored under the EVENT-form node type — or refused.
817
824
  // Same rule as connector inputs: emitting a placeholder for a value nobody
@@ -859,7 +866,7 @@ function eventDetail(rc, sub, nodeId, opts, site) {
859
866
  .map(([k, v]) => {
860
867
  const problem = jmesStringProblem(String(v));
861
868
  if (problem)
862
- throw new Error(`event node "${nodeId}": \`where.${k}\`: ${problem}`);
869
+ throw new CompileRefusal('EVENT_FILTER_BACKSLASH', `event node "${nodeId}": \`where.${k}\`: ${problem}`);
863
870
  return `${k} == ${jmesString(String(v))}`;
864
871
  });
865
872
  // Each authored filter is read structurally (which operator, which value) and
@@ -867,15 +874,22 @@ function eventDetail(rc, sub, nodeId, opts, site) {
867
874
  // measured table. `check` has already refused a malformed leaf; a leaf that
868
875
  // still cannot be read here is an author error, not something to drop silently.
869
876
  const conditions = filters.map((f, i) => {
870
- const { read } = readEventFilter(f);
877
+ const { keys, read } = readEventFilter(f);
871
878
  if (!read) {
872
- throw new Error(`event node "${nodeId}": filters[${i}] must carry a field and exactly ONE operator `
879
+ // The code `check` gives the same leaf (`core/event-checks.ts`).
880
+ const field = f?.field;
881
+ throw new CompileRefusal(typeof field !== 'string' || field === '' ? 'EVENT_FILTER_NO_FIELD'
882
+ : keys.length > 1 ? 'EVENT_FILTER_MANY_OPERATORS' : 'EVENT_FILTER_NO_VALUE', `event node "${nodeId}": filters[${i}] must carry a field and exactly ONE operator `
873
883
  + `(contains, startsWith, endsWith, equals, notEquals, lessThan, lessThanOrEqual, greaterThan, `
874
884
  + `greaterThanOrEqual) — got ${JSON.stringify(f)}.`);
875
885
  }
886
+ // An ambiguous backslash first, under the code `check` gives it
887
+ // (`core/event-checks.ts`); `eventFilterProblem` would report it too.
876
888
  const problem = eventFilterProblem(read);
877
- if (problem)
878
- throw new Error(`event node "${nodeId}": filters[${i}] on \`${read.field}\`: ${problem}`);
889
+ const backslash = typeof read.value === 'string' && jmesStringProblem(read.value) !== undefined;
890
+ if (problem) {
891
+ throw new CompileRefusal(backslash ? 'EVENT_FILTER_BACKSLASH' : 'EVENT_FILTER_BAD_VALUE', `event node "${nodeId}": filters[${i}] on \`${read.field}\`: ${problem}`);
892
+ }
879
893
  return read;
880
894
  });
881
895
  const filterClauses = conditions.map((c) => `(${eventFilterJmes(c)})`);
@@ -986,7 +1000,7 @@ function eventDetail(rc, sub, nodeId, opts, site) {
986
1000
  */
987
1001
  function renderDeep(val, rename = NO_RENAME, rendered) {
988
1002
  if (isLookupToken(val))
989
- throw new Error(misplacedLookupMessage(val));
1003
+ throw new CompileRefusal('LOOKUP_MISPLACED', misplacedLookupMessage(val));
990
1004
  if (val instanceof Expr)
991
1005
  return renderValue(val, rename);
992
1006
  // A value `ctx.render` already rendered is the file's: a `lit`'s object is
@@ -1032,14 +1046,14 @@ function emittableGlobals(built, where, kind = 'root') {
1032
1046
  const shared = sharedInputsOf(built, where, kind);
1033
1047
  const plan = planFlowGlobals({ inputs, outputs: built.outputs, vars: [...built.vars, ...shared.autoVars] });
1034
1048
  if (plan.problems.length) {
1035
- throw new Error(`${where}: ${plan.problems.map((p) => `[${p.code}] ${p.message}`).join(' ')}`);
1049
+ throw new CompileRefusal(plan.problems[0].code, `${where}: ${plan.problems.map((p) => `[${p.code}] ${p.message}`).join(' ')}`);
1036
1050
  }
1037
1051
  // An output the flow sets from inside gets an `inout` Variable to hold the
1038
1052
  // value; the End copies it into the Output (#1092, `flow-output-backing.ts`).
1039
1053
  const sharedOutputs = new Set(shared.targets.filter((t) => t.declaredAs === 'output').map((t) => t.name));
1040
1054
  const backing = planOutputBacking(built, plan.outputs, plan.vars, sharedOutputs);
1041
1055
  if (backing.problems.length) {
1042
- throw new Error(`${where}: ${backing.problems.map((p) => `[${p.code}] ${p.message}`).join(' ')}`);
1056
+ throw new CompileRefusal(backing.problems[0].code, `${where}: ${backing.problems.map((p) => `[${p.code}] ${p.message}`).join(' ')}`);
1043
1057
  }
1044
1058
  return { ...plan, backing: backing.backing, backingReads: backing.reads };
1045
1059
  }
@@ -1052,14 +1066,14 @@ function sharedInputsOf(built, where, kind) {
1052
1066
  if (kind === 'subflow') {
1053
1067
  const marked = built.inputs.filter((i) => i.shared !== undefined);
1054
1068
  if (marked.length) {
1055
- throw new Error(`${where}: [SHARED_INPUT_IN_SUBFLOW] input "${marked[0].name}" has { shared: "${marked[0].shared}" }, but a subflow `
1069
+ throw new CompileRefusal('SHARED_INPUT_IN_SUBFLOW', `${where}: [SHARED_INPUT_IN_SUBFLOW] input "${marked[0].name}" has { shared: "${marked[0].shared}" }, but a subflow `
1056
1070
  + `has one root — its caller — so there is nothing to merge. Read the input with input('${marked[0].name}').`);
1057
1071
  }
1058
1072
  }
1059
1073
  const plan = planSharedInputs({ roots: sharedInputRoots(built), outputs: built.outputs, vars: built.vars });
1060
1074
  const errors = plan.problems.filter((p) => p.level === 'error');
1061
1075
  if (errors.length)
1062
- throw new Error(`${where}: ${errors.map((p) => `[${p.code}] ${p.message}`).join(' ')}`);
1076
+ throw new CompileRefusal(errors[0].code, `${where}: ${errors.map((p) => `[${p.code}] ${p.message}`).join(' ')}`);
1063
1077
  return plan;
1064
1078
  }
1065
1079
  function newScope(outputs, vars = [], backing = new Map(), backingReads = backing) {
@@ -1138,7 +1152,7 @@ function resolveRejoins(scope, addEdge) {
1138
1152
  const id = scope.stepNodeId.get(target) ?? scope.endNodeId.get(target);
1139
1153
  if (!id) {
1140
1154
  const ends = [...scope.endNodeId.keys()];
1141
- throw new Error(`.stepToRef('${target}') names a step this flow does not have. Known steps: `
1155
+ throw new CompileRefusal('REF_UNKNOWN_TARGET', `.stepToRef('${target}') names a step this flow does not have. Known steps: `
1142
1156
  + `${[...scope.stepNodeId.keys()].join(', ') || '(none)'}`
1143
1157
  + `${ends.length ? `; named Ends: ${ends.join(', ')}` : ''}.`);
1144
1158
  }
@@ -1309,7 +1323,7 @@ function jsonSchemaForType(type) {
1309
1323
  case 'jsonSchema': return {};
1310
1324
  default: {
1311
1325
  const unmapped = type;
1312
- throw new Error(`jsonSchemaForType: no agent schema mapping for type '${String(unmapped)}'`);
1326
+ throw new Error(`internal SDK error: jsonSchemaForType: no agent schema mapping for type '${String(unmapped)}'`);
1313
1327
  }
1314
1328
  }
1315
1329
  }
@@ -1522,6 +1536,11 @@ export function serialize(built, opts = {}) {
1522
1536
  * "Node type `core.datafabric.read:1.4` has no matching definition".
1523
1537
  */
1524
1538
  const emittedDefs = new Map();
1539
+ /**
1540
+ * The nodes a draft compile emitted unbound (their `BINDING_MISSING` finding):
1541
+ * they write no binding rows by design, so the placeholder check exempts them.
1542
+ */
1543
+ const unboundDrafts = new Set();
1525
1544
  /**
1526
1545
  * RESOURCE bindings collected while emitting, keyed by binding id — `resource:
1527
1546
  * 'process'` for the rpa family, `resource: 'queue'` for the queue family.
@@ -1558,11 +1577,11 @@ export function serialize(built, opts = {}) {
1558
1577
  function refuseErrorEdgeWithoutPort(nodeId, stepName) {
1559
1578
  const declared = declaredErrorPorts.get(nodeId);
1560
1579
  if (declared?.port === false)
1561
- throw new Error(declaredErrorPortRefusal(stepName, declared.typeName, declared.version));
1580
+ throw new CompileRefusal('ONERROR_NO_ERROR_PORT', declaredErrorPortRefusal(stepName, declared.typeName, declared.version));
1562
1581
  const contributed = contributionDefs.get(nodeId);
1563
1582
  const def = contributed ?? bundledNodeDefs.get(nodeId);
1564
1583
  if (def !== undefined && !definitionHasErrorPort(def)) {
1565
- throw new Error(errorPortRefusal(stepName, def, { bundled: contributed === undefined }));
1584
+ throw new CompileRefusal('ONERROR_NO_ERROR_PORT', errorPortRefusal(stepName, def, { bundled: contributed === undefined }));
1566
1585
  }
1567
1586
  }
1568
1587
  /**
@@ -1639,8 +1658,8 @@ export function serialize(built, opts = {}) {
1639
1658
  const earlier = sidecars.find((f) => f.path === file.path);
1640
1659
  if (earlier !== undefined && JSON.stringify(earlier.json) !== JSON.stringify(file.json)
1641
1660
  && (explicitSource || explicitSidecars.has(file.path))) {
1642
- throw Object.assign(new Error(`two resources would both write "${file.path}" with different content, so the second would replace the first. `
1643
- + 'Give each resource its own `source`, or omit `source` to derive one.'), { code: 'INLINE_AGENT_RESOURCE_SOURCE_DUP' });
1661
+ throw new CompileRefusal('INLINE_AGENT_RESOURCE_SOURCE_DUP', `two resources would both write "${file.path}" with different content, so the second would replace the first. `
1662
+ + 'Give each resource its own `source`, or omit `source` to derive one.');
1644
1663
  }
1645
1664
  if (explicitSource)
1646
1665
  explicitSidecars.add(file.path);
@@ -1735,7 +1754,7 @@ export function serialize(built, opts = {}) {
1735
1754
  const id = safeNodeId(name);
1736
1755
  const end = endIds.get(id);
1737
1756
  if (end !== undefined) {
1738
- throw new Error(end === name
1757
+ throw new CompileRefusal('DUP_END_NAME', end === name
1739
1758
  ? `${where}: [DUP_END_NAME] Two .return({}, { name: '${name}' }) calls ask for the same End with different `
1740
1759
  + `values, label, version or updates. To end several `
1741
1760
  + `paths at one End, keep one .return and end the others with .stepToRef('${name}'); to have separate Ends, `
@@ -1745,7 +1764,7 @@ export function serialize(built, opts = {}) {
1745
1764
  }
1746
1765
  const step = stepIds.get(id);
1747
1766
  if (step !== undefined || reserved.includes(id)) {
1748
- throw new Error(`${where}: [STEP_ID_COLLISION] End "${name}" (.return({}, { name: '${name}' })) has the node id "${id}" of `
1767
+ throw new CompileRefusal('STEP_ID_COLLISION', `${where}: [STEP_ID_COLLISION] End "${name}" (.return({}, { name: '${name}' })) has the node id "${id}" of `
1749
1768
  + `${step !== undefined ? `step "${step}"` : 'the trigger or an entry point'}, and compile will not rename it "${id}2". `
1750
1769
  + 'Give the End a name no step or trigger uses.');
1751
1770
  }
@@ -1753,7 +1772,7 @@ export function serialize(built, opts = {}) {
1753
1772
  }
1754
1773
  for (const [id, name] of endIds) {
1755
1774
  if (!copyPrefix && takenIds.has(id)) {
1756
- throw new Error(`${where}: End "${name}" has node id "${id}", which another node already holds (internal: claimEndIds ran late).`);
1775
+ throw new Error(`internal SDK error: ${where}: End "${name}" has node id "${id}", which another node already holds (claimEndIds ran late).`);
1757
1776
  }
1758
1777
  scope.endNodeId.set(name, claimId(copyPrefix + id));
1759
1778
  }
@@ -1789,7 +1808,7 @@ export function serialize(built, opts = {}) {
1789
1808
  // two Terminates -> end the other paths at one; a step and a Terminate
1790
1809
  // -> rename one.
1791
1810
  const t = first === n ? (terminates.get(n) ?? 0) : 0;
1792
- throw new Error(first !== n
1811
+ throw new CompileRefusal(first !== n ? 'STEP_ID_COLLISION' : 'DUP_STEP', first !== n
1793
1812
  ? `${where}: [STEP_ID_COLLISION] steps "${first}" and "${n}" would both get node id "${safe}". Give them `
1794
1813
  + 'names that differ in letters, digits or "_".'
1795
1814
  : t >= 2 ? `${where}: [DUP_STEP] ${terminateTwiceMessage(n)}`
@@ -1804,7 +1823,7 @@ export function serialize(built, opts = {}) {
1804
1823
  const prefix = copyPrefix !== undefined && [...seen.keys(), ...alsoIds].some((id) => takenIds.has(id)) ? copyPrefix : '';
1805
1824
  for (const [safe, n] of seen) {
1806
1825
  if (!prefix && takenIds.has(safe)) {
1807
- throw new Error(`${where}: [STEP_ID_COLLISION] step "${n}" has the trigger's node id "${safe}", and compile will not `
1826
+ throw new CompileRefusal('STEP_ID_COLLISION', `${where}: [STEP_ID_COLLISION] step "${n}" has the trigger's node id "${safe}", and compile will not `
1808
1827
  + `rename it "${safe}2". Rename the step.`);
1809
1828
  }
1810
1829
  const id = claimId(prefix + safe);
@@ -1825,9 +1844,9 @@ export function serialize(built, opts = {}) {
1825
1844
  if (nodeId === undefined)
1826
1845
  return uid(scope, derivedBase);
1827
1846
  if (nodeId.trim() === '' || takenIds.has(nodeId)) {
1828
- throw Object.assign(new Error(`${where}: \`nodeId\` ${JSON.stringify(nodeId)} `
1847
+ throw new CompileRefusal('INLINE_AGENT_RESOURCE_NODE_ID', `${where}: \`nodeId\` ${JSON.stringify(nodeId)} `
1829
1848
  + (nodeId.trim() === '' ? 'is blank.' : 'is already the id of another node in this flow.')
1830
- + ' Give the resource node an id no other node has, or omit `nodeId` to derive one.'), { code: 'INLINE_AGENT_RESOURCE_NODE_ID' });
1849
+ + ' Give the resource node an id no other node has, or omit `nodeId` to derive one.');
1831
1850
  }
1832
1851
  takenIds.add(nodeId);
1833
1852
  return nodeId;
@@ -1845,11 +1864,11 @@ export function serialize(built, opts = {}) {
1845
1864
  // Unreachable after `claimStepIds`, which refuses a repeated name first;
1846
1865
  // kept so no future emission path can bring back the silent `<name>2`.
1847
1866
  if (scope.stepNodeId.has(stepName)) {
1848
- throw new Error(`step "${stepName}" is emitted twice in one scope (internal: DUP_STEP reached stepUid).`);
1867
+ throw new Error(`internal SDK error: step "${stepName}" is emitted twice in one scope (DUP_STEP reached stepUid).`);
1849
1868
  }
1850
1869
  const id = scope.pending.get(stepName);
1851
1870
  if (id === undefined) {
1852
- throw new Error(`step "${stepName}" has no claimed node id (internal: stepNamesOf missed a step kind).`);
1871
+ throw new Error(`internal SDK error: step "${stepName}" has no claimed node id (stepNamesOf missed a step kind).`);
1853
1872
  }
1854
1873
  scope.pending.delete(stepName);
1855
1874
  scope.stepNodeId.set(stepName, id);
@@ -1886,7 +1905,7 @@ export function serialize(built, opts = {}) {
1886
1905
  const hitlNode = hitlNodes.get(from.node);
1887
1906
  const refused = hitlNode ? hitlExitViolation(hitlNode, from.port, legacyCompletedNodes.has(from.node)) : undefined;
1888
1907
  if (refused)
1889
- throw new Error(`serialize: ${refused}`);
1908
+ throw new CompileRefusal(refused.code, `serialize: ${refused.message}`);
1890
1909
  scope.edges.push({ id: `edge_${edgeSeq++}`, sourceNodeId: from.node, sourcePort: from.port, targetNodeId: toNode, targetPort: toPort });
1891
1910
  }
1892
1911
  let col = 0;
@@ -1914,7 +1933,7 @@ export function serialize(built, opts = {}) {
1914
1933
  const known = o.def ? [] : [...bundledVersions(type), ...carriedVersions];
1915
1934
  const resolvable = known.length > 0 ? known
1916
1935
  : [def?.version].filter((v) => typeof v === 'string');
1917
- throw new Error(`"${label ?? id}" requests ${type}@${o.requestedVersion}, but this SDK resolves only `
1936
+ throw new CompileRefusal('NODE_VERSION_UNREGISTERED', `"${label ?? id}" requests ${type}@${o.requestedVersion}, but this SDK resolves only `
1918
1937
  + `${type}@${resolvable.join(', ') || '<none>'}. Versions are exact — omit { version } to use `
1919
1938
  + `the pinned default, request a version this SDK ships, or carry that version's definition `
1920
1939
  + `(the .flow file's definitions[] entry) with .definitions([...]).`);
@@ -2088,7 +2107,7 @@ export function serialize(built, opts = {}) {
2088
2107
  const eventTriggerRc = built.trigger?.kind === 'event'
2089
2108
  ? (() => {
2090
2109
  if (!opts.library)
2091
- throw new Error(`onEvent() needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
2110
+ throw new CompileRefusal('LIBRARY_REQUIRED', `onEvent() needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
2092
2111
  const sub = built.trigger.subscription;
2093
2112
  const rc = opts.library.resolveEvent('trigger', sub.connector, sub.event, sub.version, sub.object);
2094
2113
  refuseUnverifiedByoa(opts, 'trigger', sub);
@@ -2207,13 +2226,13 @@ export function serialize(built, opts = {}) {
2207
2226
  // own import), whose RawNode predates the check.
2208
2227
  const connectorRefusal = connectorRawNodeRefusal(def.nodeType);
2209
2228
  if (connectorRefusal)
2210
- throw new Error(`${where}: ${connectorRefusal}`);
2229
+ throw new CompileRefusal('RAW_NODE_CONNECTOR', `${where}: ${connectorRefusal}`);
2211
2230
  // An input a registered node type never reads (a Data Fabric `_sortOptions`)
2212
2231
  // is refused for every contribution, whichever class or SDK copy made it:
2213
2232
  // `validate` cannot see it, and the node would ignore it at run time.
2214
2233
  const unknownInput = rawNodeInputProblem(def.nodeType, def.version, contribution.inputs);
2215
2234
  if (unknownInput?.severity === 'error')
2216
- throw new Error(`${where}: ${unknownInput.message}`);
2235
+ throw new CompileRefusal('RAW_NODE_UNKNOWN_INPUT', `${where}: ${unknownInput.message}`);
2217
2236
  const id = stepUid(scope, stepName);
2218
2237
  contributionDefs.set(id, def);
2219
2238
  return placeStepNode({ where, stepName, id, typeName: def.nodeType, requestedVersion, def, contribution, tails, scope, parentId });
@@ -2362,7 +2381,11 @@ export function serialize(built, opts = {}) {
2362
2381
  return out !== null && typeof out === 'object' ? fileCopy(out) : out;
2363
2382
  },
2364
2383
  drafting: opts.draft !== undefined,
2365
- draft: (code, message) => opts.draft?.({ nodeId: id, code: code, message }),
2384
+ draft: (code, message) => {
2385
+ if (code === 'BINDING_MISSING')
2386
+ unboundDrafts.add(id);
2387
+ opts.draft?.({ nodeId: id, code: code, message });
2388
+ },
2366
2389
  };
2367
2390
  const bound = [];
2368
2391
  const contribution = impl.serialize(spec, ctx);
@@ -2478,7 +2501,7 @@ export function serialize(built, opts = {}) {
2478
2501
  // keys and matching expression rewrites. Keep the collision check as an
2479
2502
  // invariant backstop: an overwrite here would drop a complete body.
2480
2503
  if (subflows[id]) {
2481
- throw new Error(`subflow "${id}": internal id allocation collision; two subflow steps claimed the same body slot.`);
2504
+ throw new Error(`internal SDK error: subflow "${id}": id allocation collision; two subflow steps claimed the same body slot.`);
2482
2505
  }
2483
2506
  const node = makeNode(NODE_TYPE.subflow, id, step.name, { parentId, requestedVersion: step.options?.version });
2484
2507
  // Inputs are keyed by the CHILD's input names — that is the whole
@@ -2497,7 +2520,7 @@ export function serialize(built, opts = {}) {
2497
2520
  }
2498
2521
  if (spec.kind === 'waitEvent') {
2499
2522
  if (!opts.library)
2500
- throw new Error(`waitForEvent "${step.name}" needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
2523
+ throw new CompileRefusal('LIBRARY_REQUIRED', `waitForEvent "${step.name}" needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
2501
2524
  const sub = spec.subscription;
2502
2525
  const rc = opts.library.resolveEvent('event', sub.connector, sub.event, sub.version, sub.object);
2503
2526
  refuseUnverifiedByoa(opts, `waitForEvent "${step.name}"`, sub);
@@ -2527,9 +2550,9 @@ export function serialize(built, opts = {}) {
2527
2550
  if (maybeCustom instanceof FlowAction) {
2528
2551
  return emitContribution(maybeCustom, step.name, step.options?.version, tails, scope, parentId);
2529
2552
  }
2530
- throw new Error(`step "${step.name}": not an action spec — pass a factory result, subflow(), or a FlowAction subclass instance.`);
2553
+ throw new CompileRefusal('STEP_NOT_ACTION', `step "${step.name}": not an action spec — pass a factory result, subflow(), or a FlowAction subclass instance.`);
2531
2554
  }
2532
- throw new Error(`step "${step.name}": unknown action kind "${spec.kind}".`);
2555
+ throw new CompileRefusal('STEP_NOT_ACTION', `step "${step.name}": unknown action kind "${spec.kind}".`);
2533
2556
  }
2534
2557
  /**
2535
2558
  * Wire a step's `.onError(...)` handler — the edge out of the node's `error`
@@ -2580,7 +2603,11 @@ export function serialize(built, opts = {}) {
2580
2603
  const node = tails[0]?.node ?? '';
2581
2604
  if (!(hitlNodes.has(node) || legacyCompletedNodes.has(node)) || !tails.some((t) => t.port === port))
2582
2605
  return;
2583
- throw new Error(tails.length > 1
2606
+ // Two conditions, two codes. Several tails: the task does not route per
2607
+ // outcome, which is check's HITL_OUTCOME_PORTS_OFF. One tail: that port
2608
+ // already continues the main path (a routed task's primary outcome, or a
2609
+ // legacyCompleted task's `completed`), and outcomePorts would not help.
2610
+ throw new CompileRefusal(tails.length > 1 ? 'HITL_OUTCOME_PORTS_OFF' : 'HITL_PORT_ALREADY_WIRED', tails.length > 1
2584
2611
  ? `${method}('${port}', …) after human task "${source}": every outcome already continues to the next `
2585
2612
  + `step, '${port}' included, because this task does not route per outcome. Add outcomePorts: true `
2586
2613
  + `(the first outcome then continues and the others are routed), or route every outcome with .stepSwitch.`
@@ -2606,13 +2633,13 @@ export function serialize(built, opts = {}) {
2606
2633
  // from the SAME node. Tails from two nodes mean a new action shape reached
2607
2634
  // here without being taught about side exits; say so rather than guess.
2608
2635
  if (tails.length === 0 || tails.some((t) => t.node !== tails[0].node)) {
2609
- throw new Error(`.stepToList('${step.port}', …) after step "${source}": expected the step to leave from exactly one node, got ${tails.length} tail(s).`);
2636
+ throw new Error(`internal SDK error: .stepToList('${step.port}', …) after step "${source}": expected the step to leave from exactly one node, got ${tails.length} tail(s).`);
2610
2637
  }
2611
2638
  refuseRewire('.stepToList', step.port, source, tails);
2612
2639
  const nodeId = tails[0].node;
2613
2640
  const node = scope.nodes.find((n) => n.id === nodeId);
2614
2641
  if (!node)
2615
- throw new Error(`.stepToList('${step.port}', …) after step "${source}": emitted node "${nodeId}" not found in scope`);
2642
+ throw new Error(`internal SDK error: .stepToList('${step.port}', …) after step "${source}": emitted node "${nodeId}" not found in scope`);
2616
2643
  // Only the error port carries this: without it the platform routes but Studio
2617
2644
  // Web HIDES the handle, so no human can see the handler.
2618
2645
  if (step.port === 'error') {
@@ -2646,7 +2673,7 @@ export function serialize(built, opts = {}) {
2646
2673
  return;
2647
2674
  const nodeId = scope.stepNodeId.get(step.name);
2648
2675
  if (!nodeId)
2649
- throw new Error(`updates on "${step.name}": emitted node not found.`);
2676
+ throw new Error(`internal SDK error: updates on "${step.name}": emitted node not found.`);
2650
2677
  scope.variableUpdates[nodeId] = [
2651
2678
  ...(scope.variableUpdates[nodeId] ?? []),
2652
2679
  ...renderUpdates(updates, scope.rename),
@@ -2674,7 +2701,7 @@ export function serialize(built, opts = {}) {
2674
2701
  const nodeId = scope.stepNodeId.get(step.name);
2675
2702
  const node = nodeId ? scope.nodes.find((n) => n.id === nodeId) : undefined;
2676
2703
  if (!node)
2677
- throw new Error(`label on "${step.name}": emitted node not found.`);
2704
+ throw new Error(`internal SDK error: label on "${step.name}": emitted node not found.`);
2678
2705
  node.display = { ...(node.display ?? {}), label };
2679
2706
  };
2680
2707
  for (const [stepIndex, step] of steps.entries()) {
@@ -2682,7 +2709,7 @@ export function serialize(built, opts = {}) {
2682
2709
  // follow: nothing continues from it, so a step here would hang off no edge.
2683
2710
  const router = lastAction !== undefined && tails.length === 0 ? routerNodes.get(lastAction) : undefined;
2684
2711
  if (router !== undefined && step.kind !== 'stepToList' && step.kind !== 'stepToRef') {
2685
- throw new Error(`step "${lastAction}": its definition declares only templated exits (such as case-{item.id}), so no `
2712
+ throw new CompileRefusal('TEMPLATED_EXITS_ONLY', `step "${lastAction}": its definition declares only templated exits (such as case-{item.id}), so no `
2686
2713
  + `path continues from it and ${'name' in step ? `"${step.name}"` : `.${step.kind}()`} after it would `
2687
2714
  + `have no incoming edge. Route each exit with .stepToList('<port>', …), e.g. .stepToList('case-<id>', …).`);
2688
2715
  }
@@ -2725,7 +2752,7 @@ export function serialize(built, opts = {}) {
2725
2752
  // discover it has one port. `stepExits` carries the reason.
2726
2753
  const exits = stepExits(step.spec, step.options?.version);
2727
2754
  if (!hasExits(exits)) {
2728
- throw new Error(`.stepSwitch("${step.name}"): ${exits.reason}, so there are no exits to route.`
2755
+ throw new CompileRefusal('STEP_SWITCH_NO_EXITS', `.stepSwitch("${step.name}"): ${exits.reason}, so there are no exits to route.`
2729
2756
  + (exits.suggestion ? ` ${exits.suggestion}` : ''));
2730
2757
  }
2731
2758
  const action = {
@@ -2741,7 +2768,7 @@ export function serialize(built, opts = {}) {
2741
2768
  applyLabel(action);
2742
2769
  const nodeId = scope.stepNodeId.get(step.name);
2743
2770
  if (!nodeId)
2744
- throw new Error(`.stepSwitch("${step.name}"): emitted node not found.`);
2771
+ throw new Error(`internal SDK error: .stepSwitch("${step.name}"): emitted node not found.`);
2745
2772
  const armTails = [];
2746
2773
  const routed = new Set();
2747
2774
  // Outcomes of a human task whose path stops at an End: an arm that ends
@@ -2750,7 +2777,7 @@ export function serialize(built, opts = {}) {
2750
2777
  for (const c of step.cases) {
2751
2778
  const exit = exits.find((e) => e.value === c.value);
2752
2779
  if (!exit) {
2753
- throw new Error(`.stepSwitch("${step.name}"): no exit named ${JSON.stringify(c.value)}. `
2780
+ throw new CompileRefusal('STEP_SWITCH_EXIT_UNKNOWN', `.stepSwitch("${step.name}"): no exit named ${JSON.stringify(c.value)}. `
2754
2781
  + `It declares ${exits.map((e) => JSON.stringify(e.value)).join(', ')}.`);
2755
2782
  }
2756
2783
  routed.add(exit.port);
@@ -2886,7 +2913,7 @@ export function serialize(built, opts = {}) {
2886
2913
  // fan-out task's outcomes — must not both enter it: only one fires
2887
2914
  // and the arms never start. Fails closed, as the join does.
2888
2915
  if (tails.length > 1) {
2889
- throw new Error(`.parallel("${step.name}"): its fork "${fork.name}" would receive ${tails.length} alternative edges `
2916
+ throw new CompileRefusal('PARALLEL_FORK_ALTERNATIVES', `.parallel("${step.name}"): its fork "${fork.name}" would receive ${tails.length} alternative edges `
2890
2917
  + `(${tails.map((t) => `${t.node}@${t.port}`).join(', ')}), and only one of them fires while the gateway `
2891
2918
  + `waits for all of them. Add a step before the .parallel so the paths meet there first, or drop { fork }.`);
2892
2919
  }
@@ -2915,13 +2942,13 @@ export function serialize(built, opts = {}) {
2915
2942
  // edge per arm, and still only one arm runs.
2916
2943
  const switchTail = outcomeTail ? undefined : t.find((x) => x.humanSwitch !== undefined);
2917
2944
  if (t.length >= 2 && switchTail) {
2918
- throw new Error(`.parallel("${step.name}"): an arm sends ${t.length} alternative edges into join "${step.name}" — `
2945
+ throw new CompileRefusal('HITL_FAN_OUT_INTO_JOIN', `.parallel("${step.name}"): an arm sends ${t.length} alternative edges into join "${step.name}" — `
2919
2946
  + `among them the end of an arm of human task "${switchTail.humanSwitch}"'s .stepSwitch — and only one `
2920
2947
  + `of them fires, while the join waits for all of them. Add a step at the end of the arm, after the `
2921
2948
  + `.stepSwitch, so its arms meet there first, or move the task out of the .parallel.`);
2922
2949
  }
2923
2950
  if (t.length >= 2 && outcomeTail) {
2924
- throw new Error(`.parallel("${step.name}"): an arm sends ${t.length} alternative edges into join "${step.name}" — `
2951
+ throw new CompileRefusal('HITL_FAN_OUT_INTO_JOIN', `.parallel("${step.name}"): an arm sends ${t.length} alternative edges into join "${step.name}" — `
2925
2952
  + `among them outcome edge "${outcomeTail.port}" of human task "${outcomeTail.node}" — and only one `
2926
2953
  + `of them fires, while the join waits for all of them. Add a step at the end of the arm so its paths `
2927
2954
  + `meet there first, or move the task out of the .parallel.`);
@@ -2930,7 +2957,7 @@ export function serialize(built, opts = {}) {
2930
2957
  }
2931
2958
  const starved = hitlEndExits.slice(endsBefore).find((x) => x.scope === scope && x.parentId === parentId);
2932
2959
  if (starved && armTails.length > 0) {
2933
- throw new Error(`.parallel("${step.name}"): human task "${starved.step}" ${starved.why} inside an arm, and join `
2960
+ throw new CompileRefusal('HITL_END_OUTCOME_IN_PARALLEL', `.parallel("${step.name}"): human task "${starved.step}" ${starved.why} inside an arm, and join `
2934
2961
  + `"${step.name}" would wait forever for the path that continues. Give every outcome a path that `
2935
2962
  + `continues to the end of the arm (drop action: 'End', add the missing .stepSwitch arm, move the `
2936
2963
  + `.return() after the .parallel), or move the task out of the .parallel.`);
@@ -2974,7 +3001,7 @@ export function serialize(built, opts = {}) {
2974
3001
  const loopDef = definitionFor(NODE_TYPE.loop, version);
2975
3002
  const refusal = loopVersionRefusal(step.name, o, usesBreak, loopDef);
2976
3003
  if (refusal)
2977
- throw new Error(refusal);
3004
+ throw new CompileRefusal('LOOP_VERSION_UNSUPPORTED', refusal);
2978
3005
  const v24 = loopHasInnerHandles(version, loopDef);
2979
3006
  const declared = loopDeclaredInputs(loopDef);
2980
3007
  const id = stepUid(scope, step.name);
@@ -3019,7 +3046,7 @@ export function serialize(built, opts = {}) {
3019
3046
  }
3020
3047
  else if (step.kind === 'break') {
3021
3048
  if (!loopExits?.break) {
3022
- throw new Error('.break() has no enclosing .loop()/.doWhile() body to exit — it is only valid inside a container.');
3049
+ throw new CompileRefusal('BREAK_OUTSIDE_LOOP', '.break() has no enclosing .loop()/.doWhile() body to exit — it is only valid inside a container.');
3023
3050
  }
3024
3051
  for (const t of tails)
3025
3052
  addEdge(scope, t, loopExits.node, loopExits.break);
@@ -3030,7 +3057,7 @@ export function serialize(built, opts = {}) {
3030
3057
  // to — so a handler or an arm can go on to the next iteration instead of
3031
3058
  // ending the run on an End of its own (#1129).
3032
3059
  if (!loopExits) {
3033
- throw new Error('.continue() has no enclosing .loop()/.doWhile() body to go back to — it is only valid inside a container.');
3060
+ throw new CompileRefusal('CONTINUE_OUTSIDE_LOOP', '.continue() has no enclosing .loop()/.doWhile() body to go back to — it is only valid inside a container.');
3034
3061
  }
3035
3062
  for (const t of tails)
3036
3063
  addEdge(scope, t, loopExits.node, loopExits.continue);
@@ -3051,7 +3078,7 @@ export function serialize(built, opts = {}) {
3051
3078
  // The remedy depends on what holds the name: another Terminate (end the
3052
3079
  // other paths at it) or a step of another kind (rename one).
3053
3080
  if (scope.stepNodeId.has(step.name)) {
3054
- throw new Error(terminateNames.get(scope)?.has(step.name)
3081
+ throw new CompileRefusal('DUP_STEP', terminateNames.get(scope)?.has(step.name)
3055
3082
  ? terminateTwiceMessage(step.name)
3056
3083
  : terminateStepClashMessage(step.name));
3057
3084
  }
@@ -3088,7 +3115,7 @@ export function serialize(built, opts = {}) {
3088
3115
  const router = tails.length === 0 && lastAction !== undefined ? routerNodes.get(lastAction) : undefined;
3089
3116
  const from = tails[0] ?? (router !== undefined ? { node: router, port: step.port } : undefined);
3090
3117
  if (!from)
3091
- throw new Error(`.stepToRef('${step.port}', '${step.target}') has no step to leave from.`);
3118
+ throw new Error(`internal SDK error: .stepToRef('${step.port}', '${step.target}') has no step to leave from.`);
3092
3119
  refuseRewire('.stepToRef', step.port, lastAction ?? '?', tails);
3093
3120
  if (step.port === 'error') {
3094
3121
  refuseErrorEdgeWithoutPort(from.node, lastAction ?? from.node);
@@ -3121,7 +3148,7 @@ export function serialize(built, opts = {}) {
3121
3148
  const extra = step.options?.updates ?? {};
3122
3149
  for (const name of Object.keys(extra)) {
3123
3150
  if (name in step.values) {
3124
- throw new Error(`.return(): "${name}" appears both as a returned value and in { updates } — one write per variable.`);
3151
+ throw new CompileRefusal('RETURN_UPDATES_SAME_VAR', `.return(): "${name}" appears both as a returned value and in { updates } — one write per variable.`);
3125
3152
  }
3126
3153
  }
3127
3154
  updates.push(...renderUpdates(extra, scope.rename));
@@ -3142,7 +3169,7 @@ export function serialize(built, opts = {}) {
3142
3169
  // (#1231). `check` reports it first as DUP_END_NAME; this is the
3143
3170
  // backstop for `--no-check`.
3144
3171
  if (prior) {
3145
- throw new Error(`Two .return({}, { name: '${name}' }) calls ask for the same End with different values, label, version, updates or scope. `
3172
+ throw new CompileRefusal('DUP_END_NAME', `Two .return({}, { name: '${name}' }) calls ask for the same End with different values, label, version, updates or scope. `
3146
3173
  + `To end several paths at one End, keep one .return and end the others with .stepToRef('${name}'); `
3147
3174
  + `to have separate Ends, give each its own name.`);
3148
3175
  }
@@ -3150,7 +3177,7 @@ export function serialize(built, opts = {}) {
3150
3177
  // `claimEndIds` (#1253); only an unnamed End's id is the SDK's to make up.
3151
3178
  const id = name === undefined ? uid(scope, 'end') : scope.endNodeId.get(name);
3152
3179
  if (id === undefined) {
3153
- throw new Error(`End "${name}" has no claimed node id (internal: namedNodesOf missed a step kind).`);
3180
+ throw new Error(`internal SDK error: End "${name}" has no claimed node id (namedNodesOf missed a step kind).`);
3154
3181
  }
3155
3182
  emitEnd(scope, id, tails, { label: step.options?.label, parentId, requestedVersion: step.options?.version, explicit });
3156
3183
  if (updates.length)
@@ -3205,7 +3232,7 @@ export function serialize(built, opts = {}) {
3205
3232
  const sharedTarget = firstShared && 'name' in firstShared ? root.stepNodeId.get(firstShared.name) : undefined;
3206
3233
  for (const ep of entryPoints) {
3207
3234
  if (takenIds.has(ep.id)) {
3208
- throw new Error(`.entryPoint("${ep.id}"): the id collides with a step, the trigger, or another entry point.`);
3235
+ throw new CompileRefusal('ENTRY_POINT_DUP_ID', `.entryPoint("${ep.id}"): the id collides with a step, the trigger, or another entry point.`);
3209
3236
  }
3210
3237
  takenIds.add(ep.id);
3211
3238
  const spec = ep.trigger;
@@ -3227,7 +3254,7 @@ export function serialize(built, opts = {}) {
3227
3254
  }
3228
3255
  else if (epSpec.kind === 'event') {
3229
3256
  if (!opts.library)
3230
- throw new Error(`onEvent() needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
3257
+ throw new CompileRefusal('LIBRARY_REQUIRED', `onEvent() needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
3231
3258
  const sub = epSpec.subscription;
3232
3259
  const rc = opts.library.resolveEvent('trigger', sub.connector, sub.event, sub.version, sub.object);
3233
3260
  refuseUnverifiedByoa(opts, 'trigger', sub);
@@ -3274,13 +3301,13 @@ export function serialize(built, opts = {}) {
3274
3301
  node.inputs = { entryPointId: stableId(`${built.id}:${ep.id}:entryPoint`) };
3275
3302
  }
3276
3303
  else {
3277
- throw new Error(`.entryPoint("${ep.id}"): unsupported trigger kind ${JSON.stringify(epSpec.kind)}.`);
3304
+ throw new CompileRefusal('TRIGGER_UNKNOWN_KIND', `.entryPoint("${ep.id}"): unsupported trigger kind ${JSON.stringify(epSpec.kind)}.`);
3278
3305
  }
3279
3306
  root.nodes.push(node);
3280
3307
  const prefixTails = emit(ep.steps, [{ node: ep.id, port: 'output' }], root);
3281
3308
  for (const t of prefixTails) {
3282
3309
  if (!sharedTarget) {
3283
- throw new Error(`.entryPoint("${ep.id}") runs into the shared body, but the flow has no shared step to join. `
3310
+ throw new CompileRefusal('ENTRY_POINT_NO_SHARED_BODY', `.entryPoint("${ep.id}") runs into the shared body, but the flow has no shared step to join. `
3284
3311
  + `End the prefix terminally (.return()/.terminate()) or add a shared step.`);
3285
3312
  }
3286
3313
  addEdge(root, t, sharedTarget);
@@ -3366,7 +3393,7 @@ export function serialize(built, opts = {}) {
3366
3393
  // Emitted-document invariants over connections, checked on the FINISHED file
3367
3394
  // so every family and every emission order is covered by one rule, not by
3368
3395
  // each call site remembering it (flow-builder-sdk#865/#866).
3369
- assertBindingPlaceholdersResolve(flow);
3396
+ assertBindingPlaceholdersResolve(flow, unboundDrafts);
3370
3397
  assertFileWideIdsUnique(flow);
3371
3398
  assertConnectorConfigurationRestorable(flow);
3372
3399
  connectionConnectors(flow);
@@ -3413,7 +3440,7 @@ export function assertConnectorConfigurationRestorable(flow) {
3413
3440
  broken.push(node.id);
3414
3441
  }
3415
3442
  if (broken.length) {
3416
- throw new Error(`serialize: connector configuration on ${broken.map((id) => `"${id}"`).join(', ')} has a non-empty `
3443
+ throw new Error(`internal SDK error: serialize: connector configuration on ${broken.map((id) => `"${id}"`).join(', ')} has a non-empty `
3417
3444
  + `optionalConfiguration without a fieldsContainer. Studio Web shows such a node's fields empty and a `
3418
3445
  + `save drops its bodyParameters (flow-builder-sdk#936). Emit { essentialConfiguration } only.`);
3419
3446
  }
@@ -3449,7 +3476,7 @@ export function assertFileWideIdsUnique(flow) {
3449
3476
  }
3450
3477
  }
3451
3478
  if (dupes.length) {
3452
- throw new Error(`serialize: ids must be unique across the root and all subflows; duplicated: ${dupes.join('; ')}.`);
3479
+ throw new Error(`internal SDK error: serialize: ids must be unique across the root and all subflows; duplicated: ${dupes.join('; ')}.`);
3453
3480
  }
3454
3481
  }
3455
3482
  /**
@@ -3474,12 +3501,15 @@ export function assertFileWideIdsUnique(flow) {
3474
3501
  * It holds for real definitions: of the 5,102 in the local registry index
3475
3502
  * (2026-09-30), 4,999 reference a `<bindings.…>` name and every one of them
3476
3503
  * declares that name in `model.bindings.values`, which is what the emitters
3477
- * close. Agent resource definitions are the one exemption (see below).
3504
+ * close. Agent resource definitions are one exemption (see below); `unbound`,
3505
+ * the nodes a draft compile reported as `BINDING_MISSING`, is the other: such a
3506
+ * node names no connection, so it writes no binding rows, and the draft finding
3507
+ * already says so.
3478
3508
  *
3479
3509
  * @throws BindingRefusal `BINDING_PLACEHOLDER_UNRESOLVED`, naming each dangling
3480
3510
  * placeholder and the step that carries it.
3481
3511
  */
3482
- export function assertBindingPlaceholdersResolve(flow) {
3512
+ export function assertBindingPlaceholdersResolve(flow, unbound = new Set()) {
3483
3513
  const bindings = Array.isArray(flow?.bindings) ? flow.bindings : [];
3484
3514
  const defs = new Map();
3485
3515
  for (const def of Array.isArray(flow?.definitions) ? flow.definitions : []) {
@@ -3498,6 +3528,8 @@ export function assertBindingPlaceholdersResolve(flow) {
3498
3528
  // deployed corpus carries no flow `bindings[]` entry for them.
3499
3529
  if (typeof node?.type !== 'string' || node.type.startsWith('uipath.agent.resource.'))
3500
3530
  continue;
3531
+ if (unbound.has(node.id))
3532
+ continue;
3501
3533
  const model = defs.get(`${node.type}@${node.typeVersion}`)?.model;
3502
3534
  if (!Array.isArray(model?.context))
3503
3535
  continue;