@uipath/maestro-builder-sdk 6.54.1 → 6.56.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.
@@ -0,0 +1,321 @@
1
+ import { FORM_FIELD_TYPES, formFieldJsonSchema, formFieldOptionsProblem, formFieldOptionsWhy, formFieldSettingProblems, formFieldSettingsWire, formFieldTypeHint, liftFormFieldSettings, validFormFieldTypesText, wireFormFieldType, } from '../core/form-field-type.js';
2
+ import { outcomeIdOf } from '../core/hitl-routing.js';
3
+ import { schemaId as stableSchemaId } from '../core/stable-id.js';
4
+ /** The `uipath:type` version both the converter and Studio Web write for `Actions.HITL`. */
5
+ const QUICK_TYPE_VERSION = 'v2';
6
+ /** The response row the app variant derives from the registry; the quick form keeps it. */
7
+ const RESPONSE_ROW = 'Process response';
8
+ const DRAFT_07 = 'http://json-schema.org/draft-07/schema#';
9
+ /**
10
+ * A field id has to be a plain, unreserved identifier, for two reasons.
11
+ *
12
+ * An answer is read back as the bare expression `=<id>`, and `=review-note` is a
13
+ * subtraction. And the local evaluator makes every key of the response a PARAMETER of
14
+ * one `new Function(...keys, body)`, so a single key that cannot be a parameter name —
15
+ * `review-note`, `default`, `true` — throws for every output row of the task, the
16
+ * outcome mapping included, and `applyOutputs` writes `undefined` for all of them.
17
+ */
18
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
19
+ /** Words a parameter cannot be named: ECMAScript's reserved words, strict-mode ones, and the literal-like globals. */
20
+ const RESERVED_WORDS = new Set([
21
+ 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else',
22
+ 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new',
23
+ 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with',
24
+ 'yield', 'let', 'static', 'implements', 'interface', 'package', 'private', 'protected', 'public',
25
+ 'arguments', 'eval', 'undefined', 'NaN', 'Infinity',
26
+ ]);
27
+ /** The response's own key for the outcome picked — an answer cannot claim it. */
28
+ const OUTCOME_KEY = 'Action';
29
+ /** Whether a field is shown to the reviewer (its value is sent). */
30
+ function isShown(f) {
31
+ return f.direction === 'input' || f.direction === 'inOut';
32
+ }
33
+ /** Whether a field comes back as an answer. */
34
+ function isAnswer(f) {
35
+ return f.direction === 'output' || f.direction === 'inOut';
36
+ }
37
+ /**
38
+ * A shown value as the form schema's `binding`: the variable PATH the designer reads
39
+ * (`vars.amount`), when the expression is one; otherwise the expression as written.
40
+ */
41
+ function bindingOf(value) {
42
+ const path = /^=(?:js:)?(vars\.[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)$/.exec(value);
43
+ return path ? path[1] : value;
44
+ }
45
+ /** An output row's type for a field: its JSON Schema type, with structures as `json`. */
46
+ function rowTypeOf(type) {
47
+ const json = formFieldJsonSchema(type)?.type ?? 'string';
48
+ return json === 'object' || json === 'array' ? 'json' : json;
49
+ }
50
+ /**
51
+ * Refuse a form the engine could not render, at the call that wrote it.
52
+ *
53
+ * @throws Error naming the task and what is wrong.
54
+ */
55
+ function validate(id, input) {
56
+ const { form } = input;
57
+ const where = `humanTask "${id}"`;
58
+ if (!Array.isArray(form.fields))
59
+ throw new Error(`${where}: form.fields must be an array.`);
60
+ if (!Array.isArray(form.outcomes) || form.outcomes.length === 0) {
61
+ throw new Error(`${where}: the form needs at least one outcome, e.g. outcomes: ['Approve', 'Reject'].`);
62
+ }
63
+ const ids = new Set();
64
+ for (const f of form.fields) {
65
+ if (typeof f.id !== 'string' || f.id === '')
66
+ throw new Error(`${where}: every form field needs an id.`);
67
+ if (ids.has(f.id))
68
+ throw new Error(`${where}: form field id "${f.id}" is used twice.`);
69
+ ids.add(f.id);
70
+ if (f.id === OUTCOME_KEY) {
71
+ throw new Error(`${where}: form field "${OUTCOME_KEY}" collides with the response's own "${OUTCOME_KEY}" — the outcome picked. Rename it, e.g. id: '${OUTCOME_KEY}Value'.`);
72
+ }
73
+ if (!IDENTIFIER.test(f.id)) {
74
+ throw new Error(`${where}: form field "${f.id}" (${f.direction}) is not a name the runtime can read back as =${f.id}. ` +
75
+ `Use an identifier (letters, digits, _ or $), e.g. id: '${f.id.replace(/\W+(\w)?/g, (_m, c) => (c ?? '').toUpperCase())}'.`);
76
+ }
77
+ if (RESERVED_WORDS.has(f.id)) {
78
+ throw new Error(`${where}: form field "${f.id}" is a reserved word, which the runtime cannot bind as a name — it would lose every answer and the outcome. Rename it, e.g. id: '${f.id}Value'.`);
79
+ }
80
+ // The SDK's own spellings only. The product also resolves platform aliases such as
81
+ // `System.String`, but those are not `FormFieldType`s: accepting one here would let a
82
+ // decompile lift write source that does not type-check, so such an import stays verbatim.
83
+ if (!FORM_FIELD_TYPES.includes(String(f.type))) {
84
+ throw new Error(`${where}: form field "${f.id}" has type "${String(f.type)}". Valid: ${validFormFieldTypesText()}. ${formFieldTypeHint(f.type)}`.trimEnd());
85
+ }
86
+ // The checks Flow's form checker reports as errors: a picker the reviewer cannot
87
+ // pick from, and a setting the platform's schema refuses. (Options on a field that
88
+ // renders no picker is a warning there, and is not refused here.)
89
+ if (formFieldOptionsProblem(f) === 'missing') {
90
+ throw new Error(`${where}: form field "${f.id}" ${formFieldOptionsWhy('missing', f.type)}. Add options: [{ id: 'o1', value: 'a', label: 'A' }, …].`);
91
+ }
92
+ const settings = formFieldSettingProblems(f);
93
+ if (settings.length)
94
+ throw new Error(`${where}: form field "${f.id}" sets ${settings.join('; ')}.`);
95
+ if (isShown(f) && (typeof f.value !== 'string' || !f.value.startsWith('='))) {
96
+ throw new Error(`${where}: form field "${f.id}" (${f.direction}) needs a value — a =-expression such as '=vars.${f.id}' — or the form shows it empty.`);
97
+ }
98
+ }
99
+ // One variable, one writer. Two answers landing in one variable, or an `outputs`
100
+ // mapping onto an answer's variable, both evaluate and the LAST row wins, so an answer
101
+ // is lost with no signal (`outputs: { decision: '=Action' }` beside a field named
102
+ // `decision` replaces the reviewer's text with the outcome name). The product's own
103
+ // converter never writes that shape (`applyHitlExtension` dedupes with `seenVars`).
104
+ const writers = new Map();
105
+ const claim = (variable, writer) => {
106
+ const first = writers.get(variable);
107
+ if (first !== undefined) {
108
+ throw new Error(`${where}: ${first} and ${writer} both write vars.${variable}, so one would overwrite the other. Give one a different variable.`);
109
+ }
110
+ writers.set(variable, writer);
111
+ };
112
+ claim(input.outputVar ?? `${id}_processResponse`, 'the task response (outputVar)');
113
+ for (const f of form.fields)
114
+ if (isAnswer(f))
115
+ claim(f.variable ?? f.id, `form field "${f.id}"`);
116
+ for (const v of Object.keys(input.outputs ?? {}))
117
+ claim(v, `outputs.${v}`);
118
+ const names = new Set();
119
+ const outcomeIds = new Map();
120
+ form.outcomes.forEach((o, i) => {
121
+ const name = typeof o === 'string' ? o : o?.name;
122
+ if (typeof name !== 'string' || name.trim() === '')
123
+ throw new Error(`${where}: every outcome needs a name.`);
124
+ if (names.has(name))
125
+ throw new Error(`${where}: outcome "${name}" is listed twice.`);
126
+ names.add(name);
127
+ // The schema's outcome ids must be unique too, and two names can reach one: an
128
+ // explicit `id` repeated, or `Needs review` and `Needs-review`, which both derive
129
+ // `needs-review`.
130
+ const outcomeId = outcomeIdOf(o, i);
131
+ const clash = outcomeIds.get(outcomeId);
132
+ if (clash !== undefined) {
133
+ throw new Error(`${where}: outcomes "${clash}" and "${name}" both have the id "${outcomeId}". Give one an explicit, different id: { name: '${name}', id: '…' }.`);
134
+ }
135
+ outcomeIds.set(outcomeId, name);
136
+ });
137
+ }
138
+ /**
139
+ * The rows a quick human task is written as.
140
+ *
141
+ * @param id - The task's element id.
142
+ * @param processId - The process id, which the default `hitlSchemaId` is derived from.
143
+ * @param input - The form and the task options.
144
+ * @returns The context, payload and output rows, the input schema and the type version.
145
+ * @throws Error when the form is malformed — see {@link validate}.
146
+ */
147
+ export function quickHumanTaskRows(id, processId, input) {
148
+ const { form } = input;
149
+ validate(id, input);
150
+ const schemaId = form.schemaId ?? stableSchemaId(processId, id);
151
+ const fields = form.fields.map((f) => {
152
+ const row = { id: f.id, label: f.label ?? f.id, type: wireFormFieldType(f.type), direction: f.direction };
153
+ if (isShown(f))
154
+ row.binding = bindingOf(f.value);
155
+ if (isAnswer(f)) {
156
+ row.variable = `vars.${f.variable ?? f.id}`;
157
+ if (typeof f.required === 'boolean')
158
+ row.required = f.required;
159
+ }
160
+ return { ...row, ...formFieldSettingsWire(f) };
161
+ });
162
+ const outcomes = form.outcomes.map((o, i) => ({
163
+ id: outcomeIdOf(o, i),
164
+ name: typeof o === 'string' ? o : o.name,
165
+ type: 'string',
166
+ isPrimary: i === 0,
167
+ action: 'Continue',
168
+ }));
169
+ const shown = form.fields.filter(isShown);
170
+ const bindings = Object.fromEntries(shown.map((f) => [f.id, bindingOf(f.value)]));
171
+ const contextRows = [
172
+ { name: 'hitlType', type: 'string', value: 'quick' },
173
+ { name: '__hitlFormSchema', type: 'json', body: { id: schemaId, fields, outcomes } },
174
+ { name: 'hitlSchemaId', type: 'string', value: schemaId },
175
+ ...(shown.length ? [{ name: '__hitlInputBindings', type: 'json', body: bindings }] : []),
176
+ { name: 'taskTitle', type: 'string', value: input.title ?? input.name ?? id },
177
+ ...(input.priority !== undefined ? [{ name: 'priority', type: 'string', value: input.priority }] : []),
178
+ ...(input.labels !== undefined ? [{ name: 'labels', type: 'string', value: input.labels }] : []),
179
+ ];
180
+ // What the form shows, as JSON Schema — the converter's `inputSchema`.
181
+ const properties = {};
182
+ const required = [];
183
+ for (const f of shown) {
184
+ properties[f.id] = { ...formFieldJsonSchema(f.type), title: f.label ?? f.id };
185
+ if (f.direction === 'inOut' && f.required === true)
186
+ required.push(f.id);
187
+ }
188
+ const inputSchema = { $schema: DRAFT_07, type: 'object', properties, ...(required.length ? { required } : {}) };
189
+ const inputRows = [
190
+ { name: 'HitlTaskArguments', type: 'json', target: 'bodyField', body: Object.fromEntries(shown.map((f) => [f.id, f.value])) },
191
+ ];
192
+ const fieldById = new Map(form.fields.map((f) => [f.id, f]));
193
+ const outputRows = [
194
+ { name: RESPONSE_ROW, type: 'Actions.HITL', var: input.outputVar ?? `${id}_processResponse` },
195
+ ...form.fields.filter(isAnswer).map((f) => {
196
+ const v = f.variable ?? f.id;
197
+ return { name: v, type: rowTypeOf(f.type), source: `=${f.id}`, var: v, custom: true };
198
+ }),
199
+ ];
200
+ const mappedOutputRows = Object.entries(input.outputs ?? {}).map(([v, source]) => {
201
+ // The fallback type: an answer's field type for `=<fieldId>`, `string` otherwise
202
+ // (`=Action`, the outcome NAME, is a string). A declared variable overrides it.
203
+ const field = /^=([A-Za-z_$][\w$]*)$/.exec(source)?.[1];
204
+ const f = field === undefined ? undefined : fieldById.get(field);
205
+ return { name: v, type: f ? rowTypeOf(f.type) : 'string', source, var: v };
206
+ });
207
+ return { typeVersion: QUICK_TYPE_VERSION, contextRows, inputSchema, inputRows, outputRows, mappedOutputRows };
208
+ }
209
+ /**
210
+ * Rows as comparable text: each row's own keys sorted (a row read from XML and a row
211
+ * built here need not list them in the same order), but every body as its exact TEXT.
212
+ * A body that parses to the same value with other whitespace or key order would compile
213
+ * back to different bytes, so it must not count as a match.
214
+ */
215
+ function rowsText(rows) {
216
+ return JSON.stringify(rows.map((r) => {
217
+ const entries = Object.entries(r)
218
+ .filter(([, v]) => v !== undefined)
219
+ .map(([k, v]) => [k, k === 'body' || k === 'schema' ? textOf(v) : v]);
220
+ return Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b)));
221
+ }));
222
+ }
223
+ /** A body as the text it is written as — a value is JSON-stringified, as the serializer does. */
224
+ function textOf(body) {
225
+ return typeof body === 'string' ? body : JSON.stringify(body);
226
+ }
227
+ /** Parse a json body read from XML, or `undefined` when it is not JSON. */
228
+ function bodyValue(body) {
229
+ if (typeof body !== 'string')
230
+ return body;
231
+ try {
232
+ return JSON.parse(body);
233
+ }
234
+ catch {
235
+ return undefined;
236
+ }
237
+ }
238
+ /**
239
+ * Read a quick task back as `.humanTask({ form })` options — or `undefined` when its
240
+ * rows are anything but what those options compile to.
241
+ *
242
+ * Best effort by construction, and safe because of the last step: the lifted options
243
+ * are run through {@link quickHumanTaskRows} and the result must equal the node's rows,
244
+ * order and all. A Studio Web task (sidecar schema, `_schemaFileId`, no response row)
245
+ * fails that and stays the verbatim `.activity()` it was.
246
+ *
247
+ * @param node - The node's rows, as decompile read them.
248
+ * @param processId - The id the decompiled `bpmn(…)` call will carry.
249
+ * @param authoredVarType - The wire type of each variable the decompiled source will
250
+ * DECLARE with `.var()` — what the serializer types a mapped `outputs` row from on the
251
+ * way back. A variable decompile folds away is not one: its row is re-typed from the
252
+ * fallback, so it has to already carry that type to match.
253
+ * @returns The options, or `undefined` to keep the node verbatim.
254
+ */
255
+ export function liftQuickHumanTask(node, processId, authoredVarType = () => undefined) {
256
+ const ctx = new Map(node.contextRows.map((r) => [r.name, r]));
257
+ if (ctx.get('hitlType')?.value !== 'quick')
258
+ return undefined;
259
+ const schema = bodyValue(ctx.get('__hitlFormSchema')?.body);
260
+ if (!schema || !Array.isArray(schema.fields) || !Array.isArray(schema.outcomes))
261
+ return undefined;
262
+ const args = bodyValue(node.inputRows.find((r) => r.name === 'HitlTaskArguments')?.body);
263
+ let lifted;
264
+ try {
265
+ const fields = schema.fields.map((row) => {
266
+ const id = String(row.id);
267
+ const base = {
268
+ id,
269
+ ...(row.label !== undefined && row.label !== id ? { label: String(row.label) } : {}),
270
+ type: row.type,
271
+ ...liftFormFieldSettings(row),
272
+ };
273
+ const variable = typeof row.variable === 'string' ? row.variable.replace(/^vars\./, '') : undefined;
274
+ const answerVar = variable !== undefined && variable !== id ? { variable } : {};
275
+ const required = typeof row.required === 'boolean' ? { required: row.required } : {};
276
+ if (row.direction === 'input')
277
+ return { ...base, direction: 'input', value: String(args?.[id]) };
278
+ if (row.direction === 'inOut')
279
+ return { ...base, direction: 'inOut', value: String(args?.[id]), ...required, ...answerVar };
280
+ return { ...base, direction: 'output', ...required, ...answerVar };
281
+ });
282
+ const outcomes = schema.outcomes.map((o, i) => {
283
+ const name = String(o.name);
284
+ return outcomeIdOf(name, i) === o.id ? name : { name, id: String(o.id) };
285
+ });
286
+ const schemaId = String(schema.id);
287
+ const response = node.outputRows[0];
288
+ const answerVars = new Set(fields.filter((f) => f.direction !== 'input').map((f) => f.variable ?? f.id));
289
+ const extra = node.outputRows.slice(1).filter((r) => r.var !== undefined && !answerVars.has(r.var) && !r.custom);
290
+ lifted = {
291
+ form: { fields, outcomes, ...(schemaId !== stableSchemaId(processId, node.id) ? { schemaId } : {}) },
292
+ ...(node.name !== undefined ? { name: node.name } : {}),
293
+ ...(ctx.get('taskTitle')?.value !== (node.name ?? node.id) ? { title: ctx.get('taskTitle')?.value } : {}),
294
+ ...(ctx.has('priority') ? { priority: ctx.get('priority').value } : {}),
295
+ ...(ctx.has('labels') ? { labels: ctx.get('labels').value } : {}),
296
+ ...(extra.length ? { outputs: Object.fromEntries(extra.map((r) => [r.var, r.source ?? ''])) } : {}),
297
+ ...(response?.var !== undefined && response.var !== `${node.id}_processResponse` ? { outputVar: response.var } : {}),
298
+ };
299
+ }
300
+ catch {
301
+ return undefined;
302
+ }
303
+ let rebuilt;
304
+ try {
305
+ rebuilt = quickHumanTaskRows(node.id, processId, lifted);
306
+ }
307
+ catch {
308
+ return undefined;
309
+ }
310
+ const same = (node.typeVersion ?? 'v1') === rebuilt.typeVersion &&
311
+ rowsText(node.contextRows) === rowsText(rebuilt.contextRows) &&
312
+ node.inputSchema !== undefined &&
313
+ textOf(node.inputSchema) === textOf(rebuilt.inputSchema) &&
314
+ rowsText(node.inputRows) === rowsText(rebuilt.inputRows) &&
315
+ rowsText(node.outputRows) ===
316
+ rowsText([
317
+ ...rebuilt.outputRows,
318
+ ...rebuilt.mappedOutputRows.map((r) => ({ ...r, type: (r.var !== undefined ? authoredVarType(r.var) : undefined) ?? r.type })),
319
+ ]);
320
+ return same ? lifted : undefined;
321
+ }
@@ -470,6 +470,7 @@ function createNode(ctx, n) {
470
470
  skipCondition: n.skipCondition,
471
471
  contextVerbatim: n.contextVerbatim,
472
472
  outputRows: n.outputRows,
473
+ mappedOutputRows: n.mappedOutputRows,
473
474
  }, (varId) => ctx.authoredVarType.get(varId)), n);
