@uipath/maestro-builder-sdk 6.58.1 → 6.59.1

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.
@@ -188,6 +188,9 @@ export const REFUSALS = {
188
188
  FORM_TRIGGER_NO_INPUTS: 'lint',
189
189
  FUNCTION_EMPTY_RETURNS: 'lint',
190
190
  FUNCTION_KEY_NOT_KEY: 'lint',
191
+ FUNCTION_LOCAL_HAS_FOLDER: 'lint',
192
+ FUNCTION_LOCAL_NO_PROJECT_ID: 'lint',
193
+ FUNCTION_LOCAL_PROJECT_ID_NOT_GUID: 'lint',
191
194
  FUNCTION_NAME_IS_PATH: 'lint',
192
195
  FUNCTION_NO_FOLDER: 'lint',
193
196
  FUNCTION_NO_KEY: 'lint',
@@ -203,7 +206,12 @@ export const REFUSALS = {
203
206
  GUARDRAIL_NO_VALIDATOR: 'lint',
204
207
  GUARDRAIL_TYPE_UNKNOWN: 'lint',
205
208
  HITL_APP_INCOMPLETE: 'lint',
209
+ HITL_APP_INPUT_REQUIRED: 'lint',
210
+ HITL_APP_INPUT_UNKNOWN: 'lint',
206
211
  HITL_APP_MISSING: 'lint',
212
+ HITL_APP_NO_SCHEMA: 'lint',
213
+ HITL_APP_OUTCOME_UNKNOWN: 'lint',
214
+ HITL_APP_SCHEMA_INVALID: 'lint',
207
215
  HITL_APP_WITHOUT_VARIANT: 'lint',
208
216
  HITL_APP_WITH_FIELDS: 'lint',
209
217
  HITL_CHANNEL_UNKNOWN: 'lint',
@@ -174,15 +174,6 @@ export declare function httpRequestToolResourceJson(label: string, inputs: Recor
174
174
  id: string;
175
175
  canvasNodeId: string;
176
176
  }): Record<string, unknown>;
177
- /**
178
- * The .NET type an Action Center app argument of each field type carries. Studio
179
- * Web serializes the task payload through `inputSchemaDotnetTypeMapping`, which
180
- * the platform schema REQUIRES on an `actionCenter` channel; it reads the raw
181
- * type off the app's action schema, which the SDK does not fetch, so this is
182
- * the inverse of flow-workbench's `dotNetTypeToJsonSchema` for the scalars and
183
- * `System.Object` for the two composite types it has no single answer for.
184
- */
185
- export declare const DOTNET_TYPE: Record<string, string>;
186
177
  /**
187
178
  * The folder an app escalation names: the author's `folderPath`, or
188
179
  * `solution_folder` when they gave none — an app that ships in this solution.
@@ -10,6 +10,7 @@ import ixpDefinition from '../../../core/ixp-definition.json' with { type: 'json
10
10
  import { JOB_ATTACHMENT_REF, jobAttachmentDefinition } from '../../../core/agent-sidecar.js';
11
11
  import { BUILTIN_TOOL_TYPE, CONNECTOR_TOOL_RESOURCE_TYPE, HTTP_REQUEST_TOOL_TYPE, INTERNAL_TOOL_RESOURCE_TYPE, TOOL_RESOURCE_TYPE, } from '../../../core/agent-tool-resources.js';
12
12
  import { buildConfiguration } from '../../../core/connector-configuration.js';
13
+ import { ESCALATION_DOTNET_TYPE } from '../../../core/dotnet-types.js';
13
14
  import { multipartBodyFieldName, transportHttpMethod } from '../../../core/connector-inputs.js';
14
15
  import { CONTEXT_INDEX_DEFAULTS, CONTEXT_INDEX_DEFAULT_FILE_EXTENSION, contextIndexType } from '../../../core/context-index.js';
15
16
  import { formFieldJsonSchema } from '../../../core/form-field-type.js';
@@ -445,18 +446,6 @@ export function httpRequestToolResourceJson(label, inputs, spec) {
445
446
  properties: { toolType: HTTP_REQUEST_TOOL_TYPE },
446
447
  };
447
448
  }
448
- /**
449
- * The .NET type an Action Center app argument of each field type carries. Studio
450
- * Web serializes the task payload through `inputSchemaDotnetTypeMapping`, which
451
- * the platform schema REQUIRES on an `actionCenter` channel; it reads the raw
452
- * type off the app's action schema, which the SDK does not fetch, so this is
453
- * the inverse of flow-workbench's `dotNetTypeToJsonSchema` for the scalars and
454
- * `System.Object` for the two composite types it has no single answer for.
455
- */
456
- export const DOTNET_TYPE = {
457
- string: 'System.String', number: 'System.Double', integer: 'System.Int32',
458
- boolean: 'System.Boolean', object: 'System.Object', array: 'System.Object',
459
- };
460
449
  /**
461
450
  * The folder an app escalation names: the author's `folderPath`, or
462
451
  * `solution_folder` when they gave none — an app that ships in this solution.
@@ -537,7 +526,7 @@ export function escalationResourceJson(esc, spec) {
537
526
  }
538
527
  else {
539
528
  const app = esc.app;
540
- const mapping = (fields) => Object.fromEntries(Object.entries(fields ?? {}).map(([k, t]) => [k, ownEntry(DOTNET_TYPE, t) ?? 'System.Object']));
529
+ const mapping = (fields) => Object.fromEntries(Object.entries(fields ?? {}).map(([k, t]) => [k, ownEntry(ESCALATION_DOTNET_TYPE, t) ?? 'System.Object']));
541
530
  channel = {
542
531
  ...common,
543
532
  type: 'actionCenter',
@@ -0,0 +1,63 @@
1
+ import type { ActionSchema, AppRef } from './spec.js';
2
+ /** One argument of the schema, in JSON Schema terms. */
3
+ export interface ActionSchemaArg {
4
+ name: string;
5
+ schema: Record<string, unknown>;
6
+ required: boolean;
7
+ }
8
+ /** The schema, whichever spelling it came in. */
9
+ export interface NormalizedActionSchema {
10
+ inputs: ActionSchemaArg[];
11
+ outputs: ActionSchemaArg[];
12
+ inOuts: ActionSchemaArg[];
13
+ outcomes: string[];
14
+ }
15
+ /**
16
+ * The schema as one shape, or a sentence saying what is wrong with it. Two
17
+ * spellings are accepted, both verbatim:
18
+ *
19
+ * - a coded action app's `action-schema.json` — each section a JSON-Schema-ish
20
+ * `{ type: 'object', properties: { <name>: { type, required? } } }`;
21
+ * - the platform's action schema — each section an array of
22
+ * `{ name, type: 'System.String', required, isList }` (the resource file's
23
+ * `spec.actionSchema`, and what the Apps API returns).
24
+ */
25
+ export declare function normalizeActionSchema(schema: unknown): NormalizedActionSchema | string;
26
+ /**
27
+ * `inputs.app.inputSchema`: the inputs and the inOuts, with the required ones
28
+ * listed — `createInputSchemaFromAppSchema`'s shape, key for key.
29
+ */
30
+ export declare function appInputSchema(s: NormalizedActionSchema): Record<string, unknown>;
31
+ /** What the reviewer answers: the outputs and the inOuts, by name. */
32
+ export declare function appAnswerSchemas(s: NormalizedActionSchema): Array<[string, Record<string, unknown>]>;
33
+ /**
34
+ * What a `check` rule needs of an app's contract — the input names, the
35
+ * required ones, the answers and the buttons — from whichever of `actionSchema`
36
+ * and a verbatim `inputSchema` the app carries; `undefined` with neither, a
37
+ * sentence when one is malformed.
38
+ */
39
+ export declare function appContract(app: AppRef | undefined): {
40
+ inputs: string[];
41
+ required: string[];
42
+ answers: string[];
43
+ outcomes: string[];
44
+ } | string | undefined;
45
+ /**
46
+ * The schema back from a node, as decompile prints it:
47
+ *
48
+ * - `{ actionSchema }`, in the `action-schema.json` spelling, when one
49
+ * reproduces the node's `inputSchema` and typed answer BYTE FOR BYTE — what
50
+ * the SDK and Studio Web's picker write. A property in both the input schema
51
+ * and the answer is an inOut; the rest split by side; outcomes are the task's.
52
+ * - `{ inputSchema }`, verbatim, otherwise — a hand-edited schema (a `title`, a
53
+ * nested object's `properties`) that no action schema lowers to. Recompiling
54
+ * writes it back unchanged.
55
+ *
56
+ * Either way serialize writes the node's `inputSchema` back as it was: the lift
57
+ * is checked against the emission, not assumed to invert it.
58
+ */
59
+ export declare function liftActionSchema(inputSchema: unknown, answers: Record<string, unknown> | undefined, outcomes: string[]): {
60
+ actionSchema: ActionSchema;
61
+ } | {
62
+ inputSchema: Record<string, unknown>;
63
+ } | undefined;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * An Action App's action schema — the contract of its form: what the task sends
3
+ * it (`inputs`), what the reviewer answers (`outputs`), what goes both ways
4
+ * (`inOuts`) and which buttons complete it (`outcomes`) — read in either of the
5
+ * two spellings an author can hold, and lowered to what the node carries.
6
+ *
7
+ * Why the node needs it: flow-v1's serializer types the task's
8
+ * `HitlTaskArguments` from `inputs.app.inputSchema`, and with none it falls back
9
+ * to `{ type: 'string' }` for every bound input (`hitl-serialization.ts`, the
10
+ * `appInputBindings` fallback). A number or an integer then reaches the app as a
11
+ * string. Studio Web's app picker writes the schema from the app's action schema
12
+ * (`HITLAppPicker.tsx` → `createInputSchemaFromAppSchema`); this SDK is offline,
13
+ * so the author hands it the same schema.
14
+ *
15
+ * The emission is the PRODUCT's, not a reading of JSON Schema: the platform
16
+ * spelling goes through the picker's own `.NET → JSON` table, and the
17
+ * `action-schema.json` spelling first through `uip codedapp push`'s `JSON → .NET`
18
+ * table — the path the same app's schema takes to reach the picker — so one app
19
+ * written either way emits the same bytes (core/dotnet-types.ts). That path
20
+ * keeps only a type: an object input's nested `properties`, a `description`, a
21
+ * `title` do not survive it, and do not survive here.
22
+ */
23
+ import { actionSchemaFileDotNetType, dotNetTypeToJsonSchema } from '../../../core/dotnet-types.js';
24
+ const FILE_TYPES = 'string, integer, number, boolean, object, file, ContentValidationData or array';
25
+ /** An `action-schema.json` property's JSON Schema, or what is wrong with it. */
26
+ function fileSchema(where, p) {
27
+ const isList = p.type === 'array';
28
+ const item = (isList ? p.items : p);
29
+ if (isList && (!item || typeof item !== 'object'))
30
+ return `"${where}" is an array with no items schema`;
31
+ if (item.type === 'array')
32
+ return `"${where}" is an array of arrays, which uip codedapp push refuses`;
33
+ const dotNet = actionSchemaFileDotNetType(item.type, item.format);
34
+ if (dotNet === undefined) {
35
+ return `"${where}" has the type ${JSON.stringify(item.type)}; an action-schema.json type is one of ${FILE_TYPES}`;
36
+ }
37
+ return dotNetTypeToJsonSchema(dotNet, isList);
38
+ }
39
+ /**
40
+ * The schema as one shape, or a sentence saying what is wrong with it. Two
41
+ * spellings are accepted, both verbatim:
42
+ *
43
+ * - a coded action app's `action-schema.json` — each section a JSON-Schema-ish
44
+ * `{ type: 'object', properties: { <name>: { type, required? } } }`;
45
+ * - the platform's action schema — each section an array of
46
+ * `{ name, type: 'System.String', required, isList }` (the resource file's
47
+ * `spec.actionSchema`, and what the Apps API returns).
48
+ */
49
+ export function normalizeActionSchema(schema) {
50
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema))
51
+ return 'it is not an object';
52
+ const s = schema;
53
+ const section = (key) => {
54
+ const v = s[key];
55
+ if (v === undefined || v === null)
56
+ return [];
57
+ if (Array.isArray(v)) {
58
+ const args = [];
59
+ for (const a of v) {
60
+ if (!a || typeof a.name !== 'string' || a.name === '' || typeof a.type !== 'string') {
61
+ return `"${key}" has an entry without a name and a type`;
62
+ }
63
+ args.push({ name: a.name, schema: dotNetTypeToJsonSchema(a.type, a.isList === true), required: a.required === true });
64
+ }
65
+ return args;
66
+ }
67
+ if (typeof v !== 'object')
68
+ return `"${key}" is neither an array nor an object`;
69
+ const props = v.properties ?? {};
70
+ if (!props || typeof props !== 'object' || Array.isArray(props))
71
+ return `"${key}.properties" is not an object`;
72
+ const args = [];
73
+ for (const [name, p] of Object.entries(props)) {
74
+ if (!p || typeof p !== 'object' || typeof p.type !== 'string')
75
+ return `"${key}.properties.${name}" has no type`;
76
+ // Outcomes are buttons: push flattens them to System.String whatever they say.
77
+ const schema = key === 'outcomes' ? { type: 'string' } : fileSchema(`${key}.properties.${name}`, p);
78
+ if (typeof schema === 'string')
79
+ return schema;
80
+ args.push({ name, schema, required: p.required === true });
81
+ }
82
+ return args;
83
+ };
84
+ const inputs = section('inputs');
85
+ const outputs = section('outputs');
86
+ const inOuts = section('inOuts');
87
+ const outcomes = section('outcomes');
88
+ for (const r of [inputs, outputs, inOuts, outcomes])
89
+ if (typeof r === 'string')
90
+ return r;
91
+ return {
92
+ inputs: inputs,
93
+ outputs: outputs,
94
+ inOuts: inOuts,
95
+ outcomes: outcomes.map((o) => o.name),
96
+ };
97
+ }
98
+ /**
99
+ * `inputs.app.inputSchema`: the inputs and the inOuts, with the required ones
100
+ * listed — `createInputSchemaFromAppSchema`'s shape, key for key.
101
+ */
102
+ export function appInputSchema(s) {
103
+ const all = [...s.inputs, ...s.inOuts];
104
+ const required = all.filter((a) => a.required).map((a) => a.name);
105
+ return {
106
+ type: 'object',
107
+ properties: Object.fromEntries(all.map((a) => [a.name, { ...a.schema }])),
108
+ ...(required.length > 0 ? { required } : {}),
109
+ };
110
+ }
111
+ /** What the reviewer answers: the outputs and the inOuts, by name. */
112
+ export function appAnswerSchemas(s) {
113
+ return [...s.outputs, ...s.inOuts].map((a) => [a.name, { ...a.schema }]);
114
+ }
115
+ /**
116
+ * What a `check` rule needs of an app's contract — the input names, the
117
+ * required ones, the answers and the buttons — from whichever of `actionSchema`
118
+ * and a verbatim `inputSchema` the app carries; `undefined` with neither, a
119
+ * sentence when one is malformed.
120
+ */
121
+ export function appContract(app) {
122
+ if (app?.actionSchema !== undefined && app?.inputSchema !== undefined) {
123
+ return 'it gives both actionSchema and inputSchema; inputSchema is the node\'s verbatim form, for a decompiled flow, so keep one';
124
+ }
125
+ if (app?.actionSchema !== undefined) {
126
+ const s = normalizeActionSchema(app.actionSchema);
127
+ if (typeof s === 'string')
128
+ return s;
129
+ const all = [...s.inputs, ...s.inOuts];
130
+ return {
131
+ inputs: all.map((a) => a.name),
132
+ required: all.filter((a) => a.required).map((a) => a.name),
133
+ answers: [...s.outputs, ...s.inOuts].map((a) => a.name),
134
+ outcomes: s.outcomes,
135
+ };
136
+ }
137
+ if (app?.inputSchema !== undefined) {
138
+ const i = app.inputSchema;
139
+ if (!i || typeof i !== 'object' || !i.properties || typeof i.properties !== 'object' || Array.isArray(i.properties)) {
140
+ return 'its inputSchema has no properties object';
141
+ }
142
+ return {
143
+ inputs: Object.keys(i.properties),
144
+ required: Array.isArray(i.required) ? i.required.filter((r) => typeof r === 'string') : [],
145
+ answers: [],
146
+ outcomes: [],
147
+ };
148
+ }
149
+ return undefined;
150
+ }
151
+ /** The `action-schema.json` property a picker-shaped JSON Schema came from, if any. */
152
+ function fileProperty(p) {
153
+ if (!p || typeof p !== 'object' || Array.isArray(p))
154
+ return undefined;
155
+ const { type, format, items, ...rest } = p;
156
+ if (Object.keys(rest).length > 0)
157
+ return undefined;
158
+ if (type === 'array') {
159
+ const item = fileProperty(items);
160
+ return item !== undefined && format === undefined ? { type: 'array', items: item } : undefined;
161
+ }
162
+ if (items !== undefined)
163
+ return undefined;
164
+ if (type === 'string' && format === 'date-time')
165
+ return { type: 'string', format: 'date' };
166
+ if (format !== undefined)
167
+ return undefined;
168
+ const byType = {
169
+ string: 'string', integer: 'integer', number: 'number', boolean: 'boolean',
170
+ 'System.Object': 'object',
171
+ 'UiPath.Platform.ResourceHandling.IResource': 'file',
172
+ 'UiPath.DocumentProcessing.Contracts.Actions.ContentValidationData': 'ContentValidationData',
173
+ };
174
+ return typeof type === 'string' && Object.hasOwn(byType, type) ? { type: byType[type] } : undefined;
175
+ }
176
+ /**
177
+ * The schema back from a node, as decompile prints it:
178
+ *
179
+ * - `{ actionSchema }`, in the `action-schema.json` spelling, when one
180
+ * reproduces the node's `inputSchema` and typed answer BYTE FOR BYTE — what
181
+ * the SDK and Studio Web's picker write. A property in both the input schema
182
+ * and the answer is an inOut; the rest split by side; outcomes are the task's.
183
+ * - `{ inputSchema }`, verbatim, otherwise — a hand-edited schema (a `title`, a
184
+ * nested object's `properties`) that no action schema lowers to. Recompiling
185
+ * writes it back unchanged.
186
+ *
187
+ * Either way serialize writes the node's `inputSchema` back as it was: the lift
188
+ * is checked against the emission, not assumed to invert it.
189
+ */
190
+ export function liftActionSchema(inputSchema, answers, outcomes) {
191
+ if (!inputSchema || typeof inputSchema !== 'object' || Array.isArray(inputSchema))
192
+ return undefined;
193
+ const verbatim = { inputSchema: JSON.parse(JSON.stringify(inputSchema)) };
194
+ const props = (inputSchema.properties ?? {});
195
+ const required = new Set(Array.isArray(inputSchema.required)
196
+ ? inputSchema.required.filter((r) => typeof r === 'string') : []);
197
+ const answerProps = Object.fromEntries(Object.entries(answers ?? {}).filter(([k]) => k !== 'Action'));
198
+ const lift = (entries, req) => {
199
+ const out = [];
200
+ for (const [k, p] of entries) {
201
+ const f = fileProperty(p);
202
+ if (f === undefined)
203
+ return undefined;
204
+ out.push([k, { ...f, ...(req(k) ? { required: true } : {}) }]);
205
+ }
206
+ return { type: 'object', properties: Object.fromEntries(out) };
207
+ };
208
+ const ins = lift(Object.entries(props).filter(([k]) => !(k in answerProps)), (k) => required.has(k));
209
+ const ios = lift(Object.entries(props).filter(([k]) => k in answerProps), (k) => required.has(k));
210
+ const outs = lift(Object.entries(answerProps).filter(([k]) => !(k in props)), () => false);
211
+ if (!ins || !ios || !outs)
212
+ return verbatim;
213
+ const actionSchema = {
214
+ inputs: ins,
215
+ ...(Object.keys(outs.properties).length > 0 ? { outputs: outs } : {}),
216
+ ...(Object.keys(ios.properties).length > 0 ? { inOuts: ios } : {}),
217
+ ...(outcomes.length > 0
218
+ ? { outcomes: { type: 'object', properties: Object.fromEntries(outcomes.map((o) => [o, { type: 'string' }])) } }
219
+ : {}),
220
+ };
221
+ // Only a lift that serializes back to the same bytes is an action schema.
222
+ const again = normalizeActionSchema(actionSchema);
223
+ if (typeof again === 'string')
224
+ return verbatim;
225
+ const same = JSON.stringify(appInputSchema(again)) === JSON.stringify(inputSchema)
226
+ && JSON.stringify(Object.fromEntries(appAnswerSchemas(again))) === JSON.stringify(answerProps);
227
+ return same ? { actionSchema } : verbatim;
228
+ }
@@ -3,6 +3,7 @@ import { nearestName } from '../../../core/edit-distance.js';
3
3
  import { FORM_FIELD_TYPES, formFieldOptionsProblem, formFieldOptionsWhy, formFieldSettingProblems, formFieldTypeHint, formFieldTypeWhy, validFormFieldTypesText, } from '../../../core/form-field-type.js';
