@uipath/maestro-builder-sdk 6.66.6 → 6.66.8

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 (41) hide show
  1. package/dist/bindings-v2.d.ts +15 -1
  2. package/dist/bindings-v2.js +29 -6
  3. package/dist/bpmn/check.js +16 -113
  4. package/dist/bpmn/serialize.d.ts +41 -0
  5. package/dist/bpmn/serialize.js +103 -1
  6. package/dist/bpmn/typed-node.d.ts +12 -0
  7. package/dist/bpmn/typed-node.js +24 -0
  8. package/dist/case/check.js +5 -40
  9. package/dist/case/serialize.js +6 -0
  10. package/dist/case/task-shape.d.ts +31 -0
  11. package/dist/case/task-shape.js +48 -0
  12. package/dist/check.js +7 -43
  13. package/dist/core/agent-sidecar.d.ts +13 -3
  14. package/dist/core/agent-sidecar.js +10 -0
  15. package/dist/core/compile-refusal.d.ts +25 -0
  16. package/dist/core/compile-refusal.js +19 -0
  17. package/dist/core/process-resource.d.ts +9 -0
  18. package/dist/core/process-resource.js +4 -0
  19. package/dist/core/refusals.d.ts +47 -44
  20. package/dist/core/refusals.js +47 -44
  21. package/dist/nodes/agent/flow/check.js +14 -42
  22. package/dist/nodes/agent/flow/inline-agent.js +62 -1
  23. package/dist/nodes/agent/flow/spec.d.ts +1 -1
  24. package/dist/nodes/connector/flow/node-type.js +5 -3
  25. package/dist/nodes/data-fabric/flow/check.js +2 -4
  26. package/dist/nodes/data-fabric/flow/shared.d.ts +9 -4
  27. package/dist/nodes/data-fabric/flow/shared.js +14 -4
  28. package/dist/nodes/document/flow/ixp.js +20 -9
  29. package/dist/nodes/hitl/flow/check.js +29 -110
  30. package/dist/nodes/hitl/flow/node-type.js +170 -3
  31. package/dist/nodes/process/flow/check.js +15 -80
  32. package/dist/nodes/process/flow/node-type.js +4 -0
  33. package/dist/nodes/process/flow/refusals.d.ts +8 -0
  34. package/dist/nodes/process/flow/refusals.js +92 -0
  35. package/dist/nodes/queue/flow/check.d.ts +25 -0
  36. package/dist/nodes/queue/flow/check.js +48 -18
  37. package/dist/nodes/queue/flow/node-type.js +13 -1
  38. package/dist/nodes/transform/flow/check.js +31 -43
  39. package/dist/nodes/transform/flow/node-type.js +35 -9
  40. package/dist/serialize.js +172 -20
  41. package/package.json +2 -2
@@ -1,4 +1,3 @@
1
- import { Expr } from '../../../core/expr.js';
2
1
  import { nearestName } from '../../../core/edit-distance.js';
3
2
  import { FORM_FIELD_TYPES, formFieldOptionsProblem, formFieldOptionsWhy, formFieldSettingProblems, formFieldTypeHint, formFieldTypeWhy, validFormFieldTypesText, } from '../../../core/form-field-type.js';
4
3
  import { isAnswerField } from '../../../core/hitl-answers.js';
@@ -85,15 +84,18 @@ export function checkHitl(inputs, ctx) {
85
84
  // Which of the three human-task node types this step emits. The two suffixed
86
85
  // variants differ in exactly one authoring-visible way — who OWNS the form —
87
86
  // and that is what the next four rules are about.
87
+ //
88
+ // What the serializer cannot write faithfully is refused there, and reported
89
+ // from there (`refuseUnwritable` in node-type.ts, #1518): HITL_VARIANT_UNKNOWN,
90
+ // HITL_DOCUMENT_WITHOUT_VARIANT, DOCVAL_NO_EXTRACTION, DOCVAL_TAXONOMY_REQUIRED,
91
+ // DOCVAL_RENDER_UNKNOWN, HITL_APP_WITHOUT_VARIANT, HITL_APP_INCOMPLETE,
92
+ // HITL_APP_SCHEMA_INVALID, HITL_APP_OUTCOME_UNKNOWN, HITL_APP_LOCAL_HAS_FOLDER,
93
+ // HITL_FIELD_NO_ID, HITL_FIELD_VARIABLE, HITL_FIELD_VALUE_NOT_REF,
94
+ // HITL_FIELD_VALUE_NOT_PATH and HITL_OUTCOME_BAD_ID. The rules below still stop
95
+ // where those used to, so a task they refuse gets no follow-on findings.
88
96
  const variant = inputs?.variant;
89
- if (variant !== undefined && variant !== 'quick-form' && variant !== 'action-app'
90
- && variant !== 'document-validation') {
91
- const nearest = nearestName(String(variant), HITL_VARIANTS);
92
- at('HITL_VARIANT_UNKNOWN', `Human task "${step}" names the variant ${JSON.stringify(variant)}, which is not a human-task `
93
- + `node type that resolves on this tenant. Valid: ${HITL_VARIANTS.join(', ')} — or omit it for `
94
- + `the default node type (uipath.human-in-the-loop).`, nearest !== undefined ? `variant: '${nearest}'` : `Omit \`variant\`.`);
95
- return;
96
- }
97
+ if (variant !== undefined && !HITL_VARIANTS.includes(variant))
98
+ return; // HITL_VARIANT_UNKNOWN
97
99
  const isApp = variant === 'action-app';
98
100
  const isDocval = variant === 'document-validation';
99
101
  // The error output is the BASE node's 1.2 definition, which no variant has.
@@ -127,12 +129,8 @@ export function checkHitl(inputs, ctx) {
127
129
  ? 'outcomePorts: false (every outcome continues to the next step), or outcomePorts: true / .stepSwitch (route each one)'
128
130
  : 'Drop `legacyCompleted`: one outcome continues to the next step on its own handle.');
129
131
  }
