@uipath/maestro-builder-sdk 6.71.0 → 6.72.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.
@@ -77,18 +77,18 @@ export function scriptBodySyntaxError(code) {
77
77
  }
78
78
  /**
79
79
  * The JavaScript engine's early error for a body run as
80
- * `function (<scopeGlobal>) { "use strict"; <code> }`, or undefined when it has
80
+ * `function (<params>) { "use strict"; <code> }`, or undefined when it has
81
81
  * none.
82
82
  *
83
83
  * TypeScript's parser does not report early errors (a duplicate `let`, a
84
- * `const $vars` that clashes with the parameter, an octal literal in strict
84
+ * `const $vars` that clashes with a parameter, an octal literal in strict
85
85
  * code), and the printed function lives in an ES module, which is strict. A body
86
86
  * with one would make the whole module unloadable, so decompile asks the engine.
87
87
  * The body is compiled, never called. A host without the `Function`
88
88
  * constructor (a strict content-security policy) cannot answer, which is
89
89
  * reported as an error too, so the caller keeps the string form.
90
90
  */
91
- export function scriptBodyEarlyError(code, runtime) {
91
+ export function scriptBodyEarlyError(code, params) {
92
92
  // Compiled for their early errors only; neither is invoked. Module code also
93
93
  // reserves `await`, which no synchronous constructor reports: an async
94
94
  // function reserves it the same way, so a body must compile as both. (A
@@ -96,8 +96,8 @@ export function scriptBodyEarlyError(code, runtime) {
96
96
  // the module's plain function.)
97
97
  const AsyncFunction = (async () => { }).constructor;
98
98
  try {
99
- new Function(runtime.scopeGlobal, `"use strict";\n${code}`);
100
- new AsyncFunction(runtime.scopeGlobal, `"use strict";\n${code}`);
99
+ new Function(...params, `"use strict";\n${code}`);
100
+ new AsyncFunction(...params, `"use strict";\n${code}`);
101
101
  return undefined;
102
102
  }
103
103
  catch (e) {
@@ -167,6 +167,7 @@ function functionName(fn) {
167
167
  return fn.name || undefined;
168
168
  return /^\s*(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/.exec(fn)?.[1];
169
169
  }
170
+ const ORDINALS = ['first', 'second', 'third', 'fourth'];
170
171
  class Lowering {
171
172
  ts;
172
173
  sf;
@@ -177,9 +178,9 @@ class Lowering {
177
178
  problems = [];
178
179
  edits = [];
179
180
  free = new Set();
180
- aliased = new Set();
181
- param;
182
- paramSymbol;
181
+ aliased = new Map();
182
+ read = new Set();
183
+ params = [];
183
184
  ownNameSymbol;
184
185
  constructor(ts, sf, fn, runtime, options) {
185
186
  this.ts = ts;
@@ -196,18 +197,18 @@ class Lowering {
196
197
  return { ok: false, problems: this.problems, free: [] };
197
198
  if (ts.isFunctionExpression(fn) && fn.name)
198
199
  this.ownNameSymbol = this.checker.getSymbolAtLocation(fn.name);
199
- const renaming = this.param !== undefined && this.param.text !== this.runtime.scopeGlobal;
200
- if (renaming) {
201
- this.checkNoCapture();
202
- this.checkNoRedeclaredParameter();
200
+ for (const param of this.params) {
201
+ if (param.id.text === param.scope.global)
202
+ continue;
203
+ this.checkNoCapture(param.scope.global);
204
+ this.checkNoRedeclaredParameter(param);
203
205
  }
204
206
  this.checkConstGlobals();
205
207
  this.visit(fn.body, 0);
206
- if (this.aliased.size > 0) {
207
- const name = [...this.aliased][0];
208
- this.problems.push(`it uses its parameter \`${name}\` other than as a dotted read (\`${name}.start.output.x\`): an alias, a computed or optional read, `
209
- + `or passing \`${name}\` on hides the step it reads from the step renaming and reference checks, which see only \`${this.runtime.scopeGlobal}.<step>\`. `
210
- + 'Read each step with dots.');
208
+ for (const [name, scope] of this.aliased) {
209
+ this.problems.push(`it uses its parameter \`${name}\` other than as a dotted read (\`${name}.${scope.example}\`): an alias, a computed or optional read, `
210
+ + `or passing \`${name}\` on hides the ${scope.reads} it reads from ${scope.checkedBy}, which see only \`${scope.global}.<${scope.reads}>\`. `
211
+ + `Read each ${scope.reads} with dots.`);
211
212
  }
212
213
  const free = [...this.free].sort();
213
214
  if (free.length > 0) {
@@ -216,7 +217,9 @@ class Lowering {
216
217
  }
217
218
  if (this.problems.length > 0)
218
219
  return { ok: false, problems: this.problems, free };
219
- return { ok: true, code: this.body() };
220
+ const scopesRead = this.runtime.scopes.filter((s) => this.read.has(s)).map((s) => s.global);
221
+ const scopesDeclared = this.runtime.scopes.filter((s) => this.bindsInFunctionScope(s.global)).map((s) => s.global);
222
+ return { ok: true, code: this.body(), scopesRead, scopesDeclared };
220
223
  }
221
224
  /** The function forms that cannot become a body at all. */
222
225
  checkShape() {
@@ -226,34 +229,49 @@ class Lowering {
226
229
  this.problems.push('it is `async`; a script body runs synchronously.');
227
230
  if (ts.isFunctionExpression(fn) && fn.asteriskToken)
228
231
  this.problems.push('it is a generator; a script body returns one value.');
229
- if (fn.parameters.length > 1) {
230
- this.problems.push(`it takes ${fn.parameters.length} parameters; a script function takes one, the runtime's variable scope (${this.runtime.scopeGlobal}), or none.`);
231
- return;
232
- }
233
- const p = fn.parameters[0];
234
- if (!p)
232
+ const { scopes } = this.runtime;
233
+ if (fn.parameters.length > scopes.length) {
234
+ const order = scopes.map((s) => `${s.holds} (${s.global})`);
235
+ this.problems.push(scopes.length === 1
236
+ ? `it takes ${fn.parameters.length} parameters; a script function takes one, ${order[0]}, or none.`
237
+ : `it takes ${fn.parameters.length} parameters; a script function takes at most ${scopes.length}, in this order: ${order.join(', ')}.`);
235
238
  return;
236
- if (p.dotDotDotToken)
237
- this.problems.push('its parameter is a rest parameter; take the variable scope as one plain parameter.');
238
- else if (p.initializer)
239
- this.problems.push('its parameter has a default value; the runtime always supplies the variable scope.');
240
- else if (!ts.isIdentifier(p.name)) {
241
- this.problems.push('its parameter is destructured. Take the variable scope as one plain parameter and read it with dots '
242
- + `(\`vars.start.output.x\`): a destructured read is invisible to the step renaming and reference checks that read \`${this.runtime.scopeGlobal}.<step>\`.`);
243
- }
244
- else {
245
- this.param = p.name;
246
- this.paramSymbol = this.checker.getSymbolAtLocation(p.name);
247
239
  }
240
+ fn.parameters.forEach((p, i) => {
241
+ const scope = scopes[i];
242
+ const its = fn.parameters.length > 1 ? `its ${ORDINALS[i]} parameter` : 'its parameter';
243
+ if (p.dotDotDotToken)
244
+ this.problems.push(`${its} is a rest parameter; take ${scope.holds} as one plain parameter.`);
245
+ else if (p.initializer)
246
+ this.problems.push(`${its} has a default value; the runtime always supplies ${scope.holds}.`);
247
+ else if (!ts.isIdentifier(p.name)) {
248
+ this.problems.push(`${its} is destructured. Take ${scope.holds} as one plain parameter and read it with dots `
249
+ + `(\`${scope.param}.${scope.example}\`): a destructured read is invisible to ${scope.checkedBy} that read \`${scope.global}.<${scope.reads}>\`.`);
250
+ }
251
+ else {
252
+ // Parameters bind by position, so one named like another scope would be
253
+ // renamed to its own scope's global and read that scope instead:
254
+ // `($metadata) => $metadata.InstanceId` would read `$vars.InstanceId`.
255
+ const name = p.name.text;
256
+ const named = scopes.find((other) => other !== scope && other.global === name);
257
+ if (named) {
258
+ this.problems.push(`${its} is named \`${named.global}\` but receives ${scope.holds} (${scope.global}): parameters bind by position, `
259
+ + `and \`${named.global}\` is the ${ORDINALS[scopes.indexOf(named)]} one. `
260
+ + `Take them in order, \`(${scopes.map((s) => s.global).join(', ')}) => …\`, or give this one another name.`);
261
+ }
262
+ else {
263
+ this.params.push({ id: p.name, symbol: this.checker.getSymbolAtLocation(p.name), scope });
264
+ }
265
+ }
266
+ });
248
267
  }
249
268
  /**
250
- * Refuse a binding named like the scope global anywhere in the function: once
251
- * the parameter's references are rewritten to that name, the binding would
269
+ * Refuse a binding named like a scope global anywhere in the function: once
270
+ * a parameter's references are rewritten to that name, the binding would
252
271
  * capture them (`const $vars = {…}; return vars.x` would read the local).
253
272
  */
254
- checkNoCapture() {
273
+ checkNoCapture(target) {
255
274
  const { ts } = this;
256
- const target = this.runtime.scopeGlobal;
257
275
  let found = false;
258
276
  const visit = (n) => {
259
277
  if (found)
@@ -269,6 +287,38 @@ class Lowering {
269
287
  + `Rename the inner binding, or name the parameter \`${target}\` so nothing is renamed.`);
270
288
  }
271
289
  }
290
+ /**
291
+ * Whether the body binds `name` in the function's own scope: a top-level
292
+ * `let`, `const`, `class` or `function` (a label does not scope what it
293
+ * labels), or a `var` anywhere outside a nested function, which hoists.
294
+ */
295
+ bindsInFunctionScope(name) {
296
+ const { ts } = this;
297
+ const body = this.fn.body;
298
+ if (!ts.isBlock(body))
299
+ return false;
300
+ for (let stmt of body.statements) {
301
+ while (ts.isLabeledStatement(stmt))
302
+ stmt = stmt.statement;
303
+ if (ts.isVariableStatement(stmt) && stmt.declarationList.declarations.some((d) => bindsName(ts, d.name, name)))
304
+ return true;
305
+ if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && stmt.name?.text === name)
306
+ return true;
307
+ }
308
+ let found = false;
309
+ const vars = (n) => {
310
+ if (found || ts.isFunctionLike(n) || ts.isClassLike(n))
311
+ return;
312
+ if (ts.isVariableDeclarationList(n) && (n.flags & ts.NodeFlags.BlockScoped) === 0
313
+ && n.declarations.some((d) => bindsName(ts, d.name, name))) {
314
+ found = true;
315
+ return;
316
+ }
317
+ ts.forEachChild(n, vars);
318
+ };
319
+ ts.forEachChild(body, vars);
320
+ return found;
321
+ }
272
322
  /**
273
323
  * Refuse a `var vars` or `function vars() {}` in the body: either binds the
274
324
  * parameter's own name in the function's scope, so the rename would rewrite the
@@ -278,9 +328,9 @@ class Lowering {
278
328
  * still resolving the reads to the parameter. A nested function (arrows
279
329
  * included) has its own `var` scope, so its declarations are its own.
280
330
  */
281
- checkNoRedeclaredParameter() {
331
+ checkNoRedeclaredParameter(param) {
282
332
  const { ts } = this;
283
- const name = this.param.text;
333
+ const name = param.id.text;
284
334
  let found = false;
285
335
  const visit = (n) => {
286
336
  if (found)
@@ -303,7 +353,7 @@ class Lowering {
303
353
  };
304
354
  visit(this.fn.body);
305
355
  if (found) {
306
- this.problems.push(`it redeclares its parameter \`${name}\` (a \`var\` or a function declaration), which the rename to \`${this.runtime.scopeGlobal}\` would split from its reads. `
356
+ this.problems.push(`it redeclares its parameter \`${name}\` (a \`var\` or a function declaration), which the rename to \`${param.scope.global}\` would split from its reads. `
307
357
  + 'Give the local a name of its own.');
308
358
  }
309
359
  }
@@ -445,14 +495,18 @@ class Lowering {
445
495
  const parent = id.parent;
446
496
  const shorthand = ts.isShorthandPropertyAssignment(parent) && parent.name === id;
447
497
  const symbol = shorthand ? this.checker.getShorthandAssignmentValueSymbol(parent) : this.checker.getSymbolAtLocation(id);
448
- if (symbol && symbol === this.paramSymbol) {
498
+ const param = symbol ? this.params.find((p) => p.symbol === symbol) : undefined;
499
+ if (param) {
500
+ const { scope } = param;
501
+ this.read.add(scope);
449
502
  if (this.options.scopeReadsOnly && !(ts.isPropertyAccessExpression(parent) && parent.expression === id && !parent.questionDotToken)) {
450
- this.aliased.add(id.text);
503
+ if (!this.aliased.has(id.text))
504
+ this.aliased.set(id.text, scope);
451
505
  }
452
- if (this.param.text !== this.runtime.scopeGlobal) {
506
+ if (param.id.text !== scope.global) {
453
507
  this.edits.push(shorthand
454
- ? [id.getStart(), id.end, `${id.text}: ${this.runtime.scopeGlobal}`]
455
- : [id.getStart(), id.end, this.runtime.scopeGlobal]);
508
+ ? [id.getStart(), id.end, `${id.text}: ${scope.global}`]
509
+ : [id.getStart(), id.end, scope.global]);
456
510
  }
457
511
  return;
458
512
  }
@@ -471,7 +525,7 @@ class Lowering {
471
525
  }
472
526
  if (symbol && !isUndefinedSymbol(symbol))
473
527
  return;
474
- if (id.text === this.runtime.scopeGlobal || this.runtime.globals.has(id.text)) {
528
+ if (this.runtime.scopes.some((s) => s.global === id.text) || this.runtime.globals.has(id.text)) {
475
529
  this.checkGlobalMember(id);
476
530
  return;
477
531
  }
@@ -541,7 +595,7 @@ class Lowering {
541
595
  }
542
596
  }
543
597
  /** The checker of a one-file program over `sf`, with no default library: globals resolve to nothing. */
544
- function oneFileChecker(ts, sf) {
598
+ export function oneFileChecker(ts, sf) {
545
599
  const options = { noLib: true, noResolve: true, types: [], target: ts.ScriptTarget.ES2020 };
546
600
  const host = {
547
601
  getSourceFile: (name) => (name === sf.fileName ? sf : undefined),
@@ -557,7 +611,7 @@ function oneFileChecker(ts, sf) {
557
611
  return ts.createProgram({ rootNames: [sf.fileName], options, host }).getTypeChecker();
558
612
  }
559
613
  /** The checker's own symbol for `undefined`, which a no-lib program still binds. */
560
- function isUndefinedSymbol(s) {
614
+ export function isUndefinedSymbol(s) {
561
615
  return s.escapedName === 'undefined' && (s.declarations?.length ?? 0) === 0;
562
616
  }
563
617
  /** Whether `id` is read as a value, not a property name, label, or type. */
@@ -33,6 +33,9 @@ export const PROCESS_DIAGNOSTICS = {
33
33
  AGENTIC_RESERVED_INPUT: { severity: 'error', summary: 'an agentic process step with the reserved fireAndForget input' },
34
34
  APIWF_EMPTY_RETURNS: { severity: 'warning', summary: 'an API workflow step with returns: {}' },
35
35
  APIWF_KEY_NOT_KEY: { severity: 'error', summary: 'an API workflow step whose key is not a GUID' },
36
+ APIWF_LOCAL_HAS_FOLDER: { severity: 'error', summary: 'an API workflow step in-solution, with a folderPath' },
37
+ APIWF_LOCAL_NO_PROJECT_ID: { severity: 'error', summary: 'an API workflow step in-solution, with no projectId' },
38
+ APIWF_LOCAL_PROJECT_ID_NOT_GUID: { severity: 'error', summary: 'an API workflow step in-solution, with a projectId that is not a GUID' },
36
39
  APIWF_NAME_IS_PATH: { severity: 'error', summary: 'an API workflow step whose name looks like a folder path' },
37
40
  APIWF_NO_FOLDER: { severity: 'error', summary: 'an API workflow step with no folderPath' },
38
41
  APIWF_NO_KEY: { severity: 'error', summary: 'an API workflow step with no key' },
@@ -157,10 +160,21 @@ export function checkApiWorkflow(inputs, ctx) {
157
160
  at('APIWF_NO_NAME', `API workflow step "${step}" has no \`name\`. The runtime starts the job by name + folder, not by the `
158
161
  + `key in the node type, so without it the workflow cannot be resolved and the run fails before dispatch.`, `name: 'NameToAgeFixed'`);
159
162
  }
160
- if (blank(inputs?.folderPath)) {
163
+ const inSolution = inputs?.location === 'in-solution';
164
+ if (!inSolution && blank(inputs?.folderPath)) {
161
165
  at('APIWF_NO_FOLDER', `API workflow step "${step}" has no \`folderPath\`. The runtime starts the job by name + folder, so the `
162
- + `Orchestrator folder holding the workflow is required.`, `folderPath: 'Shared'`);
166
+ + `Orchestrator folder holding the workflow is required. For an API workflow in the SAME solution, set `
167
+ + `\`location: 'in-solution'\` and its \`projectId\` instead.`, `folderPath: 'Shared'`);
168
+ }
169
+ if (inSolution && blank(inputs?.projectId)) {
170
+ at('APIWF_LOCAL_NO_PROJECT_ID', `In-solution API workflow step "${step}" has no \`projectId\`. Read \`resource.projectKey\` from `
171
+ + `\`resources/solution_folder/process/api/<project>.json\`.`, `projectId: '<resource.projectKey>'`);
172
+ }
173
+ else if (inSolution && !GUID_RE.test(String(inputs.projectId).trim())) {
174
+ at('APIWF_LOCAL_PROJECT_ID_NOT_GUID', `In-solution API workflow step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
175
+ + `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
163
176
  }
177
+ // A folderPath beside in-solution is the serializer's APIWF_LOCAL_HAS_FOLDER (refusals.ts).
164
178
  const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
165
179
  if (name.includes('/')) {
166
180
  at('APIWF_NAME_IS_PATH', `API workflow step "${step}"'s \`name\` is ${JSON.stringify(inputs.name)}, which is a folder path. `
@@ -176,7 +176,8 @@ const API_WORKFLOW_PREFIX = 'uipath.core.api-workflow.';
176
176
  * one the platform's own definition writes.
177
177
  */
178
178
  function apiWorkflowDef(spec) {
179
- const resourceKey = `${spec.folderPath}.${spec.name}`;
179
+ const loc = processResourceLocation(spec);
180
+ const { resourceKey } = loc;
180
181
  const properties = {};
181
182
  for (const [k, v] of Object.entries(spec.inputs)) {
182
183
  properties[k] = { type: typeof v === 'number' ? 'number' : typeof v === 'boolean' ? 'boolean' : 'string' };
@@ -186,8 +187,8 @@ function apiWorkflowDef(spec) {
186
187
  version: '1.0.0',
187
188
  category: 'api-workflow',
188
189
  runtimeConstraints: { exclude: ['api-function'] },
189
- description: `(${spec.folderPath})`,
190
- tags: [],
190
+ description: loc.description,
191
+ tags: loc.tags,
191
192
  sortOrder: resourceNodeSortOrder('api-workflow'),
192
193
  supportsErrorHandling: true,
193
194
  display: { label: spec.name, icon: 'api' },
@@ -195,11 +196,12 @@ function apiWorkflowDef(spec) {
195
196
  { position: 'left', handles: [{ id: 'input', type: 'target', handleType: 'input' }] },
196
197
  { position: 'right', handles: [{ id: 'output', type: 'source', handleType: 'output' }] },
197
198
  ],
199
+ ...loc.definitionExtras,
198
200
  model: {
199
201
  type: 'bpmn:ServiceTask',
200
202
  serviceType: 'Orchestrator.ExecuteApiWorkflowAsync',
201
203
  version: 'v2',
202
- section: 'Published',
204
+ ...(loc.inSolution ? {} : { section: 'Published' }),
203
205
  bindings: {
204
206
  resource: 'process',
205
207
  resourceSubType: 'Api',
@@ -207,9 +209,10 @@ function apiWorkflowDef(spec) {
207
209
  orchestratorType: 'api',
208
210
  values: [
209
211
  { name: 'name', propertyAttribute: 'name', default: spec.name },
210
- { name: 'folderPath', propertyAttribute: 'folderPath', default: spec.folderPath },
212
+ { name: 'folderPath', propertyAttribute: 'folderPath', default: loc.folderPath },
211
213
  ],
212
214
  },
215
+ ...loc.modelExtras,
213
216
  context: [
214
217
  { name: 'name', type: 'string', value: '<bindings.name>' },
215
218
  { name: 'folderPath', type: 'string', value: '<bindings.folderPath>' },
@@ -611,7 +614,10 @@ export function processDefinition(spec) {
611
614
  inputs: s.inputs ?? {}, returns: s.returns,
612
615
  });
613
616
  case 'apiWorkflow':
614
- return apiWorkflowDef({ key: s.key, name: s.name, folderPath: s.folderPath, inputs: s.inputs ?? {}, returns: s.returns });
617
+ return apiWorkflowDef({
618
+ key: s.key, name: s.name, folderPath: s.folderPath, location: s.location, projectId: s.projectId,
619
+ inputs: s.inputs ?? {}, returns: s.returns,
620
+ });
615
621
  case 'agenticProcess':
616
622
  return agenticProcessDef({
617
623
  key: s.key, name: s.name, folderPath: s.folderPath, location: s.location, projectId: s.projectId,
@@ -723,11 +729,14 @@ function liftProcess(node, ctx, prefix, kind, form) {
723
729
  const name = binding('name');
724
730
  const folderPath = binding('folderPath');
725
731
  // A sibling project of the same solution (#951): the model names its project
726
- // instead of a folder. `isInSolution` is flow-core's marker. Read back as
727
- // `location: 'in-solution'` so the recompile takes the in-solution path
728
- // instead of a published one with an empty folder.
732
+ // instead of a folder. `isInSolution` is flow-core's marker; files that carry
733
+ // `projectName` but neither it nor `projectId` still carry the `in-solution`
734
+ // tag and an empty folder. Read back as `location: 'in-solution'` so the
735
+ // recompile takes the in-solution path instead of a published one with an
736
+ // empty folder (#1396). A tag beside a real folder is not enough.
729
737
  const projectId = typeof def?.model?.projectId === 'string' ? def.model.projectId : undefined;
730
- const inSolution = def?.model?.isInSolution === true || projectId !== undefined;
738
+ const inSolution = def?.model?.isInSolution === true || projectId !== undefined
739
+ || (Array.isArray(def?.tags) && def.tags.includes('in-solution') && !folderPath);
731
740
  const inputs = { ...(n.inputs ?? {}) };
732
741
  // The design-time fire-and-forget switch is an OPTION on agenticProcess(),
733
742
  // not a process argument — serialize adds it to `inputs` after rendering the
@@ -25,7 +25,7 @@ const KINDS = {
25
25
  noKeySuggestion: `key: '486edc26-0658-4ac1-92c9-1ef953927151'`,
26
26
  },
27
27
  apiWorkflow: {
28
- noKeyCode: 'APIWF_NO_KEY', label: 'API workflow', oldField: 'workflow',
28
+ noKeyCode: 'APIWF_NO_KEY', localCode: 'APIWF_LOCAL_HAS_FOLDER', label: 'API workflow', oldField: 'workflow', localLabel: 'API workflow step',
29
29
  noKey: (step) => `API workflow step "${step}" has no \`key\`. That is the published workflow's key (a GUID), and `
30
30
  + `it becomes part of the node's type — without it there is no workflow to invoke. Find it with `
31
31
  + `\`uip or processes list --all-folders\` (an API workflow's ProcessKey reads "<name>.api.<name>").`,
@@ -74,12 +74,38 @@ export interface ApiWorkflowInputs {
74
74
  * `uip or processes list --all-folders --process-type Api` (an API workflow's
75
75
  * `ProcessKey` reads `<name>.api.<name>`; the GUID you want is the row's `Key`)
76
76
  * or `uip maestro flow registry search`.
77
+ *
78
+ * For an in-solution sibling it is `resource.key` from
79
+ * `resources/solution_folder/process/api/<project>.json`, the suffix of the
80
+ * node type `uip maestro flow registry list --local` shows.
77
81
  */
78
82
  key: string;
79
83
  /** The workflow's name in Orchestrator — e.g. `'NameToAgeFixed'`. */
80
84
  name: string;
81
- /** The Orchestrator folder holding it — e.g. `'Shared'`. */
82
- folderPath: string;
85
+ /**
86
+ * Where the workflow lives. `'published'` (the default) is a workflow
87
+ * deployed to `folderPath`. `'in-solution'` is a sibling API workflow project
88
+ * of the same solution: it needs `projectId` and no `folderPath`, and `key` is
89
+ * that sibling's `resource.key`.
90
+ */
91
+ location?: ProcessLocation;
92
+ /**
93
+ * The Orchestrator folder holding it — e.g. `'Shared'`. Required for a
94
+ * published workflow; omit it for an in-solution sibling.
95
+ *
96
+ * @enforcedBy APIWF_NO_FOLDER A published API workflow needs its folder.
97
+ * @enforcedBy APIWF_LOCAL_HAS_FOLDER An in-solution API workflow has no folder.
98
+ */
99
+ folderPath?: string;
100
+ /**
101
+ * The sibling API workflow project's `resource.projectKey`, read from
102
+ * `resources/solution_folder/process/api/<project>.json`. Required only when
103
+ * `location` is `'in-solution'`.
104
+ *
105
+ * @enforcedBy APIWF_LOCAL_NO_PROJECT_ID An in-solution API workflow needs its project id.
106
+ * @enforcedBy APIWF_LOCAL_PROJECT_ID_NOT_GUID The project id is the sibling's GUID, not its name.
107
+ */
108
+ projectId?: string;
83
109
  /**
84
110
  * The WORKFLOW's own input arguments, by its own argument names (e.g.
85
111
  * `{ name: 'tomasz' }`). Not a fixed schema — every workflow declares its own.
@@ -9,9 +9,11 @@ import type { ScriptInputs, ScriptValue } from './spec.js';
9
9
  * Declare an inline script action, as a TypeScript function the compiler checks.
10
10
  *
11
11
  * The function is never called. Its body, exactly as written, becomes the
12
- * script; its parameter is an authoring-time name for the Flow runtime's own
13
- * `$vars` global and is renamed to it. `returns` is the declaration that reaches
14
- * the `.flow`, and tsc checks the function's return against it.
12
+ * script; its parameters are authoring-time names for the Flow runtime's own
13
+ * `$vars` and `$metadata` globals (typed `FlowVars` and `FlowMetadata`), in
14
+ * that order, and are renamed to them. Take neither, `$vars` alone, or both.
15
+ * `returns` is the declaration that reaches the `.flow`, and tsc checks the
16
+ * function's return against it.
15
17
  *
16
18
  * The function may read only what the script runtime has (the ES2020 built-ins
17
19
  * and `console`): a module constant, an import or a library throws here, because
@@ -20,29 +22,29 @@ import type { ScriptInputs, ScriptValue } from './spec.js';
20
22
  * ```ts
21
23
  * .step('post', script(parseMessage, { returns: { channel: 'string', threadTs: 'string' } }))
22
24
  *
23
- * function parseMessage(vars: FlowVars) {
24
- * const m = /archives\/([A-Z0-9]+)\/p(\d{10})(\d{6})/.exec(String(vars.start.output.message));
25
+ * function parseMessage($vars: FlowVars) {
26
+ * const m = /archives\/([A-Z0-9]+)\/p(\d{10})(\d{6})/.exec(String($vars.start.output.message));
25
27
  * if (!m) throw new Error('not a Slack permalink');
26
28
  * return { channel: m[1], threadTs: m[2] + '.' + m[3] };
27
29
  * }
28
30
  * ```
29
31
  *
30
- * @param fn - The script, as a function of the variable scope (or of nothing).
32
+ * @param fn - The script, as a function of the variable scope and the instance metadata (or of fewer).
31
33
  * @param options - What the function returns (`returns`), as `script({ returns })` takes it.
32
34
  * @returns An action spec to pass to `.step(name, …)`.
33
35
  */
34
- export declare function script<V, const R extends ScriptReturns>(fn: (vars: V) => ScriptValue<R>, options: {
36
+ export declare function script<V, const R extends ScriptReturns, M = unknown>(fn: (vars: V, metadata: M) => ScriptValue<R>, options: {
35
37
  returns: R;
36
38
  }): ActionSpec;
37
39
  /**
38
40
  * Declare an inline script action, as a TypeScript function, with no declared
39
41
  * return: the body's type is inferred as it is for a string body.
40
42
  *
41
- * @param fn - The script, as a function of the variable scope (or of nothing).
43
+ * @param fn - The script, as a function of the variable scope and the instance metadata (or of fewer).
42
44
  * @param options - No `returns`; pass one to have tsc check the function against it.
43
45
  * @returns An action spec to pass to `.step(name, …)`.
44
46
  */
45
- export declare function script<V>(fn: (vars: V) => unknown, options?: {
47
+ export declare function script<V, M = unknown>(fn: (vars: V, metadata: M) => unknown, options?: {
46
48
  returns?: never;
47
49
  }): ActionSpec;
48
50
  /**
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { declaredScriptShape, inferScriptShape } from '../../../core/script-return.js';
8
8
  import { jsonClone } from '../../../core/json-clone.js';
9
+ import { unknownMetadataKeys } from '../../../core/flow-metadata.js';
9
10
  import { scriptBodyEarlyError, scriptBodySyntaxError, scriptBodyTypeScriptSyntax, tryLowerScriptFunction } from '../../../core/script-function.js';
10
11
  import { FLOW_SCRIPT_RUNTIME } from './runtime.js';
11
12
  import { checkScript } from './check.js';
@@ -106,12 +107,15 @@ const UNCHANGED = 'its text would not come back unchanged as a function body (a
106
107
  * this holds for nearly every body that passes the others; it is checked
107
108
  * because it is cheap, and a line comment on the last line is the case that
108
109
  * fails it (it would swallow the closing brace);
109
- * 5. the engine compiles it as a strict function of `$vars`: an early error
110
- * (a duplicate `let`, a `const $vars`, an octal literal) that TypeScript's
111
- * parser does not report would make the whole module unloadable.
110
+ * 5. the engine compiles it as a strict function of its parameters: an early
111
+ * error (a duplicate `let`, a `const $vars`, an octal literal) that
112
+ * TypeScript's parser does not report would make the whole module
113
+ * unloadable.
112
114
  *
113
- * The parameter is named `$vars`, so nothing is renamed and the body's text is
114
- * the node's text.
115
+ * The parameters are named `$vars` and, when the body reads it, `$metadata`, so
116
+ * nothing is renamed and the body's text is the node's text. A body that binds
117
+ * its own `$metadata` in the function's scope reads that, and prints without
118
+ * the parameter, which would clash with it.
115
119
  */
116
120
  function printAsFunction(code, ctx) {
117
121
  if (ctx.scripts !== 'function')
@@ -130,7 +134,7 @@ function printAsFunction(code, ctx) {
130
134
  // With authoring's own rules: the printed function is lowered by `script(fn)`
131
135
  // when the source is compiled back, so a body that rule refuses (`$vars?.x`,
132
136
  // `$vars['x']`, an alias) must stay a string here, or compile would refuse it.
133
- const lowered = tryLowerScriptFunction(`function ${name}($vars) {${code}}`, FLOW_SCRIPT_RUNTIME, { scopeReadsOnly: true });
137
+ const lowered = tryLowerScriptFunction(`function ${name}($vars, $metadata) {${code}}`, FLOW_SCRIPT_RUNTIME, { scopeReadsOnly: true });
134
138
  if (!lowered.ok) {
135
139
  // Worded as the SDK's list, not as a fault: whether a string body that reads
136
140
  // the name really fails in the run is what #1411 has yet to establish.
@@ -144,11 +148,21 @@ function printAsFunction(code, ctx) {
144
148
  }
145
149
  if (lowered.code !== code)
146
150
  return keep(UNCHANGED);
151
+ // A body that binds its own `$metadata` in the function's scope (`const`, or a
152
+ // `var`, which is no early error but would not type-check beside a
153
+ // `FlowMetadata` parameter) reads that local, so it prints without one.
154
+ const metadata = lowered.scopesRead.includes('$metadata') && !lowered.scopesDeclared.includes('$metadata');
147
155
  // Last, on what will actually be printed: an early error makes the module unloadable.
148
- const early = scriptBodyEarlyError(code, FLOW_SCRIPT_RUNTIME);
156
+ const early = scriptBodyEarlyError(code, metadata ? ['$vars', '$metadata'] : ['$vars']);
149
157
  if (early)
150
158
  return keep(`as a function in an ES module (strict code) it would not load: ${early}.`);
151
- ctx.declareFunction(`function ${name}($vars: ${ctx.typeHelper('FlowVars')}) {${code}}`);
159
+ // A key the engine does not set (a case may set it) is not on `FlowMetadata`,
160
+ // so the type is widened for it, or the printed module would not type-check.
161
+ const metadataType = () => (unknownMetadataKeys(code).length > 0
162
+ ? `${ctx.typeHelper('FlowMetadata')} & Record<string, any>`
163
+ : ctx.typeHelper('FlowMetadata'));
164
+ const params = [`$vars: ${ctx.typeHelper('FlowVars')}`, ...(metadata ? [`$metadata: ${metadataType()}`] : [])];
165
+ ctx.declareFunction(`function ${name}(${params.join(', ')}) {${code}}`);
152
166
  return name;
153
167
  }
154
168
  /** Key-sorted copy, so a property map compares by content and not by order. */
@@ -5,7 +5,14 @@
5
5
  import type { ScriptRuntime } from '../../../core/script-function.js';
6
6
  /**
7
7
  * What a Flow script body can read: the language's ES2020 built-ins, the
8
- * globals the engine registers, `console`, and the runtime's `$vars`.
8
+ * globals the engine registers, `console`, and the runtime's `$vars` and
9
+ * `$metadata`.
10
+ *
11
+ * The two scopes are the script task's two base inputs: Flow's converter gives
12
+ * every script task `vars = "=vars"` and `metadata = "=metadata"`, and rewrites
13
+ * `$vars`/`$metadata` in the body to those names (UiPath/flow-workbench,
14
+ * `bpmn-to-xml.ts` and `expression-transform.ts`); the engine binds each input
15
+ * as a global. A script function takes them as its parameters, in that order.
9
16
  *
10
17
  * Measured against UiPath/PO.BpmnEngine, which runs a script body with
11
18
  * Jint 4.4.0 (`ScriptTaskService.InvokeScriptAsync`,
@@ -20,7 +20,14 @@ const FLOW_SCRIPT_GLOBAL_MEMBERS = new Map([
20
20
  ]);
21
21
  /**
22
22
  * What a Flow script body can read: the language's ES2020 built-ins, the
23
- * globals the engine registers, `console`, and the runtime's `$vars`.
23
+ * globals the engine registers, `console`, and the runtime's `$vars` and
24
+ * `$metadata`.
25
+ *
26
+ * The two scopes are the script task's two base inputs: Flow's converter gives
27
+ * every script task `vars = "=vars"` and `metadata = "=metadata"`, and rewrites
28
+ * `$vars`/`$metadata` in the body to those names (UiPath/flow-workbench,
29
+ * `bpmn-to-xml.ts` and `expression-transform.ts`); the engine binds each input
30
+ * as a global. A script function takes them as its parameters, in that order.
24
31
  *
25
32
  * Measured against UiPath/PO.BpmnEngine, which runs a script body with
26
33
  * Jint 4.4.0 (`ScriptTaskService.InvokeScriptAsync`,
@@ -45,7 +52,16 @@ const FLOW_SCRIPT_GLOBAL_MEMBERS = new Map([
45
52
  * there is evidence the list is complete (#1411).
46
53
  */
47
54
  export const FLOW_SCRIPT_RUNTIME = {
48
- scopeGlobal: '$vars',
55
+ scopes: [
56
+ {
57
+ global: '$vars', param: 'vars', holds: 'the variable scope', reads: 'step', example: 'start.output.x',
58
+ checkedBy: 'the step renaming and reference checks',
59
+ },
60
+ {
61
+ global: '$metadata', param: 'metadata', holds: 'the instance metadata', reads: 'key', example: 'InstanceId',
62
+ checkedBy: 'the metadata key check',
63
+ },
64
+ ],
49
65
  name: 'the Flow script runtime',
50
66
  constGlobals: FLOW_SCRIPT_CONST_GLOBALS,
51
67
  globalMembers: FLOW_SCRIPT_GLOBAL_MEMBERS,
@@ -60,6 +60,39 @@ export interface ScriptInputs {
60
60
  * ```
61
61
  */
62
62
  export type FlowVars = Record<string, any>;
63
+ /**
64
+ * The instance metadata, as a script function's second parameter sees it.
65
+ *
66
+ * Like `$vars`, `$metadata` is the Flow runtime's own global: `script(fn)`
67
+ * renames the second parameter to it. Unlike `$vars` its shape does not depend
68
+ * on the flow: these are the keys the engine sets on every instance
69
+ * (UiPath/PO.BpmnEngine, `BpmnGeneric.workflow.cs` `MapToMetadataScopeData`, and
70
+ * `BpmnDataManager.UpsertElementCountDatum`). `check` warns on a read of any
71
+ * other key (`UNKNOWN_METADATA_KEY`).
72
+ *
73
+ * A case that starts the flow may set more. To read one of those, widen the
74
+ * parameter's type (`$metadata: FlowMetadata & { Region?: string }`).
75
+ *
76
+ * ```ts
77
+ * .step('tag', script(tag, { returns: 'string' }))
78
+ *
79
+ * function tag($vars: FlowVars, $metadata: FlowMetadata) {
80
+ * return $metadata.InstanceId + ':' + $vars.start.output.id;
81
+ * }
82
+ * ```
83
+ */
84
+ export interface FlowMetadata {
85
+ /** The key (UUID) of the Orchestrator folder the instance runs in. */
86
+ FolderKey: string;
87
+ /** The instance's id (UUID). */
88
+ InstanceId: string;
89
+ /** The key of the process the instance runs. */
90
+ ProcessKey: string;
91
+ /** The external id a case gives the instance; unset for a flow no case started. */
92
+ ExternalId?: string;
93
+ /** How many times each element has run, by element id; unset until the first count. */
94
+ ElementCount?: Record<string, number>;
95
+ }
63
96
  /** The TypeScript type one of {@link ScriptReturnType}'s names stands for. */
64
97
  export type ScriptValueOf<T extends ScriptReturnType> = T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : T extends 'array' ? readonly unknown[] : T extends 'object' ? object : unknown;
65
98
  /**