@uipath/maestro-builder-sdk 6.33.2 → 6.35.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.
@@ -372,7 +372,7 @@ export interface BuiltCaseExitCondition {
372
372
  marksCaseComplete?: boolean;
373
373
  rules: CaseRule[][];
374
374
  }
375
- export type TaskKind = 'process' | 'agent' | 'rpa' | 'api-workflow' | 'case-management' | 'flow-process' | 'external-agent' | 'external-workflow' | 'action' | 'connector' | 'wait-for-timer' | 'wait-for-connector';
375
+ export type TaskKind = 'process' | 'agent' | 'rpa' | 'api-workflow' | 'case-management' | 'flow-process' | 'business-rule' | 'external-agent' | 'external-workflow' | 'action' | 'connector' | 'wait-for-timer' | 'wait-for-connector';
376
376
  /** Published-resource task kinds that the Case schema permits as unresolved skeletons. */
377
377
  export type UnresolvedReferenceTaskKind = 'process' | 'agent' | 'rpa' | 'api-workflow' | 'case-management';
378
378
  /**
@@ -428,6 +428,25 @@ export interface TaskRef {
428
428
  name: string;
429
429
  folderPath: string;
430
430
  }
431
+ /**
432
+ * Options for {@link TaskBuilder.businessRule}.
433
+ */
434
+ export interface BusinessRuleOptions {
435
+ /** The Orchestrator folder the rule is deployed in, e.g. `Shared`. */
436
+ folder: string;
437
+ /**
438
+ * The rule's whole result schema, as `uip maestro case tasks describe --type
439
+ * business-rule` returns it on the `output` row (`_jsonSchema`): one object
440
+ * keyed by decision name, each holding that decision's columns. Optional; it
441
+ * is copied onto the `output` row so the designer can show the columns.
442
+ */
443
+ resultSchema?: unknown;
444
+ }
445
+ /** Normalized business-rule configuration stored on a built task. */
446
+ export interface BusinessRuleSpecData {
447
+ /** The rule's decision-keyed result schema, when the author supplied it. */
448
+ resultSchema?: unknown;
449
+ }
431
450
  /**
432
451
  * Action-task recipient type. `2` = a single user by email — the only value the
433
452
  * platform corpus exercises for case action tasks; `0`/`1`/`3` are reserved for
@@ -520,6 +539,8 @@ export interface BuiltTask {
520
539
  name: string;
521
540
  kind: TaskKind;
522
541
  ref?: TaskRef;
542
+ /** business-rule task: the rule's decision-keyed result schema, when known. */
543
+ businessRule?: BusinessRuleSpecData;
523
544
  action?: ActionSpecData;
524
545
  connector?: ConnectorSpecData;
525
546
  /** Descriptor-resolved external agent/workflow configuration. */