130
- if (inputs?.document !== undefined && !isDocval) {
131
- at('HITL_DOCUMENT_WITHOUT_VARIANT', `Human task "${step}" names a document to validate, but its variant is `
132
- + `${variant === undefined ? 'the default node type' : `"${variant}"`} — only the validation `
133
- + `station reads it, so it would be emitted and ignored.`, `variant: 'document-validation'`);
134
- return;
135
- }
132
+ if (inputs?.document !== undefined && !isDocval)
133
+ return; // HITL_DOCUMENT_WITHOUT_VARIANT
136
134
  if (isDocval) {
137
135
  if (Array.isArray(inputs?.fields) && inputs.fields.length > 0) {
138
136
  at('HITL_DOCVAL_WITH_FIELDS', `Human task "${step}" is a Document Validation task but also declares ${inputs.fields.length} `
@@ -145,31 +143,12 @@ export function checkHitl(inputs, ctx) {
145
143
  + `has nothing to validate.`, `document: { extractionResult: out('<extractStep>', 'ExtractionResult') }`);
146
144
  return;
147
145
  }
148
- const er = doc.extractionResult;
149
- const erEmpty = er === undefined || er === null || (typeof er === 'string' && !er.trim());
150
- if (erEmpty) {
151
- at('DOCVAL_NO_EXTRACTION', `Human task "${step}" gives the validation station no extractionResult. Bind the `
152
- + `ExtractionResult output of the upstream extract step — the platform's own validator `
153
- + `rejects the node without it.`, `extractionResult: out('<extractStep>', 'ExtractionResult')`);
154
- }
155
- if (doc.render === 'custom') {
156
- const tx = doc.taxonomy;
157
- if (tx === undefined || tx === null || (typeof tx === 'string' && !tx.trim())) {
158
- at('DOCVAL_TAXONOMY_REQUIRED', `Human task "${step}" renders validation in a custom app (render: 'custom') but names no `
159
- + `taxonomy — the platform requires one for the app's Validation Control.`, `taxonomy: out('<classifyStep>', 'taxonomy') // or the taxonomy id string`);
160
- }
161
- }
162
- else if (doc.render !== undefined && doc.render !== 'standard') {
163
- at('DOCVAL_RENDER_UNKNOWN', `Human task "${step}" gives the validation render type ${JSON.stringify(doc.render)}. Valid: `
164
- + `'standard' (the native station, the default) or 'custom' (an app with a Validation Control).`, `render: 'standard'`);
165
- }
146
+ // Its extractionResult, render and taxonomy are the serializer's
147
+ // DOCVAL_NO_EXTRACTION, DOCVAL_RENDER_UNKNOWN and DOCVAL_TAXONOMY_REQUIRED.
166
148
  // No inline form — skip the field rules; outcomes still route, fall through.
167
149
  }
168
150
  if (!isApp && !(isDocval && inputs?.document?.render === 'custom') && inputs?.app !== undefined) {
169
- at('HITL_APP_WITHOUT_VARIANT', `Human task "${step}" names a deployed app, but its variant is `
170
- + `${variant === undefined ? 'the default node type' : `"${variant}"`}, whose form lives in the `
171
- + `flow. The app would be emitted and then ignored — the reviewer still gets the inline form.`, `variant: 'action-app'`);
172
- return;
151
+ return; // HITL_APP_WITHOUT_VARIANT
173
152
  }
174
153
  if (isApp) {
175
154
  // The app owns the form, so `fields` is not merely unnecessary here — it is a
@@ -186,25 +165,11 @@ export function checkHitl(inputs, ctx) {
186
165
  + `App — but names no app, so nothing tells the platform which form to show.`, `app: { name: 'Expense Review', key: '<guid>', folderPath: 'Shared' }`);
187
166
  return;
188
167
  }
189
- // An in-solution app has no folder to author. A published one does: with
190
- // none it is serialized as an inline app, and on a serializer before
191
- // flow-v1 #4076 the task loses its folderPath context and fails at run
192
- // time (170004, "Invalid input for the HITL task").
168
+ // A missing name, key or folder is the serializer's HITL_APP_INCOMPLETE.
193
169
  const inSolution = app.location === 'in-solution';
194
170
  for (const k of inSolution ? ['name', 'key'] : ['name', 'key', 'folderPath']) {
195
- if (typeof app[k] !== 'string' || !app[k].trim()) {
196
- at('HITL_APP_INCOMPLETE', inSolution
197
- ? `Human task "${step}" names an in-solution Action App with no ${k}. An in-solution app is `
198
- + `resolved by its key (\`resource.key\` in resources/solution_folder/app/codedAction/<App>.json), `
199
- + `with the name shown to the reviewer — both are needed for the platform to find it.`
200
- : `Human task "${step}" names an Action App with no ${k}. An app is resolved by its key plus `
201
- + `the folder it is deployed in, with the name shown to the reviewer — all three are needed `
202
- + `for the platform to find it. If the app is in the same solution as the flow, set `
203
- + `\`location: 'in-solution'\` instead of a folder.`, inSolution
204
- ? `app: { name: 'InvoiceReview', key: '<resource.key>', location: 'in-solution' }`
205
- : `app: { name: 'Expense Review', key: '<guid>', folderPath: 'Shared' }`);
171
+ if (typeof app[k] !== 'string' || !app[k].trim())
206
172
  return;
207
- }
208
173
  }
209
174
  // The app's action schema: what types its inputs, and what it answers.
210
175
  const bound = Object.keys(app.inputs ?? {});
@@ -218,10 +183,7 @@ export function checkHitl(inputs, ctx) {
218
183
  }
219
184
  }
220
185
  else if (typeof contract === 'string') {
221
- at('HITL_APP_SCHEMA_INVALID', `Human task "${step}" gives its Action App a schema that is not usable: ${contract}. `
222
- + `Pass the app's action-schema.json verbatim, or the platform's action schema (its `
223
- + `inputs/outputs/inOuts/outcomes arrays).`);
224
- return;
186
+ return; // HITL_APP_SCHEMA_INVALID
225
187
  }