4
4
  import { isAnswerField } from '../../../core/hitl-answers.js';
5
5
  import { hitlDerivesOutcomeHandles, outcomeIdOf } from '../../../core/hitl-routing.js';
6
+ import { appContract } from './action-schema.js';
6
7
  export const HITL_DIAGNOSTICS = {
7
8
  HITL_VARIANT_UNKNOWN: { severity: 'error', summary: 'a variant that names no human-task node type' },
8
9
  HITL_OUTCOME_PORTS_VARIANT: { severity: 'error', summary: 'exposeError or outcomePorts on a variant that cannot honour it' },
@@ -19,6 +20,11 @@ export const HITL_DIAGNOSTICS = {
19
20
  HITL_APP_WITH_FIELDS: { severity: 'error', summary: 'form fields on an app-backed task' },
20
21
  HITL_APP_MISSING: { severity: 'error', summary: 'an action-app task that names no app' },
21
22
  HITL_APP_INCOMPLETE: { severity: 'error', summary: 'an app reference missing name, key or folderPath' },
23
+ HITL_APP_SCHEMA_INVALID: { severity: 'error', summary: 'an app actionSchema that is not an action schema' },
24
+ HITL_APP_NO_SCHEMA: { severity: 'warning', summary: 'inputs sent to an app with no actionSchema, so every one is a string' },
25
+ HITL_APP_INPUT_UNKNOWN: { severity: 'error', summary: 'an app input the action schema does not declare' },
26
+ HITL_APP_INPUT_REQUIRED: { severity: 'error', summary: 'a required app input left unbound' },
27
+ HITL_APP_OUTCOME_UNKNOWN: { severity: 'error', summary: 'an outcome the app has no button for' },
22
28
  HITL_NO_FIELDS: { severity: 'error', summary: 'no fields array' },
23
29
  HITL_FIELD_NO_ID: { severity: 'error', summary: 'a form field with no id' },
24
30
  HITL_DUP_FIELD: { severity: 'error', summary: 'a form field id declared twice' },
@@ -183,6 +189,48 @@ export function checkHitl(inputs, ctx) {
183
189
  return;
184
190
  }
185
191
  }
192
+ // The app's action schema: what types its inputs, and what it answers.
193
+ const bound = Object.keys(app.inputs ?? {});
194
+ const contract = appContract(app);
195
+ if (contract === undefined) {
196
+ if (bound.length > 0) {
197
+ at('HITL_APP_NO_SCHEMA', `Human task "${step}" sends ${bound.length} input(s) to its Action App but gives no actionSchema. `
198
+ + `The platform types the task's arguments from the app's schema and falls back to string for every `
199
+ + `input without one, so a number or a boolean reaches the app as text — and its answer fields `
200
+ + `cannot be read with out('${step}', …).`, `app: { …, actionSchema: <the app's action-schema.json> }`);
201
+ }
202
+ }
203
+ else if (typeof contract === 'string') {
204
+ at('HITL_APP_SCHEMA_INVALID', `Human task "${step}" gives its Action App a schema that is not usable: ${contract}. `
205
+ + `Pass the app's action-schema.json verbatim, or the platform's action schema (its `
206
+ + `inputs/outputs/inOuts/outcomes arrays).`);
207
+ return;
208
+ }
209
+ else {
210
+ const names = contract.inputs;
211
+ for (const k of bound) {
212
+ if (!names.includes(k)) {
213
+ const nearest = nearestName(k, names);
214
+ at('HITL_APP_INPUT_UNKNOWN', `Human task "${step}" sends "${k}" to its Action App, which declares no such input `
215
+ + `(it takes ${names.length > 0 ? names.join(', ') : 'none'}). The value is dropped.`, nearest !== undefined ? `inputs: { ${JSON.stringify(nearest)}: … }` : undefined);
216
+ }
217
+ }
218
+ const missing = contract.required.filter((r) => !bound.includes(r));
219
+ if (missing.length > 0) {
220
+ at('HITL_APP_INPUT_REQUIRED', `Human task "${step}" leaves the Action App's required input(s) ${missing.join(', ')} unbound, `
221
+ + `so the task's arguments fail the app's own schema.`, `inputs: { ${missing.map((m) => `${JSON.stringify(m)}: …`).join(', ')} }`);
222
+ }
223
+ if (contract.outcomes.length > 0) {
224
+ for (const o of Array.isArray(inputs?.outcomes) ? inputs.outcomes : []) {
225
+ const name = typeof o === 'string' ? o : o?.name;
226
+ if (typeof name === 'string' && name !== '' && !contract.outcomes.includes(name)) {
227
+ const nearest = nearestName(name, contract.outcomes);
228
+ at('HITL_APP_OUTCOME_UNKNOWN', `Human task "${step}" routes on the outcome "${name}", but its Action App completes only with `
229
+ + `${contract.outcomes.join(', ')}. The reviewer can never pick it.`, nearest !== undefined ? `'${nearest}'` : undefined);
230
+ }
231
+ }
232
+ }
233
+ }
186
234
  // Outcomes still matter for an app task: the flow's own routing and its
187
235
  // `Action`/`status` outputs are derived from them, so fall through to the
188
236
  // outcome rules below rather than returning here.
@@ -389,11 +437,23 @@ export function checkHitlRead({ node: name, field, rest, reader: step }, form, r
389
437
  return; // `$vars.<name>.output` — the whole object, fine.
390
438
  if (answer === 'Action')
391
439
  return; // which outcome was pressed.
392
- const answers = (form?.fields ?? [])
393
- .filter((f) => isAnswerField(f) && typeof f?.id === 'string')
394
- .map((f) => f.id);
440
+ const appSchema = form?.variant === 'action-app' && form.app?.actionSchema !== undefined
441
+ ? appContract({ ...form.app, inputSchema: undefined }) : undefined;
442
+ const appAnswers = typeof appSchema === 'object' ? appSchema.answers : [];
443
+ const answers = [
444
+ ...(form?.fields ?? []).filter((f) => isAnswerField(f) && typeof f?.id === 'string').map((f) => f.id),
445
+ ...appAnswers,
446
+ ];
395
447
  if (answers.includes(answer))
396
448
  return;
449
+ if (form?.variant === 'action-app') {
450
+ const nearestApp = nearestName(answer, [...answers, 'Action']);
451
+ report('HITL_READ_UNKNOWN_FIELD', `Human task "${name}" has no answer "${answer}" (read as "$vars.${name}.output.${answer}" in `
452
+ + `"${step}"). Its Action App answers with ${appSchema === undefined
453
+ ? 'fields only its actionSchema declares, and it gives none'
454
+ : `its schema's outputs and inOuts (${appAnswers.length > 0 ? appAnswers.join(', ') : 'none'})`} plus "Action".`, nearestApp !== undefined ? `out('${name}', '${nearestApp}')` : `app: { …, actionSchema: <the app's action-schema.json> }`);
455
+ return;
456
+ }
397
457
  const shown = (form?.fields ?? [])
398
458
  .filter((f) => f?.direction === 'input' && typeof f?.id === 'string')
399
459
  .map((f) => f.id);
@@ -19,6 +19,7 @@ import { OUTCOME_HANDLE_NODE_TYPES, hitlExitModel, outcomeIdOf, outcomeSlug } fr
19
19
  import { jsonClone } from '../../../core/json-clone.js';
20
20
  import { schemaId } from '../../../core/stable-id.js';
21
21
  import { wireExprBody } from '../../../core/wire-expr.js';
22
+ import { appAnswerSchemas, appInputSchema, liftActionSchema, normalizeActionSchema } from './action-schema.js';
22
23
  import { checkHitl, checkHitlRead } from './check.js';
23
24
  import { CompileRefusal } from '../../../core/compile-refusal.js';
24
25
  const BASE = 'uipath.human-in-the-loop';
@@ -107,6 +108,15 @@ function hitlInputs(inputs, nodeType, ctx) {
107
108
  continue;
108
109
  properties[f.id] = { ...formFieldJsonSchema(f.type) };
109
110
  }
111
+ // An app's answer is its action schema's outputs and inOuts — the app owns the
112
+ // form, so the schema is the only place they are declared.
113
+ const appSchema = isApp && inputs?.app?.actionSchema !== undefined
114
+ ? normalizeActionSchema(inputs.app.actionSchema) : undefined;
115
+ const schema = typeof appSchema === 'object' ? appSchema : undefined;
116
+ if (schema !== undefined) {
117
+ for (const [name, s] of appAnswerSchemas(schema))
118
+ properties[name] = s;
119
+ }
110
120
  const names = outcomes.map((o) => o.name).filter((n) => typeof n === 'string');
111
121
  if (names.length > 0) {
112
122
  properties.Action = { type: 'string', enum: names, default: names[0] };
@@ -187,6 +197,10 @@ function hitlInputs(inputs, nodeType, ctx) {
187
197
  key: app.key,
188
198
  folderPath: app.folderPath,
189
199
  ...(app.appSystemName !== undefined ? { appSystemName: app.appSystemName } : {}),
200
+ // What types the task's arguments (see action-schema.ts): lowered
201
+ // from the action schema, or the decompiled node's own, verbatim.
202
+ ...(schema !== undefined ? { inputSchema: appInputSchema(schema) }
203
+ : isApp && app.inputSchema !== undefined ? { inputSchema: jsonClone(app.inputSchema) } : {}),
190
204
  },
191
205
  ...(Object.keys(appInputBindings).length > 0 ? { appInputBindings } : {}),
192
206
  }
@@ -406,6 +420,12 @@ function liftHitl(node, typeName, ctx) {
406
420
  ...(app.appSystemName === null ? { appSystemName: null }
407
421
  : typeof app.appSystemName === 'string' ? { appSystemName: app.appSystemName } : {}),
408
422
  ...(appInputs !== undefined ? { inputs: appInputs } : {}),
423
+ // The schema comes back as an action schema when one serializes to the
424
+ // node's exact bytes, else as the node's inputSchema verbatim — either way a
425
+ // recompile writes the same input schema (action-schema.ts).
426
+ ...(variant === 'action-app' && app.inputSchema !== undefined
427
+ ? liftActionSchema(app.inputSchema, n.outputs?.output?.properties, outcomes.map((o) => (typeof o === 'string' ? o : o.name)))
428
+ : {}),
409
429
  };
410
430
  // The validation station's own inputs ride as PLAIN `$vars.…` paths. Anything
411
431
  // but a `$vars.` reference stays the wire text verbatim.
@@ -502,6 +522,8 @@ function printHitl({ inputs }, ctx) {
502
522
  : h.app.appSystemName !== undefined ? ctx.string(h.app.appSystemName) : undefined],
503
523
  ['inputs', h.app.inputs !== undefined
504
524
  ? obj(Object.entries(h.app.inputs).map(([k, v]) => [k, val(v)]), ctx) : undefined],
525
+ ['actionSchema', h.app.actionSchema !== undefined ? ctx.value(h.app.actionSchema) : undefined],
526
+ ['inputSchema', h.app.inputSchema !== undefined ? ctx.value(h.app.inputSchema) : undefined],
505
527
  ], ctx);
506
528
  const document = h.document === undefined ? undefined : obj([
507
529
  ['extractionResult', val(h.document.extractionResult)],
@@ -182,7 +182,106 @@ export interface AppRef {
182
182
  * into the task's `HitlTaskArguments`.
183
183
  */
184
184
  inputs?: Record<string, Expr | string>;
185
+ /**
186
+ * The app's action schema, verbatim — what types each input, and what the
187
+ * reviewer answers. For a coded action app that is its `action-schema.json`,
188
+ * so `import schema from './InvoiceReview/action-schema.json'` and pass it;
189
+ * the platform's own spelling (the arrays of `{ name, type: 'System.Decimal',
190
+ * required }` in the solution's `resources/solution_folder/app/codedAction/<app>.json`
191
+ * `spec.actionSchema`) is accepted too.
192
+ *
193
+ * Without it every input reaches the app as a STRING: the platform types the
194
+ * task's arguments from `inputs.app.inputSchema`, which is written from this,
195
+ * and falls back to `string` for each bound input when there is none. It also
196
+ * declares the answer, so `out('<step>', '<output>')` reads are checked.
197
+ *
198
+ * @enforcedBy HITL_APP_NO_SCHEMA Inputs sent to an app without its schema all become strings.
199
+ * @enforcedBy HITL_APP_INPUT_UNKNOWN An input the app does not declare.
200
+ * @enforcedBy HITL_APP_INPUT_REQUIRED A required app input left unbound.
201
+ * @enforcedBy HITL_APP_OUTCOME_UNKNOWN An outcome the app has no button for.
202
+ *
203
+ * Only each argument's TYPE reaches the node, as Studio Web writes it: an
204
+ * object input becomes `{ type: 'System.Object' }` and a `file` input
205
+ * `{ type: 'UiPath.Platform.ResourceHandling.IResource' }` (the platform's .NET
206
+ * names, passed through by its picker), and a nested object's `properties`, a
207
+ * `description` or a `title` are not carried.
208
+ */
209
+ actionSchema?: ActionSchema;
210
+ /**
211
+ * The node's `inputs.app.inputSchema`, verbatim — what decompile writes for a
212
+ * flow whose schema no {@link AppRef.actionSchema} lowers to (a hand-edited
213
+ * `title`, a nested object's `properties`), so recompiling leaves it as it
214
+ * was. Author new flows with `actionSchema`; give one or the other.
215
+ *
216
+ * @enforcedBy HITL_APP_SCHEMA_INVALID Both forms, or an inputSchema with no properties.
217
+ */
218
+ inputSchema?: Record<string, unknown>;
219
+ }
220
+ /** One property of an `action-schema.json` section. */
221
+ export interface ActionSchemaProperty {
222
+ /**
223
+ * Its type: `'string'`, `'integer'`, `'number'`, `'boolean'`, `'object'`,
224
+ * `'file'`, `'ContentValidationData'`, or `'array'` with `items` — the set
225
+ * `uip codedapp push` accepts.
226
+ */
227
+ type: string;
228
+ /** Whether the app needs it. A required input must be bound in `app.inputs`. */
229
+ required?: boolean;
230
+ /** What it is for. Documentation only: it is not emitted. */
231
+ description?: string;
232
+ /** `'date'` (a `System.DateOnly`) or `'uuid'` (a `System.Guid`). */
233
+ format?: string;
234
+ /** An array's item schema, e.g. `{ type: 'string' }`. */
235
+ items?: Record<string, unknown>;
236
+ /** An object's own properties. The app keeps them; the node does not. */
237
+ properties?: Record<string, ActionSchemaProperty>;
238
+ }
239
+ /** One section (`inputs`, `outputs`, `inOuts`, `outcomes`) of an `action-schema.json`. */
240
+ export interface ActionSchemaSection {
241
+ /** Always `'object'`. */
242
+ type?: 'object';
243
+ /** The section's arguments, by the app's own names. */
244
+ properties?: Record<string, ActionSchemaProperty>;
245
+ }
246
+ /** A coded action app's `action-schema.json`. */
247
+ export interface ActionSchemaFile {
248
+ /** What the task sends the app. Becomes `inputs.app.inputSchema`. */
249
+ inputs?: ActionSchemaSection;
250
+ /** What the reviewer answers. Readable as `out('<step>', '<name>')`. */
251
+ outputs?: ActionSchemaSection;
252
+ /** Sent to the app AND answered back. */
253
+ inOuts?: ActionSchemaSection;
254
+ /** The buttons that complete the task. The flow's `outcomes` must be among them. */
255
+ outcomes?: ActionSchemaSection;
256
+ }
257
+ /** One argument of the platform's action schema. */
258
+ export interface PlatformActionSchemaArg {
259
+ /** The argument's name, as the app declares it. */
260
+ name: string;
261
+ /** A .NET type name, e.g. `'System.Decimal'`. */
262
+ type: string;
263
+ /** Whether the app needs it. */
264
+ required?: boolean;
265
+ /** A list of `type`, which lowers to a JSON array. */
266
+ isList?: boolean;
267
+ /** The platform's other keys (`key`, `version`, `typeNamespace`, …), ignored. */
268
+ [field: string]: unknown;
269
+ }
270
+ /** The platform's action schema (Apps API, a solution's `spec.actionSchema`). */
271
+ export interface PlatformActionSchema {
272
+ /** What the task sends the app. */
273
+ inputs?: PlatformActionSchemaArg[];
274
+ /** What the reviewer answers. */
275
+ outputs?: PlatformActionSchemaArg[];
276
+ /** Sent and answered back. */
277
+ inOuts?: PlatformActionSchemaArg[];
278
+ /** The buttons that complete the task. */
279
+ outcomes?: PlatformActionSchemaArg[];
280
+ /** The schema's other keys (`key`, `id`, `name`, …), ignored. */
281
+ [field: string]: unknown;
185
282
  }
283
+ /** An Action App's action schema, in either spelling. */
284
+ export type ActionSchema = ActionSchemaFile | PlatformActionSchema;
186
285
  export interface HitlInputs {
187
286
  /**
188
287
  * Which human-task node type to emit. Omit it for today's default — see