@@ -718,6 +739,7 @@ declare class TaskBuilder {
718
739
  private readonly _name;
719
740
  private _kind?;
720
741
  private _ref?;
742
+ private _businessRule?;
721
743
  private _action?;
722
744
  private _connector?;
723
745
  private _external?;
@@ -808,6 +830,35 @@ declare class TaskBuilder {
808
830
  flowProcess(name: string, opts?: {
809
831
  folder?: string;
810
832
  }): this;
833
+ /**
834
+ * Evaluate a deployed **business rule** (a DMN decision package) and read its
835
+ * decisions back.
836
+ *
837
+ * @remarks
838
+ * Emits a `business-rule` task at `version: 'v3'` (below v3 the engine cannot
839
+ * run the `.uirule` package a BusinessRules project deploys). The rule is named
840
+ * through a `name` + `folderPath` pair of root bindings with
841
+ * `resource: 'BusinessRule'` and `resourceKey: '<folder>.<name>'`, the shape
842
+ * Studio Web and `uip maestro case bindings sync` write.
843
+ *
844
+ * The task always carries the two outputs every business-rule task has: `output`
845
+ * (`source: '=result'`, the whole decision-keyed result) and `Error`. To store
846
+ * one decision column in a case variable, add an extract with `.outputs()`
847
+ * whose source names the decision and column directly — `=<Decision>.<column>`,
848
+ * never `=result.…` or `=output.…`:
849
+ *
850
+ * ```ts
851
+ * .task('Score Credit Risk', t => t
852
+ * .businessRule('CreditRisk', { folder: 'Shared' })
853
+ * .inputs({ creditScore: { value: '=vars.creditScore', type: 'number' } })
854
+ * .outputs({ riskBand: { source: '=RiskDecision.riskBand', type: 'string' } }))
855
+ * ```
856
+ *
857
+ * @param name - The deployed rule's name.
858
+ * @param opts - `folder` (required) and the optional `resultSchema`.
859
+ * @returns This builder, so calls chain.
860
+ */
861
+ businessRule(name: string, opts: BusinessRuleOptions): this;
811
862
  private external;
812
863
  /**
813
864
  * Invoke an external agent through its generated Integration Service descriptor.
@@ -160,6 +160,7 @@ class TaskBuilder {
160
160
  _name;
161
161
  _kind;
162
162
  _ref;
163
+ _businessRule;
163
164
  _action;
164
165
  _connector;
165
166
  _external;
@@ -263,6 +264,39 @@ class TaskBuilder {
263
264
  flowProcess(name, opts) {
264
265
  return this.reference('flow-process', name, opts);
265
266
  }
267
+ /**
268
+ * Evaluate a deployed **business rule** (a DMN decision package) and read its
269
+ * decisions back.
270
+ *
271
+ * @remarks
272
+ * Emits a `business-rule` task at `version: 'v3'` (below v3 the engine cannot
273
+ * run the `.uirule` package a BusinessRules project deploys). The rule is named
274
+ * through a `name` + `folderPath` pair of root bindings with
275
+ * `resource: 'BusinessRule'` and `resourceKey: '<folder>.<name>'`, the shape
276
+ * Studio Web and `uip maestro case bindings sync` write.
277
+ *
278
+ * The task always carries the two outputs every business-rule task has: `output`
279
+ * (`source: '=result'`, the whole decision-keyed result) and `Error`. To store
280
+ * one decision column in a case variable, add an extract with `.outputs()`
281
+ * whose source names the decision and column directly — `=<Decision>.<column>`,
282
+ * never `=result.…` or `=output.…`:
283
+ *
284
+ * ```ts
285
+ * .task('Score Credit Risk', t => t
286
+ * .businessRule('CreditRisk', { folder: 'Shared' })
287
+ * .inputs({ creditScore: { value: '=vars.creditScore', type: 'number' } })
288
+ * .outputs({ riskBand: { source: '=RiskDecision.riskBand', type: 'string' } }))
289
+ * ```
290
+ *
291
+ * @param name - The deployed rule's name.
292
+ * @param opts - `folder` (required) and the optional `resultSchema`.
293
+ * @returns This builder, so calls chain.
294
+ */
295
+ businessRule(name, opts) {
296
+ this.reference('business-rule', name, { folder: opts.folder });
297
+ this._businessRule = opts.resultSchema === undefined ? {} : { resultSchema: opts.resultSchema };
298
+ return this;
299
+ }
266
300
  external(kind, descriptor, opts) {
267
301
  this.setKind(kind);
268
302
  this._external = {
@@ -448,15 +482,16 @@ class TaskBuilder {
448
482
  // Task I/O binding is reference-mode only — process/agent/rpa/api-workflow,
449
483
  // case-management, and flow-process. Connector tasks derive their own
450
484
  // inputs/outputs from the operation schema.
451
- const REF_IO = new Set(['process', 'agent', 'rpa', 'api-workflow', 'case-management', 'flow-process']);
485
+ const REF_IO = new Set(['process', 'agent', 'rpa', 'api-workflow', 'case-management', 'flow-process', 'business-rule']);
452
486
  if ((this._inputs || this._outputs) && !REF_IO.has(this._kind))
453
- throw new Error(`task "${this._name}": .inputs()/.outputs() are only supported on reference-mode tasks (process/agent/rpa/api-workflow/case-management/flow-process), not "${this._kind}".`);
487
+ throw new Error(`task "${this._name}": .inputs()/.outputs() are only supported on reference-mode tasks (process/agent/rpa/api-workflow/case-management/flow-process/business-rule), not "${this._kind}".`);
454
488
  if (!this._ref && (this._inputs || this._outputs))
455
489
  throw new Error(`task "${this._name}": an unresolved reference task cannot declare inputs or outputs.`);
456
490
  return {
457
491
  name: this._name,
458
492
  kind: this._kind,
459
493
  ref: this._ref,
494
+ businessRule: this._businessRule,
460
495
  action: this._action,
461
496
  connector: this._connector,
462
497
  external: this._external,
@@ -483,6 +483,36 @@ function checkStage(stage, diags, refStage, refPlacement) {
483
483
  message: `Task "${t.name}" binding "${'var' in invalidBinding ? invalidBinding.var : invalidBinding.name}" has unsupported type ${JSON.stringify(invalidBinding.type)}. Use a type name such as "string" or "jsonSchema"; jsonSchema(...) is only for declaring a structured case variable.`,
484
484
  where: t.name,
485
485
  });
486
+ if (t.kind === 'business-rule') {
487
+ if (!t.ref || !t.ref.name.trim() || !t.ref.folderPath.trim())
488
+ diags.push({
489
+ level: 'error',
490
+ code: 'BUSINESS_RULE_EMPTY_DATA',
491
+ message: `Business-rule task "${t.name}" must name its deployed rule and the Orchestrator folder it is deployed in: .businessRule('<rule>', { folder: '<folder>' }).`,
492
+ where: t.name,
493
+ });
494
+ for (const o of t.outputs ?? []) {
495
+ const wrapped = /^=\s*(result|output)\./.exec(o.source);
496
+ if (wrapped)
497
+ diags.push({
498
+ level: 'error',
499
+ code: 'BUSINESS_RULE_OUTPUT_SOURCE',
500
+ message: `Business-rule task "${t.name}" output "${o.var}" reads "${o.source}". An extract reads a field of the rule's result directly — write "=${o.source.replace(/^=\s*(result|output)\./, '')}" (\`=<Decision>.<column>\`). The whole result is already the task's own \`output\` row (\`=result\`); \`=${wrapped[1]}.…\` names a field the result does not have.`,
501
+ where: t.name,
502
+ });
503
+ // An extract row is NAMED after its source's last segment (`emitTaskOutput`),
504
+ // so `=Decision.output` writes a second row named `output` beside the
505
+ // task's own. The case variable it stores into collides with nothing.
506
+ const rowName = o.source.replace(/^=/, '').split('.').pop();
507
+ if (rowName === 'output' || rowName === 'Error')
508
+ diags.push({
509
+ level: 'error',
510
+ code: 'BUSINESS_RULE_OUTPUT_NAME',
511
+ message: `Business-rule task "${t.name}" output "${o.var}" reads "${o.source}", which writes an output row named "${rowName}" — the name of the task's own ${rowName === 'output' ? '`output` row (the whole result, `=result`)' : '`Error` row (`=Error`)'}. Two rows with one name are ambiguous to the designer; rename the decision column.`,
512
+ where: t.name,
513
+ });
514
+ }
515
+ }
486
516
  if (t.kind === 'flow-process' && (!t.ref || !t.ref.name.trim()))
487
517
  diags.push({
488
518
  level: 'error',
@@ -364,7 +364,12 @@ function syncBindingsV2(caseplan, outPath) {
364
364
  resource: group.resource,
365
365
  key: group.key,
366
366
  value: group.value,
367
- metadata: group.subType ? { subType: group.subType } : {},
367
+ // A BusinessRule entry carries the bindings version deploy needs to
368
+ // override its folder — the metadata `uip maestro case bindings sync`
369
+ // writes for it (no subType).
370
+ metadata: group.subType
371
+ ? { subType: group.subType }
372
+ : group.resource === 'BusinessRule' ? { BindingsVersion: '2.2' } : {},
368
373
  });
369
374
  }
370
375
  resources.push(...connectionResources);
@@ -579,6 +579,8 @@ function emitTaskKind(task, refs, library) {
579
579
  const folder = folderPath ? `, ${opts([['folder', tsLiteral(folderPath)]])}` : '';
580
580
  return `.${method}(${tsLiteral(name ?? '')}${folder})`;
581
581
  }
582
+ if (isAuthorableBusinessRule(task))
583
+ return businessRuleCall(task, refs);
582
584
  if (kind === 'action') {
583
585
  let recipient;
584
586
  if (data.recipient) {
@@ -616,7 +618,12 @@ function emitTaskKind(task, refs, library) {
616
618
  const spec = waitConnectorSpec(data, `wait-for-connector task "${task.displayName ?? task.id ?? '?'}"`, refs);
617
619
  return spec === undefined ? '.waitForConnector()' : `.waitForConnector(${spec})`;
618
620
  }
619
- const message = `unsupported task kind "${kind}" for task "${task.displayName ?? task.id ?? '?'}".`;
621
+ const message = kind === 'business-rule'
622
+ // The converter reads a missing version as v2, whose result shape differs
623
+ // from v3's decision-keyed one; recompiling it as v3 would change what its
624
+ // extracts read. Keep the task as data instead.
625
+ ? `business-rule task "${task.displayName ?? task.id ?? '?'}" has ${data.version === undefined ? 'no version (the converter reads it as v2)' : `version "${String(data.version)}"`}; only v3 business-rule tasks are authorable.`
626
+ : `unsupported task kind "${kind}" for task "${task.displayName ?? task.id ?? '?'}".`;
620
627
  diagnose(refs, {
621
628
  code: 'UNSUPPORTED_TASK_KIND',
622
629
  message,
@@ -624,6 +631,29 @@ function emitTaskKind(task, refs, library) {
624
631
  });
625
632
  return `.__UNRESOLVED_TASK_KIND__(${tsLiteral(kind)}, ${tsLiteral(data)})`;
626
633
  }
634
+ /** A business-rule task `.businessRule()` can rebuild: v3 only. */
635
+ function isAuthorableBusinessRule(task) {
636
+ return task.type === 'business-rule' && task.data?.version === 'v3';
637
+ }
638
+ /** The two outputs every business-rule task carries, which `.businessRule()` re-emits itself. */
639
+ function isBusinessRuleOwnOutput(row) {
640
+ return (row.name === 'output' && row.source === '=result') || (row.name === 'Error' && row.source === '=Error');
641
+ }
642
+ function businessRuleCall(task, refs) {
643
+ const data = task.data ?? {};
644
+ const owner = `business-rule task "${task.displayName ?? task.id ?? '?'}"`;
645
+ const name = bindingDefault(data.name, refs);
646
+ const folderPath = bindingDefault(data.folderPath, refs);
647
+ if (typeof name !== 'string' || !name || typeof folderPath !== 'string')
648
+ throw new Error(`${owner} must name its rule and folder (data.name / data.folderPath).`);
649
+ const outputRow = (Array.isArray(data.outputs) ? data.outputs : [])
650
+ .find((row) => row.name === 'output' && row.source === '=result');
651
+ const resultSchema = outputRow?._jsonSchema;
652
+ return `.businessRule(${tsLiteral(name)}, ${opts([
653
+ ['folder', tsLiteral(folderPath)],
654
+ ['resultSchema', resultSchema !== undefined ? tsLiteral(resultSchema) : undefined],
655
+ ])})`;
656
+ }
627
657
  function ioArg(rows, kind) {
628
658
  const valueField = kind === 'inputs' ? 'value' : 'source';
629
659
  const entries = rows.map((row) => {
@@ -647,6 +677,14 @@ function emitTask(out, depth, task, lane, refs, library) {
647
677
  if (Array.isArray(data.outputs) && data.outputs.length)
648
678
  out.line(inner, `.outputs(${ioArg(data.outputs, 'outputs')})`);
649
679
  }
680
+ if (isAuthorableBusinessRule(task)) {
681
+ const data = task.data ?? {};
682
+ const extracts = (Array.isArray(data.outputs) ? data.outputs : []).filter((row) => !isBusinessRuleOwnOutput(row));
683
+ if (Array.isArray(data.inputs) && data.inputs.length)
684
+ out.line(inner, `.inputs(${ioArg(data.inputs, 'inputs')})`);
685
+ if (extracts.length)
686
+ out.line(inner, `.outputs(${ioArg(extracts, 'outputs')})`);
687
+ }
650
688
  if (task.isRequired !== undefined)
651
689
  out.line(inner, task.isRequired ? '.required()' : '.required(false)');
652
690
  if (task.shouldRunOnlyOnce !== undefined)
@@ -749,11 +787,20 @@ function producedVariableNames(plan) {
749
787
  if (!isStage(node))
750
788
  continue;
751
789
  for (const lane of node.data?.tasks ?? [])
752
- for (const task of lane)
753
- if (ownEntry(TASK_KIND_METHOD, task.type))
790
+ for (const task of lane) {
791
+ if (ownEntry(TASK_KIND_METHOD, task.type)) {
754
792
  for (const row of task.data?.outputs ?? [])
755
793
  if (row.var)
756
794
  produced.add(row.var);
795
+ }
796
+ else if (isAuthorableBusinessRule(task)) {
797
+ // A business-rule extract registers its case variable on recompile, as
798
+ // a reference-task output does; the task's own rows do not.
799
+ for (const row of task.data?.outputs ?? [])
800
+ if (row.var && !isBusinessRuleOwnOutput(row))
801
+ produced.add(row.var);
802
+ }
803
+ }
757
804
  }
758
805
  return produced;
759
806
  }
@@ -40,7 +40,7 @@ export declare const CASE_FORMAT_PROFILE: Readonly<CaseFormatProfile>;
40
40
  * Which Case JSON schema versions `.schemaVersion()` may declare.
41
41
  *
42
42
  * ONE, and that is the honest answer rather than a placeholder. Case is a
43
- * closed, whole-document schema: `@uipath/case-schema` ships a chain of 31
43
+ * closed, whole-document schema: `@uipath/case-schema` ships a chain of
44
44
  * grammars (`case-schema-census.json`'s `versionChain`), each with its own
45
45
  * forward migration, and a version is a claim that the entire document matches
46
46
  * that grammar. This SDK emits the V30 grammar natively — the V30 rule schemas
@@ -28,16 +28,20 @@ export const CASE_FORMAT_PROFILE = {
28
28
  { featureFlag: 'canvas-useExplicitEntryExitConditions', fromVersion: '12.0.0' },
29
29
  { featureFlag: 'canvas-caseUnifiedSchema', fromVersion: '20.0.0' },
30
30
  ],
31
+ // The validation pin (the `@uipath/case-schema` devDependency in
32
+ // typescript/package.json; the census records it as `generatedFrom.version`)
33
+ // and the newest Case JSON version that package reads. The writer stays at V30 (`writeVersion`): V31+ is read
34
+ // and migrated by the platform, not emitted here.
31
35
  validationCeiling: {
32
- caseSchemaVersion: '0.1016.2',
33
- formatVersion: '30.0.0',
36
+ caseSchemaVersion: '0.1173.0',
37
+ formatVersion: '32.0.4',
34
38
  },
35
39
  };
36
40
  /**
37
41
  * Which Case JSON schema versions `.schemaVersion()` may declare.
38
42
  *
39
43
  * ONE, and that is the honest answer rather than a placeholder. Case is a
40
- * closed, whole-document schema: `@uipath/case-schema` ships a chain of 31
44
+ * closed, whole-document schema: `@uipath/case-schema` ships a chain of
41
45
  * grammars (`case-schema-census.json`'s `versionChain`), each with its own
42
46
  * forward migration, and a version is a claim that the entire document matches
43
47
  * that grammar. This SDK emits the V30 grammar natively — the V30 rule schemas
@@ -491,8 +491,18 @@ function applyPreservedIdentities(result, preservation) {
491
491
  take(task, taskIdentity.id);
492
492
  takeConditions(task?.entryConditions, taskIdentity.entryConditions);
493
493
  (taskIdentity.inputs ?? []).forEach((row, index) => take(task?.data?.inputs?.[index], row.id));
494
- (taskIdentity.outputs ?? []).forEach((row, index) => {
495
- const target = task?.data?.outputs?.[index];
494
+ const outputRows = task?.data?.outputs ?? [];
495
+ const wantedRows = taskIdentity.outputs ?? [];
496
+ // Match by name when unique on both sides, else by position.
497
+ const unique = (names, name) => names.filter((n) => n === name).length === 1;
498
+ const currentNames = outputRows.map((r) => r?.name);
499
+ const wantedNames = wantedRows.map((r) => r.name);
500
+ const named = new Set(wantedRows.filter((r) => unique(wantedNames, r.name) && unique(currentNames, r.name)).map((r) => r.name));
501
+ wantedRows.forEach((row, index) => {
502
+ const target = named.has(row.name)
503
+ ? outputRows.find((r) => r?.name === row.name)
504
+ // A positional match never takes a row a name match owns.
505
+ : named.has(outputRows[index]?.name) ? undefined : outputRows[index];
496
506
  const currentId = target?.id;
497
507
  take(target, row.id);
498
508
  if (typeof currentId === 'string' && typeof row.id === 'string' && target?.value === currentId) {
@@ -839,6 +849,8 @@ function taskData(task, resolver, taskId, stageId, ids) {
839
849
  data.outputs = task.outputs.map((o, i) => emitTaskOutput(o, elementId, ids, `${elementId}:out:${i}:${o.var}`, resolver));
840
850
  return data;
841
851
  }
852
+ case 'business-rule':
853
+ return businessRuleTaskData(task, resolver, `${stageId}-${taskId}`, ids);
842
854
  case 'action': {
843
855
  const a = task.action ?? {};
844
856
  const data = {};
@@ -1094,6 +1106,76 @@ function emitTaskOutput(o, elementId, ids, seed, resolver) {
1094
1106
  elementId,
1095
1107
  };
1096
1108
  }
1109
+ // ─── Business-rule task ───────────────────────────────────────────────────────
1110
+ /** The version a business-rule task carries: below v3 the engine cannot run the
1111
+ * `.uirule` package a BusinessRules project deploys (the case converter falls
1112
+ * back to v2 when the field is missing). Same constant as the CLI's
1113
+ * `BUSINESS_RULE_TASK_VERSION`. */
1114
+ const BUSINESS_RULE_TASK_VERSION = 'v3';
1115
+ /** The binding `resource` a business rule is bound by — the CLI's and Studio
1116
+ * Web's spelling. `businessRule` (the registry cache category / BPMN binding
1117
+ * pattern) is a different namespace, and a binding that uses it is not a
1118
+ * BusinessRule resource to the bindings_v2 reader. */
1119
+ const BUSINESS_RULE_RESOURCE = 'BusinessRule';
1120
+ /** Mint (once per resourceKey + attribute) one root `BusinessRule` binding. */
1121
+ function businessRuleBinding(resolver, ids, resourceKey, propertyAttribute, value) {
1122
+ for (const entry of resolver.bindingSink.values()) {
1123
+ if (entry.resource === BUSINESS_RULE_RESOURCE && entry.resourceKey === resourceKey
1124
+ && entry.propertyAttribute === propertyAttribute)
1125
+ return entry.id;
1126
+ }
1127
+ const id = ids.make('b', 8, `binding:${BUSINESS_RULE_RESOURCE}:${resourceKey}:${propertyAttribute}`);
1128
+ resolver.bindingSink.set(id, {
1129
+ id,
1130
+ name: propertyAttribute,
1131
+ type: 'string',
1132
+ resource: BUSINESS_RULE_RESOURCE,
1133
+ resourceKey,
1134
+ default: value,
1135
+ propertyAttribute,
1136
+ });
1137
+ return id;
1138
+ }
1139
+ /**
1140
+ * The `data` of a `business-rule` task, as Studio Web writes it and as the
1141
+ * CLI's `tasks describe --type business-rule` + splice produce it:
1142
+ *
1143
+ * - `name` / `folderPath` are `=bindings.<id>` references to a `BusinessRule`
1144
+ * binding pair keyed `<folder>.<name>` (the runtime resolves the rule through
1145
+ * them; `validate --strict` refuses literals);
1146
+ * - `version: 'v3'`;
1147
+ * - `outputs` always starts with `output` (`source: '=result'`, the whole
1148
+ * decision-keyed result) and `Error` (`source: '=Error'`), followed by the
1149
+ * author's extract rows, whose sources read a field of the result
1150
+ * (`=<Decision>.<column>`).
1151
+ */
1152
+ function businessRuleTaskData(task, resolver, elementId, ids) {
1153
+ const ref = task.ref;
1154
+ const resourceKey = `${ref.folderPath}.${ref.name}`;
1155
+ const nameId = businessRuleBinding(resolver, ids, resourceKey, 'name', ref.name);
1156
+ const folderId = businessRuleBinding(resolver, ids, resourceKey, 'folderPath', ref.folderPath);
1157
+ const ownRow = (name, source, seed, extra = {}) => {
1158
+ const slot = ids.make('v', 8, `${elementId}:${seed}`);
1159
+ return { name, type: 'jsonSchema', id: slot, var: slot, value: slot, source, target: `=${slot}`, elementId, ...extra };
1160
+ };
1161
+ const resultSchema = task.businessRule?.resultSchema;
1162
+ const outputs = [
1163
+ ownRow('output', '=result', 'br:output', resultSchema === undefined ? {} : { _jsonSchema: resultSchema }),
1164
+ // The CLI's `buildErrorOutput()` row after `addVarIdsToInputsOutputs`:
1165
+ // `displayName`, and the error schema as both `_jsonSchema` and `body`.
1166
+ ownRow('Error', '=Error', 'br:error', {
1167
+ displayName: 'Error', _jsonSchema: structuredClone(ERROR_OUTPUT_SCHEMA), body: structuredClone(ERROR_OUTPUT_SCHEMA),
1168
+ }),
1169
+ ...(task.outputs ?? []).map((o, i) => emitTaskOutput(o, elementId, ids, `${elementId}:out:${i}:${o.var}`, resolver)),
1170
+ ];
1171
+ return {
1172
+ name: `=bindings.${nameId}`,
1173
+ folderPath: `=bindings.${folderId}`,
1174
+ version: BUSINESS_RULE_TASK_VERSION,
1175
+ inputs: (task.inputs ?? []).map((inp, i) => emitTaskInput(inp, elementId, ids, `${elementId}:in:${i}:${inp.name}`)),
1176
+ outputs,
1177
+ };
1178
+ }
1097
1179
  // ─── Connector task (execute-connector-activity) ──────────────────────────────
1098
1180
  /**
1099
1181
  * Build the `execute-connector-activity` `data` payload for an IS connector task,
@@ -1353,6 +1435,14 @@ function connectorOutputVar(resolver, base) {
1353
1435
  resolver.connectorOutputCounts.set(base, occurrence);
1354
1436
  return occurrence === 1 ? base : `${base}${occurrence}`;
1355
1437
  }
1438
+ /** The `Error` output's schema: the designer's (and the CLI's) `ERROR_OUTPUT_SCHEMA`. */
1439
+ const ERROR_OUTPUT_SCHEMA = {
1440
+ type: 'object',
1441
+ properties: {
1442
+ code: { type: 'string' }, message: { type: 'string' }, detail: { type: 'string' },
1443
+ category: { type: 'string' }, status: { type: 'number' }, element: { type: 'string' },
1444
+ },
1445
+ };
1356
1446
  function connectorOutputs(rc, elementId, resolver) {
1357
1447
  const responseSchema = rc.entry.outputSchema
1358
1448
  ? schemaFromFields(rc.entry.outputSchema.fields ?? [])
@@ -1367,13 +1457,7 @@ function connectorOutputs(rc, elementId, resolver) {
1367
1457
  },
1368
1458
  {
1369
1459
  name: 'Error', displayName: 'Error', type: 'jsonSchema', source: '=Error',
1370
- body: {
1371
- type: 'object',
1372
- properties: {
1373
- code: { type: 'string' }, message: { type: 'string' }, detail: { type: 'string' },
1374
- category: { type: 'string' }, status: { type: 'number' }, element: { type: 'string' },
1375
- },
1376
- },
1460
+ body: structuredClone(ERROR_OUTPUT_SCHEMA),
1377
1461
  var: errorVar, id: errorVar, value: errorVar, target: `=${errorVar}`, elementId,
1378
1462
  },
1379
1463
  ];
package/dist/check.js CHANGED
@@ -10,7 +10,7 @@ import { FlowNode } from './core/node-classes.js';
10
10
  import { planContributionBindings } from './core/contribution-bindings.js';
11
11
  import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
12
12
  import { actionVersion, bundledActionNodeType, bundledContainerDefinition, bundledDefinition, perCallDefinition, resolveContributionDefinition } from './serialize.js';
13
- import { FORM_FIELD_TYPES, formFieldOptionsProblem, formFieldOptionsWhy, formFieldTypeHint, formFieldTypeWhy, validFormFieldTypesText } from './core/form-field-type.js';
13
+ import { FORM_FIELD_TYPES, formFieldOptionsProblem, formFieldOptionsWhy, formFieldSettingProblems, formFieldTypeHint, formFieldTypeWhy, validFormFieldTypesText, } from './core/form-field-type.js';
14
14
  import { AGENTIC_PROCESS_FORMS } from './core/agentic-process-forms.js';
15
15
  import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
16
16
  import { checkCookedEscapes } from './flow-expr-check.js';
@@ -3573,7 +3573,7 @@ function checkInlineAgent(step, inputs, diags, opts = {}, ledger = newResourceId
3573
3573
  + `${JSON.stringify(f?.type)}, ${formFieldTypeWhy(f?.type, `which the form does not recognize — it would `
3574
3574
  + `be published as a plain string`)}. Valid: ${validFormFieldTypesText()}.`, formFieldTypeHint(f?.type));
3575
3575
  }
3576
- const optionsProblem = formFieldOptionsProblem(f?.type, f?.options);
3576
+ const optionsProblem = formFieldOptionsProblem(f);
3577
3577
  if (optionsProblem !== undefined) {
3578
3578
  const message = `Inline agent step "${step}"'s quick-form escalation field "${String(f?.id ?? '?')}" `
3579
3579
  + `${formFieldOptionsWhy(optionsProblem, f?.type)}.`;
@@ -3583,6 +3583,10 @@ function checkInlineAgent(step, inputs, diags, opts = {}, ledger = newResourceId
3583
3583
  at('INLINE_AGENT_ESCALATION_FIELD_OPTIONS', message, `options: [{ id: 'opt-1', value: 'high', label: 'High' }, { id: 'opt-2', value: 'low', label: 'Low' }]`);
3584
3584
  }
3585
3585
  }
3586
+ for (const problem of formFieldSettingProblems(f)) {
3587
+ at('INLINE_AGENT_ESCALATION_FIELD_SETTING_INVALID', `Inline agent step "${step}"'s quick-form escalation gives the field "${String(f?.id ?? '?')}" `
3588
+ + `${problem}. The platform refuses the form.`);
3589
+ }
3586
3590
  }
3587
3591
  continue;
3588
3592
  }
@@ -102,19 +102,114 @@ export interface FormFieldOption {
102
102
  /** What the reviewer sees, e.g. `'High'`. */
103
103
  label: string;
104
104
  }
105
+ /**
106
+ * How a form field renders (`@uipath/hitl-schema-types` `controlType`). It is a
107
+ * separate vocabulary from the data `type`: a single choice is `type: 'string'`
108
+ * with `controlType: 'dropdown'`. When it is absent, the form uses the data
109
+ * type's default control (or the one a choice spelling such as `singleSelect`
110
+ * implies).
111
+ */
112
+ export type FormFieldControlType = 'text' | 'textarea' | 'numberInput' | 'datePicker' | 'dateAndTimePicker' | 'checkbox' | 'dropdown' | 'radioGroup' | 'multiSelect' | 'checkboxGroup' | 'fileUpload';
113
+ /**
114
+ * One condition of a form-field rule (`@uipath/hitl-schema-types`
115
+ * `fieldConditionSchema`): `when` names the field it reads, and the other keys
116
+ * say what that field's value must be.
117
+ */
118
+ export interface FormFieldCondition {
119
+ /** The id of the field the condition reads, e.g. `'priority'`. */
120
+ when: string;
121
+ /** The value must equal this. */
122
+ is?: unknown;
123
+ /** The value must not equal this. */
124
+ isNot?: unknown;
125
+ /** The value must be one of these. */
126
+ in?: unknown[];
127
+ /** The value must be none of these. */
128
+ notIn?: unknown[];
129
+ /** The value must match this regular expression. */
130
+ matches?: string;
131
+ /** A custom condition expression, kept as written. */
132
+ custom?: string;
133
+ }
134
+ /**
135
+ * A form-field rule (`@uipath/hitl-schema-types` `fieldRuleSchema`): when its
136
+ * conditions hold, its `effects` apply to the field, for example
137
+ * `{ visible: true }` or `{ required: true }`.
138
+ *
139
+ * @example
140
+ * hitl({
141
+ * fields: [
142
+ * { id: 'priority', type: 'singleSelect', direction: 'output', options: [{ id: 'o1', value: 'high', label: 'High' }] },
143
+ * {
144
+ * id: 'reason', type: 'string', direction: 'output',
145
+ * // Required only when the priority is high.
146
+ * rules: [{ id: 'rule-1', conditions: [{ when: 'priority', is: 'high' }], effects: { required: true } }],
147
+ * },
148
+ * ],
149
+ * outcomes: ['Done'],
150
+ * })
151
+ */
152
+ export interface FormFieldRule {
153
+ /** The rule's own stable id, e.g. `'rule-1'`. */
154
+ id: string;
155
+ /** What must hold for the effects to apply. */
156
+ conditions: FormFieldCondition[];
157
+ /** How the conditions combine. The platform's default is `'AND'`. */
158
+ operator?: 'AND' | 'OR';
159
+ /** What applies while the conditions hold, keyed by effect (`visible`, `required`, `disabled`). */
160
+ effects: Record<string, unknown>;
161
+ }
162
+ /**
163
+ * The form-field keys that say how a field looks and behaves, with the
164
+ * platform's own names and shapes (`@uipath/hitl-schema-types` `fieldSchemaFor`).
165
+ * Every one is optional and written to the form row as given, so a field the
166
+ * canvas saved keeps them through decompile, an edit and compile.
167
+ *
168
+ * @example
169
+ * hitl({
170
+ * fields: [{
171
+ * id: 'priority', label: 'Priority', type: 'string', direction: 'output', required: true,
172
+ * controlType: 'dropdown',
173
+ * options: [{ id: 'o1', value: 'high', label: 'High' }, { id: 'o2', value: 'low', label: 'Low' }],
174
+ * defaultValue: 'high', colSpan: 6, requiredMessage: 'Pick one',
175
+ * description: 'How urgent the invoice is.',
176
+ * }],
177
+ * outcomes: ['Done'],
178
+ * })
179
+ */
180
+ export interface FormFieldSettings {
181
+ /** Conditions that show, require or disable the field. */
182
+ rules?: FormFieldRule[];
183
+ /** The message the form shows when a required field is left empty. */
184
+ requiredMessage?: string;
185
+ /** The field's width on the form's 12-column grid: an integer from 1 to 12. Absent means the full width. */
186
+ colSpan?: number;
187
+ /**
188
+ * How the field renders, e.g. `'dropdown'` or `'textarea'`. Absent means the
189
+ * data type's default control.
190
+ */
191
+ controlType?: FormFieldControlType;
192
+ /**
193
+ * The choices of a picker field — a `dropdown` / `singleSelect` / `enum`, or a
194
+ * `multiSelect`. Written to the form verbatim, in order.
195
+ */
196
+ options?: FormFieldOption[];
197
+ /**
198
+ * The value the form starts with: one `options` value for a single-choice
199
+ * field, a list of them for a multi-select, or the starting text.
200
+ */
201
+ defaultValue?: string | string[];
202
+ /** Guidance for an agent that pre-fills the field. */
203
+ description?: string;
204
+ }
105
205
  /** A value the reviewer READS: shown on the form, filled in from the flow. */
106
- export interface ShownField {
206
+ export interface ShownField extends FormFieldSettings {
107
207
  /** Field id — the key the answer object uses. Must be unique on the form. */
108
208
  id: string;
109
209
  /** Label the reviewer sees. Defaults to `id`. */
110
210
  label?: string;
111
211
  type: FormFieldType;
112
212
  direction: 'input';
113
- /**
114
- * The choices of a picker field — a `singleSelect` / `enum` dropdown or a
115
- * `multiSelect`. Written to the form verbatim, in order.
116
- */
117
- options?: FormFieldOption[];
118
213
  /**
119
214
  * What to show — a reference, e.g. `input('amount')` or
120
215
  * `out('fetchInvoice', 'body.total')`. Required: a shown field with nothing
@@ -124,18 +219,13 @@ export interface ShownField {
124
219
  value: Expr;
125
220
  }
126
221
  /** A value the reviewer FILLS IN: read downstream with `out('<step>', '<id>')`. */
127
- export interface AskedField {
222
+ export interface AskedField extends FormFieldSettings {
128
223
  /** Field id — the key the answer object uses. Must be unique on the form. */
129
224
  id: string;
130
225
  /** Label the reviewer sees. Defaults to `id`. */
131
226
  label?: string;
132
227
  type: FormFieldType;
133
228
  direction: 'output';
134
- /**
135
- * The choices of a picker field — a `singleSelect` / `enum` dropdown or a
136
- * `multiSelect`. Written to the form verbatim, in order.
137
- */
138
- options?: FormFieldOption[];
139
229
  /** Reviewer must fill it in before the task can be completed. */
140
230
  required?: boolean;
141
231
  }
@@ -145,18 +235,13 @@ export interface AskedField {
145
235
  * answer. Read it downstream with `out('<step>', '<id>')` — the write target is
146
236
  * the field's own id, deliberately decoupled from where the pre-fill came from.
147
237
  */
148
- export interface InOutField {
238
+ export interface InOutField extends FormFieldSettings {
149
239
  /** Field id — the key the answer object uses. Must be unique on the form. */
150
240
  id: string;
151
241
  /** Label the reviewer sees. Defaults to `id`. */
152
242
  label?: string;
153
243
  type: FormFieldType;
154
244
  direction: 'inOut';
155
- /**
156
- * The choices of a picker field — a `singleSelect` / `enum` dropdown or a
157
- * `multiSelect`. Written to the form verbatim, in order.
158
- */
159
- options?: FormFieldOption[];
160
245
  /** What to pre-fill — a reference, e.g. `out('fetchInvoice', 'body.total')`. */
161
246
  value: Expr;
162
247
  /** Reviewer must confirm/fill it before the task can be completed. */