226
188
  else {
227
189
  const names = contract.inputs;
@@ -237,22 +199,8 @@ export function checkHitl(inputs, ctx) {
237
199
  at('HITL_APP_INPUT_REQUIRED', `Human task "${step}" leaves the Action App's required input(s) ${missing.join(', ')} unbound, `
238
200
  + `so the task's arguments fail the app's own schema.`, `inputs: { ${missing.map((m) => `${JSON.stringify(m)}: …`).join(', ')} }`);
239
201
  }
240
- if (contract.outcomes.length > 0) {
241
- for (const o of Array.isArray(inputs?.outcomes) ? inputs.outcomes : []) {
242
- const name = typeof o === 'string' ? o : o?.name;
243
- if (typeof name === 'string' && name !== '' && !contract.outcomes.includes(name)) {
244
- const nearest = nearestName(name, contract.outcomes);
245
- at('HITL_APP_OUTCOME_UNKNOWN', `Human task "${step}" routes on the outcome "${name}", but its Action App completes only with `
246
- + `${contract.outcomes.join(', ')}. The reviewer can never pick it.`, nearest !== undefined ? `'${nearest}'` : undefined);
247
- }
248
- }
249
- }
250
- }
251
- if (inSolution && app.folderPath !== undefined && app.folderPath !== '') {
252
- at('HITL_APP_LOCAL_HAS_FOLDER', `Human task "${step}" names an in-solution Action App with folderPath ${JSON.stringify(app.folderPath)}. `
253
- + `An in-solution app has no folder — it is emitted with the empty folder the platform's app picker `
254
- + `writes for an app inline to the solution — so the value would be dropped. Remove it, or drop `
255
- + `\`location: 'in-solution'\` to call an app published to that folder.`, `Delete \`folderPath\`.`);
202
+ // An outcome the app does not declare is the serializer's HITL_APP_OUTCOME_UNKNOWN,
203
+ // and an in-solution app's folderPath its HITL_APP_LOCAL_HAS_FOLDER.
256
204
  }
257
205
  // Outcomes still matter for an app task: the flow's own routing and its
258
206
  // `Action`/`status` outputs are derived from them, so fall through to the
@@ -268,14 +216,10 @@ export function checkHitl(inputs, ctx) {
268
216
  }
269
217
  const seen = new Set();
270
218
  let answers = 0;