474
475
  case 'subProcess':
475
476
  return applyActivityFields(ctx, createSubProcess(ctx, n), n);
@@ -822,6 +823,13 @@ export function implicitNodeVars(nodes, declared) {
822
823
  // is what keeps a node's own outputs inside the node: 83 of the 112 declarations
823
824
  // in `claims-intake-hitl` are this shape, and every one of them had to be
824
825
  // restated as a `.var()` because the derivation could not reach the row.
826
+ // A mapped row's own type is its fallback, so an undeclared variable is declared
827
+ // as the row is written; a declared one keeps its own (`claim` skips it). First, so
828
+ // `typedNodeDeclaredVars` below, which lists them too, cannot claim them untyped.
829
+ for (const row of n.mappedOutputRows ?? []) {
830
+ const d = rowDeclaration(row);
831
+ claim(row.var, n.id, d.type, d);
832
+ }
825
833
  if (n.outputRows) {
826
834
  for (const row of n.outputRows) {
827
835
  const d = rowDeclaration(row);
@@ -376,6 +376,12 @@ export interface TypedNodeInput {
376
376
  * for a caller that already knows the exact rows.
377
377
  */
378
378
  outputRows?: TypedOutputRow[];
379
+ /**
380
+ * Rows written after everything else, each typed from the author-declared variable it
381
+ * writes (the `varType` callback), its own `type` otherwise. Unlike `outputs` these
382
+ * survive `outputRows`; `.humanTask({ form })` uses them for its `outputs`.
383
+ */
384
+ mappedOutputRows?: TypedOutputRow[];
379
385
  /** `=`-expression that skips the node when truthy (activity/event tags only). */
380
386
  skipCondition?: string;
381
387
  /**
@@ -556,6 +556,7 @@ varType) {
556
556
  // TypedNodeInput.outputRows — this is what lets a connector's response schema
557
557
  // survive a round trip.
558
558
  rows.push(...verbatimOutputRows(m, input.outputRows));
559
+ rows.push(...mappedRows(m, input.mappedOutputRows, varType));
559
560
  if (rows.length)
560
561
  payload.output = rows;
561
562
  return finish(m, spec, id, input.name, payload);
@@ -588,10 +589,15 @@ varType) {
588
589
  // which is what every row did before.
589
590
  rows.push(m.create('uipath:Output', { name: varId, type: varType?.(varId) ?? 'string', source, var: varId }));
590
591
  }
592
+ rows.push(...mappedRows(m, input.mappedOutputRows, varType));
591
593
  if (rows.length)
592
594
  payload.output = rows;
593
595
  return finish(m, spec, id, input.name, payload);
594
596
  }
597
+ /** `TypedNodeInput.mappedOutputRows`, each typed from its declared variable when there is one. */
598
+ function mappedRows(m, rows, varType) {
599
+ return verbatimOutputRows(m, (rows ?? []).map((r) => ({ ...r, type: (r.var !== undefined ? varType?.(r.var) : undefined) ?? r.type })));
600
+ }
595
601
  /** `uipath:Output` rows exactly as the caller spelled them, deriving nothing. */
596
602
  export function verbatimOutputRows(m, rows) {
597
603
  return rows.map((row) => {
@@ -1123,8 +1123,8 @@ declare class TaskBuilder {
1123
1123
  type?: TypeDesc;
1124
1124
  }>): this;
1125
1125
  /**
1126
- * Extract fields of the task's **result** into case variables (reference-mode
1127
- * tasks). Each key is the case-variable name a later task/condition reads as
1126
+ * Extract fields of the task's **result** into case variables (reference-mode,
1127
+ * connector and wait-for-connector tasks). Each key is the case-variable name a later task/condition reads as
1128
1128
  * `=vars.<name>`; each value is the source field expression (e.g. `=response`,
1129
1129
  * `=Error.Message`). Pass `{ source, type }` to set a non-string type. Emits a
1130
1130
  * `data.outputs[]` row plus a root `inputOutputs` companion so the name resolves.
@@ -1150,6 +1150,11 @@ declare class TaskBuilder {
1150
1150
  * later output's (`check` reports `OUTPUT_SELF_REFERENCE` /
1151
1151
  * `OUTPUT_FORWARD_READ`).
1152
1152
  *
1153
+ * On a connector or wait-for-connector task, a field is read from the
1154
+ * operation's (or event's) `response`: an SDD row `app_id -> postAppId` is
1155
+ * `.outputs({ postAppId: '=response.app_id' })`. Each extract is written after
1156
+ * the task's own `response` and `Error` rows.
1157
+ *
1153
1158
  * @example
1154
1159
  * **Extract a result field and assign two custom outputs**
1155
1160
  * ```ts
@@ -474,8 +474,8 @@ class TaskBuilder {
474
474
  return this;
475
475
  }
476
476
  /**
477
- * Extract fields of the task's **result** into case variables (reference-mode
478
- * tasks). Each key is the case-variable name a later task/condition reads as
477
+ * Extract fields of the task's **result** into case variables (reference-mode,
478
+ * connector and wait-for-connector tasks). Each key is the case-variable name a later task/condition reads as
479
479
  * `=vars.<name>`; each value is the source field expression (e.g. `=response`,
480
480
  * `=Error.Message`). Pass `{ source, type }` to set a non-string type. Emits a
481
481
  * `data.outputs[]` row plus a root `inputOutputs` companion so the name resolves.
@@ -501,6 +501,11 @@ class TaskBuilder {
501
501
  * later output's (`check` reports `OUTPUT_SELF_REFERENCE` /
502
502
  * `OUTPUT_FORWARD_READ`).
503
503
  *
504
+ * On a connector or wait-for-connector task, a field is read from the
505
+ * operation's (or event's) `response`: an SDD row `app_id -> postAppId` is
506
+ * `.outputs({ postAppId: '=response.app_id' })`. Each extract is written after
507
+ * the task's own `response` and `Error` rows.
508
+ *
504
509
  * @example
505
510
  * **Extract a result field and assign two custom outputs**
506
511
  * ```ts
@@ -549,13 +554,23 @@ class TaskBuilder {
549
554
  _build() {
550
555
  if (!this._kind)
551
556
  throw new Error(`task "${this._name}" has no kind — call .process()/.agent()/.rpa()/.action()/.connector()/.waitForTimer()/… once.`);
552
- // Task I/O binding is reference-mode only (REF_IO). Connector tasks derive
553
- // their own inputs/outputs from the operation schema.
557
+ // Task input binding is reference-mode only (REF_IO): a connector task takes
558
+ // its inputs in `.connector()` / `.waitForConnector()`. Output extracts also
559
+ // work on connector and wait-for-connector tasks: the task writes its own
560
+ // `response` and `Error` rows, and each extract (`=response.<field>`) is one
561
+ // more row after them, as `uip maestro case sdd convert` and `case splice`
562
+ // write an SDD row `app_id -> postAppId`.
554
563
  const REF_IO = new Set(['process', 'agent', 'rpa', 'api-workflow', 'case-management', 'flow-process', 'function', 'business-rule']);
555
- if ((this._inputs || this._outputs) && !REF_IO.has(this._kind))
556
- throw new Error(`task "${this._name}": .inputs()/.outputs() are only supported on reference-mode tasks (process/agent/rpa/api-workflow/case-management/flow-process/function/business-rule), not "${this._kind}".`);
557
- if (!this._ref && (this._inputs || this._outputs))
564
+ const CONNECTOR_OUTPUTS = new Set(['connector', 'wait-for-connector']);
565
+ if (this._inputs && !REF_IO.has(this._kind))
566
+ throw new Error(`task "${this._name}": .inputs()/.outputs() are only supported on reference-mode tasks (process/agent/rpa/api-workflow/case-management/flow-process/function/business-rule), not "${this._kind}". A connector task takes its inputs in .connector() / .waitForConnector().`);
567
+ if (this._outputs && !REF_IO.has(this._kind) && !CONNECTOR_OUTPUTS.has(this._kind))
568
+ throw new Error(`task "${this._name}": .outputs() is only supported on reference-mode tasks (process/agent/rpa/api-workflow/case-management/flow-process/function/business-rule) and on connector / wait-for-connector tasks, not "${this._kind}".`);
569
+ if (REF_IO.has(this._kind) && !this._ref && (this._inputs || this._outputs))
558
570
  throw new Error(`task "${this._name}": an unresolved reference task cannot declare inputs or outputs.`);
571
+ if (this._kind === 'wait-for-connector' && this._outputs
572
+ && !(this._waitConnector && 'connector' in this._waitConnector && 'event' in this._waitConnector))
573
+ throw new Error(`task "${this._name}": a placeholder wait-for-connector task has no event response to extract outputs from — name the event (.waitForConnector(<trigger descriptor>) or { connector, event }).`);
559
574
  return {
560
575
  name: this._name,
561
576
  kind: this._kind,
@@ -597,6 +597,20 @@ function checkStage(stage, diags, refStage, refPlacement, bindings) {
597
597
  });
598
598
  }
599
599
  }
600
+ // A connector task's result is its `response` (and `Error`): `=app_id`
601
+ // names nothing at run time. The field is `=response.app_id`, the source
602
+ // `uip maestro case sdd convert` writes for the SDD row `app_id -> postAppId`.
603
+ if (t.kind === 'connector' || t.kind === 'wait-for-connector')
604
+ for (const o of t.outputs ?? [])
605
+ // A custom row (`{ assign }`, builder-sdk#1438) sets a variable from an
606
+ // expression, not from the response, so it is not an extract.
607
+ if (!o.custom && !/^=\s*(response|Error)(?=$|[.[])/.test(o.source))
608
+ diags.push({
609
+ level: 'error',
610
+ code: 'CONNECTOR_OUTPUT_SOURCE',
611
+ message: `${t.kind === 'connector' ? 'Connector' : 'Wait-for-connector'} task "${t.name}" output "${o.var}" reads "${o.source}". A connector task's result is its response: write "=response.${o.source.replace(/^=\s*/, '')}" (or "=Error.<field>" for the error).`,
612
+ where: t.name,
613
+ });
600
614
  if (t.kind === 'flow-process' && (!t.ref || !t.ref.name.trim()))
601
615
  diags.push({
602
616
  level: 'error',
@@ -782,6 +782,28 @@ function actionApp(data, refs, task) {
782
782
  function isAuthorableBusinessRule(task) {
783
783
  return task.type === 'business-rule' && task.data?.version === 'v3';
784
784
  }
785
+ /** Task kinds whose own `response` / `Error` rows can be followed by `.outputs()` extracts. */
786
+ const CONNECTOR_EXTRACT_KINDS = new Set(['execute-connector-activity', 'wait-for-connector']);
787
+ /**
788
+ * A connector or wait-for-connector task's extract rows: every output but the
789
+ * `response` and `Error` rows `.connector()` / `.waitForConnector()` re-emit
790
+ * themselves. Dropping them lost every SDD extract on a round trip.
791
+ */
792
+ function connectorExtracts(task) {
793
+ const rows = Array.isArray(task.data?.outputs) ? task.data.outputs : [];
794
+ return rows.filter((row) => !isConnectorOwnOutput(row));
795
+ }
796
+ /**
797
+ * A connector's own `response` / `Error` row, which the builder re-emits
798
+ * itself. Its id and source may carry a plan-wide suffix (`=response2`,
799
+ * builder-sdk#518), so the source is matched with the suffix, the same test
800
+ * the trigger path uses. An exact match made a suffixed own row look like an
801
+ * extract, which `check` then refused (CONNECTOR_OUTPUT_SOURCE).
802
+ */
803
+ function isConnectorOwnOutput(row) {
804
+ return (row.name === 'response' && /^=response\d*$/.test(String(row.source ?? '')))
805
+ || (row.name === 'Error' && /^=Error\d*$/.test(String(row.source ?? '')));
806
+ }
785
807
  /** The two outputs every business-rule task carries, which `.businessRule()` re-emits itself. */
786
808
  function isBusinessRuleOwnOutput(row) {
787
809
  return (row.name === 'output' && row.source === '=result') || (row.name === 'Error' && row.source === '=Error');
@@ -837,6 +859,11 @@ function emitTask(out, depth, task, lane, refs, library) {
837
859
  if (extracts.length)
838
860
  out.line(inner, `.outputs(${ioArg(extracts, 'outputs')})`);
839
861
  }
862
+ if (CONNECTOR_EXTRACT_KINDS.has(String(task.type))) {
863
+ const extracts = connectorExtracts(task);
864
+ if (extracts.length)
865
+ out.line(inner, `.outputs(${ioArg(extracts, 'outputs')})`);
866
+ }
840
867
  if (task.isRequired !== undefined)
841
868
  out.line(inner, task.isRequired ? '.required()' : '.required(false)');
842
869
  if (task.shouldRunOnlyOnce !== undefined)
@@ -921,8 +948,7 @@ function varSpec(entry) {
921
948
  function isResolvedConnectorOutput(row, triggerId, resolved) {
922
949
  if (!resolved || row.elementId !== triggerId)
923
950
  return false;
924
- return (row.name === 'response' && /^=response\d*$/.test(String(row.source ?? '')))
925
- || (row.name === 'Error' && /^=Error\d*$/.test(String(row.source ?? '')));
951
+ return isConnectorOwnOutput(row);
926
952
  }
927
953
  function producedVariableNames(plan) {
928
954
  const produced = new Set();
@@ -955,6 +981,12 @@ function producedVariableNames(plan) {
955
981
  if (row.var && !isBusinessRuleOwnOutput(row))
956
982
  produced.add(row.var);
957
983
  }
984
+ else if (CONNECTOR_EXTRACT_KINDS.has(String(task.type))) {
985
+ // A connector extract registers its case variable on recompile too.
986
+ for (const row of connectorExtracts(task))
987
+ if (row.var)
988
+ produced.add(row.var);
989
+ }
958
990
  }
959
991
  }
960
992
  // An own-name row that carries `originalVar` (`{ id: 'estimatedAge', var:
@@ -1002,7 +1002,7 @@ function taskData(task, resolver, taskId, stageId, ids) {
1002
1002
  // `task.elementId === output.elementId` filter matched none of them and the
1003
1003
  // task's `response` / `Error` never reached a later task (#1419).
1004
1004
  case 'connector':
1005
- return connectorTaskData(task.connector, resolver, `${stageId}-${taskId}`, `Connector task "${task.name}"`);
1005
+ return withOutputExtracts(connectorTaskData(task.connector, resolver, `${stageId}-${taskId}`, `Connector task "${task.name}"`), task, ids, resolver);
1006
1006
  case 'external-agent':
1007
1007
  case 'external-workflow':
1008
1008
  return externalTaskData(task.kind, task.external, resolver, `${stageId}-${taskId}`, `${task.kind === 'external-agent' ? 'External Agent' : 'External Workflow'} task "${task.name}"`);
@@ -1018,9 +1018,33 @@ function taskData(task, resolver, taskId, stageId, ids) {
1018
1018
  case 'wait-for-connector':
1019
1019
  // Floor placeholder (no connector named) = `serviceType` only; naming the
1020
1020
  // connector/operation emits the `context` subscription.
1021
- return waitConnectorBag(task.waitConnector, { alwaysContext: false }, resolver, `${stageId}-${taskId}`, `Wait-for-connector task "${task.name}"`);
1021
+ return withOutputExtracts(waitConnectorBag(task.waitConnector, { alwaysContext: false }, resolver, `${stageId}-${taskId}`, `Wait-for-connector task "${task.name}"`), task, ids, resolver);
1022
1022
  }
1023
1023
  }
1024
+ /**
1025
+ * A connector or wait-for-connector task's `.outputs()` extracts, appended to
1026
+ * the `response` / `Error` rows the task writes itself. Each is an ordinary task
1027
+ * output (`emitTaskOutput`) on the same `elementId` as those rows, e.g. for
1028
+ * `.outputs({ postAppId: '=response.app_id' })`:
1029
+ * { name: 'app_id', type: 'string', id: 'o…', var: 'postAppId', originalVar: 'o…',
1030
+ * value: 'postAppId', source: '=response.app_id', target: '=o…', elementId }
1031
+ * That is the row `uip maestro case sdd convert` writes for the SDD row
1032
+ * `app_id -> postAppId`, and `validate --strict --sdd` matches it on
1033
+ * `source` + `var` (STRICT_SDD_EXTRACT_MISSING).
1034
+ */
1035
+ function withOutputExtracts(data, task, ids, resolver) {
1036
+ if (!task.outputs?.length)
1037
+ return data;
1038
+ const own = Array.isArray(data.outputs) ? data.outputs : [];
1039
+ const elementId = own.find((row) => typeof row.elementId === 'string')?.elementId;
1040
+ if (!elementId)
1041
+ throw new Error(`task "${task.name}": output extracts need the task's resolved response output.`);
1042
+ data.outputs = [
1043
+ ...own,
1044
+ ...task.outputs.map((o, i) => emitTaskOutput(o, elementId, ids, `${elementId}:out:${i}:${o.var}`, resolver)),
1045
+ ];
1046
+ return data;
1047
+ }
1024
1048
  /**
1025
1049
  * The `wait-for-connector` subscription bag (`data` on the task, `uipath` on the
1026
1050
  * rule). Verified against a real `uip maestro case` caseplan: a placeholder
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.54.1",
3
+ "version": "6.56.0",
4
4
  "description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uipath.com/maestro",
@@ -96,5 +96,5 @@
96
96
  "@types/node": "^22.7.0",
97
97
  "esbuild": "^0.28.1"
98
98
  },
99
- "gitref": "5dfee12e621c69cfa2cf1338fc7df299768dbe16"
99
+ "gitref": "31230281aaf4fa5e44d2844c08747ead2814e94d"
100
100
  }