271
- fields.forEach((f, i) => {
272
- const where = `fields[${i}]`;
219
+ fields.forEach((f) => {
273
220
  const id = typeof f?.id === 'string' ? f.id.trim() : '';
274
- if (!id) {
275
- at('HITL_FIELD_NO_ID', `Human task "${step}" has a form field with no id (${where}). The id is the key the `
276
- + `reviewer's answer comes back under, so a field without one is dropped.`, `id: 'amount'`);
277
- return;
278
- }
221
+ if (!id)
222
+ return; // HITL_FIELD_NO_ID
279
223
  if (seen.has(id)) {
280
224
  at('HITL_DUP_FIELD', `Human task "${step}" declares the form field "${id}" twice. The answer object is keyed `
281
225
  + `by field id, so the second silently replaces the first.`, `Rename one of them.`);
@@ -299,20 +243,8 @@ export function checkHitl(inputs, ctx) {
299
243
  for (const problem of formFieldSettingProblems(f)) {
300
244
  at('HITL_FIELD_SETTING_INVALID', `Human task "${step}" gives the form field "${id}" ${problem}. The platform refuses the form.`);
301
245
  }
302
- // The landing variable is read as `$vars.<variable>`, so it has to be one
303
- // identifier — not a path, not a `vars.` prefix — and only an asked field
304
- // has one: a shown field's row is written without it (#1309).
305
- const variable = f?.variable;
306
- if (variable !== undefined) {
307
- if (f?.direction !== 'output') {
308
- at('HITL_FIELD_VARIABLE', `Human task "${step}"'s form field "${id}" sets \`variable\` on a ${f?.direction === 'inOut' ? 'pre-filled' : 'shown'} field. `
309
- + `Only an asked field (direction 'output') lands its answer in a variable; the SDK writes the row without one.`, 'Drop `variable`, or make the field direction: \'output\'.');
310
- }
311
- else if (typeof variable !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(variable)) {
312
- at('HITL_FIELD_VARIABLE', `Human task "${step}"'s form field "${id}" has variable ${JSON.stringify(variable)}, which is not one identifier. `
313
- + `It is read as $vars.<variable>, so give the name only — no \`vars.\` prefix, no dots.`, typeof variable === 'string' && variable.startsWith('vars.') ? `variable: '${variable.slice(5)}'` : `variable: '${step}_${id}'`);
314
- }
315
- }
246
+ // A `variable` on a row that has none, or one that is no identifier, is the
247
+ // serializer's HITL_FIELD_VARIABLE (#1309).
316
248
  if (isAnswerField(f))
317
249
  answers++; // an `output`, or an `inOut` whose reviewed value comes back
318
250
  if (f?.direction === 'input' || f?.direction === 'inOut') {
@@ -321,20 +253,8 @@ export function checkHitl(inputs, ctx) {
321
253
  at('HITL_FIELD_NO_VALUE', `Human task "${step}" shows the form field "${id}" to the reviewer but gives it no value, `
322
254
  + `so the field renders empty — the flow says "show them this" and shows them nothing.`, `value: input('<flowInput>') // or out('<step>', '<field>')`);
323
255
  }
324
- else if (!(value instanceof Expr)) {
325
- at('HITL_FIELD_VALUE_NOT_REF', `Human task "${step}" gives the form field "${id}" a raw `
326
- + `${Array.isArray(value) ? 'array' : typeof value} as its value. A shown field is filled `
327
- + `from a variable PATH, not a constant — the platform reads the string as a lookup, so a `
328
- + `literal resolves to nothing on the form.`, `value: input('<flowInput>')`);
329
- }
330
- else if (value.literal || !/^\$vars\.[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(value.js.trim())) {
331
- // Same trap as a transform's `collection`: a computed expression resolves
332
- // locally and to nothing once deployed, where it is used as a path.
333
- at('HITL_FIELD_VALUE_NOT_PATH', `Human task "${step}" fills the form field "${id}" from the expression `
334
- + `"${value.literal ? String(value.value) : value.js.trim().slice(0, 60)}". A field binding is `
335
- + `a variable PATH, so a computed expression resolves to nothing on the deployed form. `
336
- + `Compute it in a script step first and point the field at that step's output.`, `value: out('<scriptStep>')`);
337
- }
256
+ // A value that is no `$vars.` path is the serializer's HITL_FIELD_VALUE_NOT_REF
257
+ // (a constant) or HITL_FIELD_VALUE_NOT_PATH (an expression).
338
258
  }
339
259
  else if (f?.direction !== 'output') {
340
260
  at('HITL_FIELD_DIRECTION', `Human task "${step}" gives the form field "${id}" the direction `
@@ -359,8 +279,7 @@ export function checkHitl(inputs, ctx) {
359
279
  const id = outcomeIdOf(o, i);
360
280
  const explicitId = typeof o === 'object' && o !== null ? o.id : undefined;
361
281
  if (explicitId !== undefined && (typeof explicitId !== 'string' || explicitId === '')) {
362
- at('HITL_OUTCOME_BAD_ID', `Human task "${step}" outcome ${JSON.stringify(name ?? i)} has an id that is not a non-empty string. `
363
- + `Leave \`id\` out to use the slug of the name.`, `{ name: ${JSON.stringify(name ?? 'Approve')} }`);
282
+ // The serializer's HITL_OUTCOME_BAD_ID; the id it would get is no clash to report.
364
283
  }
365
284
  else if (ids.has(id) && ids.get(id) !== String(name ?? '')) {
366
285
  // Same name, same id: HITL_DUP_OUTCOME below reports it once, as a name clash.
@@ -17,9 +17,10 @@ import { formFieldJsonSchema, formFieldRowLoss, formFieldRowLossMessage, formFie
17
17
  import { isAnswerField } from '../../../core/hitl-answers.js';
18
18
  import { OUTCOME_HANDLE_NODE_TYPES, hitlExitModel, outcomeIdOf, outcomeSlug } from '../../../core/hitl-routing.js';
19
19
  import { jsonClone } from '../../../core/json-clone.js';
20
+ import { nearestName } from '../../../core/edit-distance.js';
20
21
  import { schemaId } from '../../../core/stable-id.js';
21
22
  import { wireExprBody } from '../../../core/wire-expr.js';
22
- import { appAnswerSchemas, appInputSchema, liftActionSchema, normalizeActionSchema } from './action-schema.js';
23
+ import { appAnswerSchemas, appContract, appInputSchema, liftActionSchema, normalizeActionSchema } from './action-schema.js';
23
24
  import { checkHitl, checkHitlRead } from './check.js';
24
25
  import { CompileRefusal } from '../../../core/compile-refusal.js';
25
26
  import { processResourceBindings } from '../../../core/process-resource.js';
@@ -287,7 +288,7 @@ function appBindingExpr(value, ctx) {
287
288
  * A shown field's `binding`: the platform resolves it as a path against the
288
289
  * workflow's variables (`vars.<flowInput>`, `vars.<node>.output.<field>`), so the
289
290
  * `$vars.` prefix our expressions carry is dropped. A computed expression cannot
290
- * be a path — `check` rejects one (HITL_FIELD_VALUE_NOT_PATH) before we get here.
291
+ * be a path — `refuseUnwritable` refuses one (HITL_FIELD_VALUE_NOT_PATH) before we get here.
291
292
  */
292
293
  function bindingPath(value, ctx) {
293
294
  if (!(value instanceof Expr))
@@ -300,6 +301,171 @@ function bindingPath(value, ctx) {
300
301
  // `__hitlInputBindings` the designer reads back stores the binding verbatim.
301
302
  return path.startsWith('vars.') ? path : `vars.${path}`;
302
303
  }
304
+ /**
305
+ * The human-task shapes this serializer cannot write faithfully, refused while
306
+ * the node is emitted (#1518). `compile` no longer runs `check` (#1497), so
307
+ * without these each one compiled clean into a `.flow` that dropped, rewrote or
308
+ * invented what the author wrote. For instance an unknown `variant: 'quik-form'`
309
+ * became a plain base node, `document: {}` became the extraction result
310
+ * `"undefined"`, and an asked field with no id landed in `vars.undefined`.
311
+ * `check` reports the same refusals, on the step, from this one place (collect
312
+ * mode). Every condition each code names is refused, the lossy shape and the
313
+ * rest alike: the rest are tasks the platform cannot run as written (a blank
314
+ * extraction result, an app it cannot find, a computed field binding that
315
+ * resolves to nothing), and `compile` refused all of them before #1497.
316
+ * The checks run in `checkHitl`'s order, so the first refusal is the one
317
+ * `check` used to report first.
318
+ */
319
+ function refuseUnwritable(inputs, typeName, step) {
320
+ const variant = inputs?.variant;
321
+ const isApp = typeName === ACTION_APP;
322
+ const isDocval = typeName === DOC_VALIDATION;
323
+ const variantText = variant === undefined ? 'the default node type' : `"${variant}"`;
324
+ // An unknown variant resolves to the base node type (`matches`), so the
325
+ // variant the author named would leave no trace in the `.flow`.
326
+ if (variant !== undefined && !VARIANTS.includes(variant)) {
327
+ const nearest = nearestName(String(variant), [...VARIANTS]);
328
+ throw new CompileRefusal('HITL_VARIANT_UNKNOWN', `Human task "${step}" names the variant ${JSON.stringify(variant)}, which is not a human-task `
329
+ + `node type that resolves on this tenant. Valid: ${VARIANTS.join(', ')} — or omit it for `
330
+ + `the default node type (uipath.human-in-the-loop).`, nearest !== undefined ? `variant: '${nearest}'` : `Omit \`variant\`.`);
331
+ }
332
+ // Only the station's node type writes `document` (`docvalInputs`).
333
+ if (inputs?.document !== undefined && !isDocval) {
334
+ throw new CompileRefusal('HITL_DOCUMENT_WITHOUT_VARIANT', `Human task "${step}" names a document to validate, but its variant is ${variantText} — only the validation `
335
+ + `station reads it, so it would be emitted and ignored.`, `variant: 'document-validation'`);
336
+ }
337
+ const doc = inputs?.document;
338
+ if (isDocval && doc && typeof doc === 'object') {
339
+ // A missing one was written as the text "undefined" (or "null").
340
+ const er = doc.extractionResult;
341
+ if (er === undefined || er === null || (typeof er === 'string' && !er.trim())) {
342
+ throw new CompileRefusal('DOCVAL_NO_EXTRACTION', `Human task "${step}" gives the validation station no extractionResult. Bind the `
343
+ + `ExtractionResult output of the upstream extract step — the platform's own validator `
344
+ + `rejects the node without it.`, `extractionResult: out('<extractStep>', 'ExtractionResult')`);
345
+ }
346
+ const render = doc.render;
347
+ if (render === 'custom') {
348
+ // A null taxonomy was written as the text "null".
349
+ const tx = doc.taxonomy;
350
+ if (tx === undefined || tx === null || (typeof tx === 'string' && !tx.trim())) {
351
+ throw new CompileRefusal('DOCVAL_TAXONOMY_REQUIRED', `Human task "${step}" renders validation in a custom app (render: 'custom') but names no `
352
+ + `taxonomy — the platform requires one for the app's Validation Control.`, `taxonomy: out('<classifyStep>', 'taxonomy') // or the taxonomy id string`);
353
+ }
354
+ }
355
+ else if (render !== undefined && render !== 'standard') {
356
+ // A null render was written as 'standard'.
357
+ throw new CompileRefusal('DOCVAL_RENDER_UNKNOWN', `Human task "${step}" gives the validation render type ${JSON.stringify(render)}. Valid: `
358
+ + `'standard' (the native station, the default) or 'custom' (an app with a Validation Control).`, `render: 'standard'`);
359
+ }
360
+ }
361
+ // Only an app task and a custom-render station write `app` (`emitsApp`).
362
+ if (!isApp && !(isDocval && doc?.render === 'custom') && inputs?.app !== undefined) {
363
+ throw new CompileRefusal('HITL_APP_WITHOUT_VARIANT', `Human task "${step}" names a deployed app, but its variant is ${variantText}, whose form lives in the `
364
+ + `flow. The app would be emitted and then ignored — the reviewer still gets the inline form.`, `variant: 'action-app'`);
365
+ }
366
+ const app = inputs?.app;
367
+ if (isApp && app && typeof app === 'object') {
368
+ // A published app with no folder was written with folderPath '', byte for
369
+ // byte an in-solution app.
370
+ const inSolution = app.location === 'in-solution';
371
+ for (const k of inSolution ? ['name', 'key'] : ['name', 'key', 'folderPath']) {
372
+ if (typeof app[k] !== 'string' || !app[k].trim()) {
373
+ throw new CompileRefusal('HITL_APP_INCOMPLETE', inSolution
374
+ ? `Human task "${step}" names an in-solution Action App with no ${k}. An in-solution app is `
375
+ + `resolved by its key (\`resource.key\` in resources/solution_folder/app/codedAction/<App>.json), `
376
+ + `with the name shown to the reviewer — both are needed for the platform to find it.`
377
+ : `Human task "${step}" names an Action App with no ${k}. An app is resolved by its key plus `
378
+ + `the folder it is deployed in, with the name shown to the reviewer — all three are needed `
379
+ + `for the platform to find it. If the app is in the same solution as the flow, set `
380
+ + `\`location: 'in-solution'\` instead of a folder.`, inSolution
381
+ ? `app: { name: 'InvoiceReview', key: '<resource.key>', location: 'in-solution' }`
382
+ : `app: { name: 'Expense Review', key: '<guid>', folderPath: 'Shared' }`);
383
+ }
384
+ }
385
+ // An unusable actionSchema was dropped; with an inputSchema too, the
386
+ // inputSchema was replaced by the lowered actionSchema.
387
+ const contract = appContract(app);
388
+ if (typeof contract === 'string') {
389
+ throw new CompileRefusal('HITL_APP_SCHEMA_INVALID', `Human task "${step}" gives its Action App a schema that is not usable: ${contract}. `
390
+ + `Pass the app's action-schema.json verbatim, or the platform's action schema (its `
391
+ + `inputs/outputs/inOuts/outcomes arrays).`);
392
+ }
393
+ // The schema's outcome list is not written, so the mismatch would be gone.
394
+ if (contract !== undefined && contract.outcomes.length > 0) {
395
+ for (const o of Array.isArray(inputs?.outcomes) ? inputs.outcomes : []) {
396
+ const name = typeof o === 'string' ? o : o?.name;
397
+ if (typeof name === 'string' && name !== '' && !contract.outcomes.includes(name)) {
398
+ const nearest = nearestName(name, contract.outcomes);
399
+ throw new CompileRefusal('HITL_APP_OUTCOME_UNKNOWN', `Human task "${step}" routes on the outcome "${name}", but its Action App completes only with `
400
+ + `${contract.outcomes.join(', ')}. The reviewer can never pick it.`, nearest !== undefined ? `'${nearest}'` : undefined);
401
+ }
402
+ }
403
+ }
404
+ // An in-solution app is written with folderPath '' whatever the source says.
405
+ if (inSolution && app.folderPath !== undefined && app.folderPath !== '') {
406
+ throw new CompileRefusal('HITL_APP_LOCAL_HAS_FOLDER', `Human task "${step}" names an in-solution Action App with folderPath ${JSON.stringify(app.folderPath)}. `
407
+ + `An in-solution app has no folder — it is emitted with the empty folder the platform's app picker `
408
+ + `writes for an app inline to the solution — so the value would be dropped. Remove it, or drop `
409
+ + `\`location: 'in-solution'\` to call an app published to that folder.`, `Delete \`folderPath\`.`);
410
+ }
411
+ }
412
+ // An app task or a validation station drops `fields` (check's
413
+ // HITL_APP_WITH_FIELDS / HITL_DOCVAL_WITH_FIELDS), so its rows are never
414
+ // written and the field rules do not apply — matching `checkHitl`.
415
+ const fields = !isApp && !isDocval && Array.isArray(inputs?.fields) ? inputs.fields : [];
416
+ fields.forEach((f, i) => {
417
+ // An asked field with no id landed in `vars.undefined`.
418
+ const id = typeof f?.id === 'string' ? f.id.trim() : '';
419
+ if (!id) {
420
+ throw new CompileRefusal('HITL_FIELD_NO_ID', `Human task "${step}" has a form field with no id (fields[${i}]). The id is the key the `
421
+ + `reviewer's answer comes back under, so a field without one is dropped.`, `id: 'amount'`);
422
+ }
423
+ // Only an asked row is written with a `variable`, as `vars.<variable>`.
424
+ const variable = f.variable;
425
+ if (variable !== undefined) {
426
+ if (f.direction !== 'output') {
427
+ throw new CompileRefusal('HITL_FIELD_VARIABLE', `Human task "${step}"'s form field "${id}" sets \`variable\` on a ${f.direction === 'inOut' ? 'pre-filled' : 'shown'} field. `
428
+ + `Only an asked field (direction 'output') lands its answer in a variable; the SDK writes the row without one.`, 'Drop `variable`, or make the field direction: \'output\'.');
429
+ }
430
+ if (typeof variable !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(variable)) {
431
+ throw new CompileRefusal('HITL_FIELD_VARIABLE', `Human task "${step}"'s form field "${id}" has variable ${JSON.stringify(variable)}, which is not one identifier. `
432
+ + `It is read as $vars.<variable>, so give the name only — no \`vars.\` prefix, no dots.`, typeof variable === 'string' && variable.startsWith('vars.') ? `variable: '${variable.slice(5)}'` : `variable: '${step}_${id}'`);
433
+ }
434
+ }
435
+ // A shown row's value is written as a variable path (`bindingPath`): a
436
+ // constant `'hello'` became the path `hello`, and `lit('hello')` or an
437
+ // expression not rooted in `$vars.` became `vars.<it>`.
438
+ if (f.direction === 'input' || f.direction === 'inOut') {
439
+ const value = f.value;
440
+ if (value === undefined || value === null)
441
+ return; // check's HITL_FIELD_NO_VALUE
442
+ if (!(value instanceof Expr)) {
443
+ throw new CompileRefusal('HITL_FIELD_VALUE_NOT_REF', `Human task "${step}" gives the form field "${id}" a raw `
444
+ + `${Array.isArray(value) ? 'array' : typeof value} as its value. A shown field is filled `
445
+ + `from a variable PATH, not a constant — the platform reads the string as a lookup, so a `
446
+ + `literal resolves to nothing on the form. If the value already is an \`input(...)\` or `
447
+ + `\`out(...)\` reference, two copies of the SDK are loaded: an expression built by the other `
448
+ + `copy is not recognised as one — import every helper from one SDK install.`, `value: input('<flowInput>')`);
449
+ }
450
+ if (value.literal || !/^\$vars\.[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(value.js.trim())) {
451
+ throw new CompileRefusal('HITL_FIELD_VALUE_NOT_PATH', `Human task "${step}" fills the form field "${id}" from the expression `
452
+ + `"${value.literal ? String(value.value) : value.js.trim().slice(0, 60)}". A field binding is `
453
+ + `a variable PATH, so a computed expression resolves to nothing on the deployed form. `
454
+ + `Compute it in a script step first and point the field at that step's output.`, `value: out('<scriptStep>')`);
455
+ }
456
+ }
457
+ });
458
+ // An id that is not a non-empty string was replaced by the name's slug (`outcomeIdOf`).
459
+ const outcomes = Array.isArray(inputs?.outcomes) ? inputs.outcomes : [];
460
+ outcomes.forEach((o, i) => {
461
+ const explicitId = typeof o === 'object' && o !== null ? o.id : undefined;
462
+ if (explicitId !== undefined && (typeof explicitId !== 'string' || explicitId === '')) {
463
+ const name = (typeof o === 'string' ? o : o?.name)?.trim();
464
+ throw new CompileRefusal('HITL_OUTCOME_BAD_ID', `Human task "${step}" outcome ${JSON.stringify(name ?? i)} has an id that is not a non-empty string. `
465
+ + `Leave \`id\` out to use the slug of the name.`, `{ name: ${JSON.stringify(name ?? 'Approve')} }`);
466
+ }
467
+ });
468
+ }
303
469
  /**
304
470
  * The exits `.stepSwitch` routes over, or why the task has none — read off the
305
471
  * same model the task's tails come from.
@@ -639,6 +805,7 @@ function hitlAt(typeName, version, factory) {
639
805
  throw new CompileRefusal('HITL_EXPOSE_ERROR_VERSION', `"${ctx.step}": exposeError needs the task's 1.2 definition, but { version: '${pinned}' } pins it. `
640
806
  + `Drop the pin or match it.`);
641
807
  }
808
+ refuseUnwritable(inputs, typeName, ctx.step);
642
809
  const { inputs: nodeInputs, outputs } = hitlInputs(inputs, typeName, ctx);
643
810
  // The definition's `outputDefinition` gives the two output SLOTS; the form
644
811
  // gives their shape (see hitlInputs), so the instance's outputs replace it.
@@ -696,7 +863,7 @@ function hitlType(typeName, versions, factory, variant) {
696
863
  kind: 'hitl', typeName, factory: 'hitl', defaultVersion: '1.0',
697
864
  versions: Object.fromEntries(versions.map((v) => [v, hitlAt(typeName, v, factory)])),
698
865
  // The base node is the one an absent or UNKNOWN variant resolves to, as it
699
- // always has: `check` reports the unknown one (HITL_VARIANT_UNKNOWN).
866
+ // always has: its serialize refuses the unknown one (HITL_VARIANT_UNKNOWN).
700
867
  matches: ({ inputs }) => (variant === undefined
701
868
  ? !VARIANTS.includes(inputs?.variant)
702
869
  : inputs?.variant === variant),
@@ -2,7 +2,7 @@ import { AGENTIC_PROCESS_FORMS } from '../../../core/agentic-process-forms.js';
2
2
  import { GUID_RE } from '../../../core/binding-messages.js';
3
3
  import { nearestName } from '../../../core/edit-distance.js';
4
4
  import { ownEntry } from '../../../core/own-entry.js';
5
- import { renamedFieldMessage } from '../../../core/process-resource.js';
5
+ import { blank } from '../../../core/process-resource.js';
6
6
  export const PROCESS_DIAGNOSTICS = {
7
7
  AGENT_EMPTY_INPUT_FLOOR: { severity: 'warning', summary: 'an agent step that binds no inputs' },
8
8
  AGENT_EMPTY_RETURNS: { severity: 'warning', summary: 'an agent step with returns: {}' },
@@ -90,16 +90,8 @@ export const PROCESS_DIAGNOSTICS = {
90
90
  export function checkRpaWorkflow(inputs, ctx) {
91
91
  const step = ctx.step;
92
92
  const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
93
- const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
94
- if (blank(inputs?.key) && !blank(inputs?.process)) {
95
- at('RPA_NO_KEY', renamedFieldMessage('RPA', step, 'process'), `key: ${JSON.stringify(inputs.process)}`);
96
- }
97
- else if (blank(inputs?.key)) {
98
- at('RPA_NO_KEY', `RPA step "${step}" has no \`key\`. That is the published process's release key (a GUID), and `
99
- + `it becomes part of the node's type — without it there is no process to start. Find it with `
100
- + `\`uip or processes list --all-folders --process-type Process\` (the row's \`Key\`).`, `key: '486edc26-0658-4ac1-92c9-1ef953927151'`);
101
- }
102
- else if (!GUID_RE.test(String(inputs.key).trim())) {
93
+ // A blank key is the serializer's RPA_NO_KEY (refusals.ts).
94
+ if (!blank(inputs?.key) && !GUID_RE.test(String(inputs.key).trim())) {
103
95
  at('RPA_KEY_NOT_KEY', `RPA step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a release key. `
104
96
  + `\`key\` is the GUID the tenant gives the published process (it becomes the node type's suffix); `
105
97
  + `the process's NAME goes in \`name\` and its folder in \`folderPath\`.`, `key: '<guid>', name: ${JSON.stringify(inputs.key)}`);
@@ -121,12 +113,7 @@ export function checkRpaWorkflow(inputs, ctx) {
121
113
  at('RPA_LOCAL_PROJECT_ID_NOT_GUID', `In-solution RPA step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
122
114
  + `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
123
115
  }
124
- if (inSolution && !blank(inputs?.folderPath)) {
125
- at('RPA_LOCAL_HAS_FOLDER', `In-solution RPA step "${step}" also sets \`folderPath\` ${JSON.stringify(inputs.folderPath)}. An `
126
- + `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
127
- + `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
128
- + `call the published copy.`);
129
- }
116
+ // A folderPath beside in-solution is the serializer's RPA_LOCAL_HAS_FOLDER (refusals.ts).
130
117
  // The two are swappable by shape, and swapping them is invisible until
131
118
  // Orchestrator answers "Folder does not exist or the user does not have access".
132
119
  const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
@@ -160,16 +147,8 @@ export function checkRpaWorkflow(inputs, ctx) {
160
147
  export function checkApiWorkflow(inputs, ctx) {
161
148
  const step = ctx.step;
162
149
  const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
163
- const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
164
- if (blank(inputs?.key) && !blank(inputs?.workflow)) {
165
- at('APIWF_NO_KEY', renamedFieldMessage('API workflow', step, 'workflow'), `key: ${JSON.stringify(inputs.workflow)}`);
166
- }
167
- else if (blank(inputs?.key)) {
168
- at('APIWF_NO_KEY', `API workflow step "${step}" has no \`key\`. That is the published workflow's key (a GUID), and `
169
- + `it becomes part of the node's type — without it there is no workflow to invoke. Find it with `
170
- + `\`uip or processes list --all-folders\` (an API workflow's ProcessKey reads "<name>.api.<name>").`, `key: 'ce857908-ee1d-4392-b552-38bcea0be29c'`);
171
- }
172
- else if (!GUID_RE.test(String(inputs.key).trim())) {
150
+ // A blank key is the serializer's APIWF_NO_KEY (refusals.ts).
151
+ if (!blank(inputs?.key) && !GUID_RE.test(String(inputs.key).trim())) {
173
152
  at('APIWF_KEY_NOT_KEY', `API workflow step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a key. `
174
153
  + `\`key\` is the GUID the tenant gives the published workflow (it becomes the node type's suffix); `
175
154
  + `the workflow's NAME goes in \`name\` and its folder in \`folderPath\`.`, `key: '<guid>', name: ${JSON.stringify(inputs.key)}`);
@@ -208,13 +187,8 @@ export function checkApiWorkflow(inputs, ctx) {
208
187
  export function checkPublishedFunction(inputs, ctx) {
209
188
  const step = ctx.step;
210
189
  const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
211
- const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
212
- if (blank(inputs?.key)) {
213
- at('FUNCTION_NO_KEY', `Function step "${step}" has no \`key\`. That is the published function's key (a GUID), and it becomes `
214
- + `part of the node's type — without it there is no function to invoke. Find it with `
215
- + `\`uip maestro flow registry search "uipath.core.function"\`.`, `key: '7059bdb5-fdd7-4e13-9d7b-1748aaeb129d'`);
216
- }
217
- else if (!GUID_RE.test(String(inputs.key).trim())) {
190
+ // A blank key is the serializer's FUNCTION_NO_KEY (refusals.ts).
191
+ if (!blank(inputs?.key) && !GUID_RE.test(String(inputs.key).trim())) {
218
192
  at('FUNCTION_KEY_NOT_KEY', `Function step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a key. \`key\` is the `
219
193
  + `GUID the tenant gives the published function (it becomes the node type's suffix); the function's NAME `
220
194
  + `goes in \`name\` and its folder in \`folderPath\`.`, `key: '<guid>', name: ${JSON.stringify(inputs.key)}`);
@@ -238,12 +212,7 @@ export function checkPublishedFunction(inputs, ctx) {
238
212
  at('FUNCTION_LOCAL_PROJECT_ID_NOT_GUID', `In-solution function step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
239
213
  + `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
240
214
  }
241
- if (inSolution && !blank(inputs?.folderPath)) {
242
- at('FUNCTION_LOCAL_HAS_FOLDER', `In-solution function step "${step}" also sets \`folderPath\` ${JSON.stringify(inputs.folderPath)}. An `
243
- + `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
244
- + `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
245
- + `call the published copy.`);
246
- }
215
+ // A folderPath beside in-solution is the serializer's FUNCTION_LOCAL_HAS_FOLDER (refusals.ts).
247
216
  const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
248
217
  if (name.includes('/')) {
249
218
  at('FUNCTION_NAME_IS_PATH', `Function step "${step}"'s \`name\` is ${JSON.stringify(inputs.name)}, which is a folder path. \`name\` is `
@@ -282,17 +251,8 @@ export function checkPublishedFunction(inputs, ctx) {
282
251
  export function checkAgenticProcess(inputs, ctx) {
283
252
  const step = ctx.step;
284
253
  const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
285
- const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
286
- if (blank(inputs?.key) && !blank(inputs?.process)) {
287
- at('AGENTIC_NO_KEY', renamedFieldMessage('Agentic process', step, 'process'), `key: ${JSON.stringify(inputs.process)}`);
288
- }
289
- else if (blank(inputs?.key)) {
290
- at('AGENTIC_NO_KEY', `Agentic process step "${step}" has no \`key\`. That is the published process's key (a GUID), `
291
- + `and it becomes part of the node's type — without it there is no process to start. Find it with `
292
- + `\`uip or processes list --all-folders --limit 100\` (an agentic process's ProcessKey reads `
293
- + `"<name>.agentic.<name>"; the listing paginates at 50, so pass a limit or scope it to a folder).`, `key: '4fc450ab-89be-4462-8fc8-21ac4c1d6fb9'`);
294
- }
295
- else if (!GUID_RE.test(String(inputs.key).trim())) {
254
+ // A blank key is the serializer's AGENTIC_NO_KEY (refusals.ts).
255
+ if (!blank(inputs?.key) && !GUID_RE.test(String(inputs.key).trim())) {
296
256
  at('AGENTIC_KEY_NOT_KEY', `Agentic process step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a key. `
297
257
  + `\`key\` is the GUID the tenant gives the published process (it becomes the node type's suffix); `
298
258
  + `the process's NAME goes in \`name\` and its folder in \`folderPath\`.`, `key: '<guid>', name: ${JSON.stringify(inputs.key)}`);
@@ -337,29 +297,14 @@ export function checkAgenticProcess(inputs, ctx) {
337
297
  at('AGENTIC_LOCAL_PROJECT_ID_NOT_GUID', `In-solution agentic process step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
338
298
  + `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
339
299
  }
340
- if (inSolution && !blank(inputs?.folderPath)) {
341
- at('AGENTIC_LOCAL_HAS_FOLDER', `In-solution agentic process step "${step}" also sets \`folderPath\` ${JSON.stringify(inputs.folderPath)}. An `
342
- + `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
343
- + `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
344
- + `call the published copy.`);
345
- }
300
+ // A folderPath beside in-solution is the serializer's AGENTIC_LOCAL_HAS_FOLDER (refusals.ts).
346
301
  const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
347
302
  if (name.includes('/')) {
348
303
  at('AGENTIC_NAME_IS_PATH', `Agentic process step "${step}"'s \`name\` is ${JSON.stringify(inputs.name)}, which is a folder path. `
349
304
  + `\`name\` is the process's own name in Orchestrator; the path it lives in goes in \`folderPath\`. `
350
305
  + `Together they form the resourceKey "<folderPath>.<name>", so swapping them points the job at nothing.`);
351
306
  }
352
- // The platform's own design-time input, which its serializer strips before the
353
- // node is emitted — so a process argument by this name is dropped on the way to
354
- // the tenant and nothing local notices.
355
- if (inputs?.inputs && Object.prototype.hasOwnProperty.call(inputs.inputs, 'fireAndForget')) {
356
- at('AGENTIC_RESERVED_INPUT', `Agentic process step "${step}" passes an input called \`fireAndForget\`. That name is RESERVED: it is `
357
- + `the platform's design-time switch for starting the process without waiting, and its serializer `
358
- + `removes the key from the node's inputs before emitting — so a process argument of that name never `
359
- + `arrives. Locally it would appear to work, because this engine passes the whole envelope through. `
360
- + `Rename the argument, and note that this surface cannot author fire-and-forget at all (the engine `
361
- + `always waits for the job).`);
362
- }
307
+ // An argument called `fireAndForget` is the serializer's AGENTIC_RESERVED_INPUT (refusals.ts).
363
308
  // An empty `returns: {}` is a statement that the process returns nothing, which is
364
309
  // legal — but it is almost always a half-finished edit, and the difference between
365
310
  // it and a missing `returns` is invisible in the emitted file.
@@ -500,19 +445,9 @@ export function checkAgenticProcessRead({ node: name, field, rest, reader: step
500
445
  export function checkAgent(inputs, ctx) {
501
446
  const step = ctx.step;
502
447
  const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
503
- const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
504
448
  const inSolution = inputs?.location === 'in-solution';
505
- if (blank(inputs?.key) && !blank(inputs?.agent)) {
506
- at('AGENT_NO_KEY', renamedFieldMessage('Agent', step, 'agent'), `key: ${JSON.stringify(inputs.agent)}`);
507
- }
508
- else if (blank(inputs?.key)) {
509
- at('AGENT_NO_KEY', `Agent step "${step}" has no \`key\`. It is a GUID and becomes part of the node's `
510
- + `type — without it there is no agent to start. For a published agent, find the Orchestrator `
511
- + `release with \`uip or processes list --all-folders --limit 200\` (take the row's \`Key\`) or `
512
- + `\`uip maestro flow registry search uipath.core.agent\`. For an in-solution agent, read `
513
- + `\`resource.key\` from \`resources/solution_folder/process/agent/<project>.json\`.`, `key: '6a47dc5b-5fd4-461f-b97d-6c1817abcaaa'`);
514
- }
515
- else if (!GUID_RE.test(String(inputs.key).trim())) {
449
+ // A blank key is the serializer's AGENT_NO_KEY (refusals.ts).
450
+ if (!blank(inputs?.key) && !GUID_RE.test(String(inputs.key).trim())) {
516
451
  at('AGENT_KEY_NOT_GUID', `Agent step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a key. \`key\` is `
517
452
  + `the GUID the tenant gives the published agent (it becomes the node type's suffix); the agent's NAME `
518
453
  + `goes in \`name\` and its folder in \`folderPath\`. Passing a ProcessKey like `
@@ -17,6 +17,7 @@ import { ownEntry } from '../../../core/own-entry.js';
17
17
  import { resourceNodeSortOrder } from '../../../core/process-resource-sort-order.js';
18
18
  import { processResourceBindings, processResourceLocation } from '../../../core/process-resource.js';
19
19
  import { checkAgent, checkAgentRead, checkAgenticProcess, checkAgenticProcessRead, checkApiWorkflow, checkApiWorkflowRead, checkPublishedFunction, checkPublishedFunctionRead, checkRpaWorkflow, rpaWorkflowRead, } from './check.js';
20
+ import { refuseLossyProcess } from './refusals.js';
20
21
  /** The RPA-workflow node type family. The process's release key is the suffix. */
21
22
  const RPA_WORKFLOW_PREFIX = 'uipath.core.rpa-workflow.';
22
23
  /**
@@ -635,6 +636,9 @@ export function processDefinition(spec) {
635
636
  * are identical, and the DATA paths that differ are all downstream in the engine.
636
637
  */
637
638
  function serializeProcess(spec, ctx) {
639
+ // A missing key, a folder beside in-solution, a reserved `fireAndForget`
640
+ // argument: each would be written wrong, so it is refused here (#1518).
641
+ refuseLossyProcess(spec.kind, spec.inputs, ctx.step);
638
642
  const s = spec.inputs;
639
643
  const fireAndForget = spec.kind === 'agenticProcess' && s.completion === 'fire-and-forget';
640
644
  const def = processDefinition(spec);