@uipath/maestro-builder-sdk 6.15.5 → 6.16.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.
@@ -56,6 +56,7 @@ import BpmnModdle from 'bpmn-moddle';
56
56
  import { createRequire } from 'node:module';
57
57
  import { registryType, rowDeclaration } from './typed-node.js';
58
58
  import { errorSchema, errorSchemaWithResponse } from './bpmn-sdk.js';
59
+ import { SCRIPT_ARGS_ENVELOPE, defaultScriptInputSchema } from './serialize.js';
59
60
  import { BPMN_SCHEMA_VERSIONS } from './format-profile.js';
60
61
  import { preserveSchemaVersion } from '../schema-version.js';
61
62
  const require = createRequire(import.meta.url);
@@ -654,7 +655,17 @@ function scriptVersionOf(el) {
654
655
  /** A `uipath:mapping` script task. */
655
656
  function emitScriptTask(chain, el, payload, type) {
656
657
  const args = (payload?.input ?? []).find((r) => r.name === 'args');
657
- const inputs = args ? safeJson(args.body) : undefined;
658
+ // The serializer writes the product's envelope — `vars`/`metadata`, and the default
659
+ // inputSchema declaring them — on every script task, so the source need not repeat
660
+ // them: an `args` that is exactly the envelope decompiles to no `inputs` at all, and
661
+ // only what the designer added (`"iterator[0]":"=iterator[0]"`) comes through. A
662
+ // schema that differs from the default (the designer's carries `$schema`) is kept.
663
+ const rawInputs = args ? safeJson(args.body) : undefined;
664
+ const inputs = rawInputs
665
+ ? Object.fromEntries(Object.entries(rawInputs).filter(([k, v]) => !(k in SCRIPT_ARGS_ENVELOPE) || v !== SCRIPT_ARGS_ENVELOPE[k]))
666
+ : undefined;
667
+ const schemaBody = payload?.context?.inputSchema?.body;
668
+ const schemaIsDefault = schemaBody !== undefined && rawInputs !== undefined && JSON.stringify(safeJson(schemaBody)) === JSON.stringify(defaultScriptInputSchema(rawInputs));
658
669
  const outputs = {};
659
670
  for (const row of payload?.output ?? [])
660
671
  outputs[row.var ?? row.name] = row.source;
@@ -680,7 +691,7 @@ function emitScriptTask(chain, el, payload, type) {
680
691
  'type',
681
692
  type !== undefined && type !== 'BPMN.Variables' && type !== 'BPMN.ScriptTask' ? str(type) : undefined,
682
693
  ],
683
- ['inputSchema', payload?.context?.inputSchema?.body !== undefined ? jsonLiteral(payload.context.inputSchema.body) : undefined],
694
+ ['inputSchema', schemaBody !== undefined && !schemaIsDefault ? jsonLiteral(schemaBody) : undefined],
684
695
  ['inputs', record(inputs)],
685
696
  ['outputs', verbatim ? undefined : record(outputs)],
686
697
  ['outputRows', verbatim],
@@ -66,6 +66,14 @@ export declare function intsvcOutputVar(n: EventNode | ExternalNode): string | u
66
66
  * @returns The moddle element.
67
67
  */
68
68
  export declare function createConnectorEvent(ctx: ConnectorCtx, n: EventNode): El;
69
+ /**
70
+ * The `body` of a connector event node: the subscription's filter in the one place the
71
+ * platform reads it. `scope` (the `filter` option — the operation's event parameters, by
72
+ * equality) becomes both the `queryParams` and the leading `(k == 'v')` clauses;
73
+ * `filters` (payload conditions) follow, each in parentheses, all AND-joined. No scope and
74
+ * no filters is `{}`, which is what the designer writes for an unfiltered subscription.
75
+ */
76
+ export declare function eventFilterBody(nodeId: string, scope: Record<string, unknown> | undefined, filters: readonly unknown[] | undefined): Record<string, unknown>;
69
77
  /**
70
78
  * An external agent / workflow invocation.
71
79
  *
@@ -1,5 +1,6 @@
1
1
  import { createTypedNode, typedNodeOutputVar } from './typed-node.js';
2
2
  import { connectorBindingRef } from './connector.js';
3
+ import { eventFilterJmes, eventFilterProblem, readEventFilter } from '../core/event-filters.js';
3
4
  /**
4
5
  * Which extension type an external invocation lowers to.
5
6
  *
@@ -96,8 +97,18 @@ export function createConnectorEvent(ctx, n) {
96
97
  // and watches nothing. A curated entry carries its own and the author passes none.
97
98
  objectName: n.object ?? op.objectName ?? '',
98
99
  };
99
- if (n.filter !== undefined)
100
- inputs.filter = n.filter;
100
+ // The subscription's filter goes in the `body` row, where the platform reads it:
101
+ // `PO.BpmnEngine` `BpmnInputExtensions.GetEventFilters` takes `body.filters.expression`
102
+ // (and `queryParams` registers the scope with the connector). This emitter used to write
103
+ // `<uipath:input name="filter" target="filter">{…}</uipath:input>`, an input no handler
104
+ // reads: on flow-v2-run-12 the wait so scoped validated and matched every mail in the
105
+ // mailbox (outlook-waitfor-email 1.00 → 0.43), while the v1 arm and every Studio Web
106
+ // export write `{"filters":{"expression":"(parentFolderId == '…') && (contains(subject, '…'))"},"queryParams":{"parentFolderId":"…"}}`.
107
+ //
108
+ // The expression is the scope's equality clauses AND-joined with the authored
109
+ // `filters`, each lowered by the designer's own grammar (`core/event-filters.ts`) — the
110
+ // same lowering the Flow serializer does for `onEvent`/`waitForEvent`.
111
+ inputs.body = eventFilterBody(n.id, n.filter, n.filters);
101
112
  if (n.parameters !== undefined)
102
113
  inputs.parameters = n.parameters;
103
114
  return createTypedNode(ctx.m, n.id, type, {
@@ -108,6 +119,36 @@ export function createConnectorEvent(ctx, n) {
108
119
  skipCondition: n.skipCondition,
109
120
  }, (varId) => ctx.varTypeOf?.(varId));
110
121
  }
122
+ /**
123
+ * The `body` of a connector event node: the subscription's filter in the one place the
124
+ * platform reads it. `scope` (the `filter` option — the operation's event parameters, by
125
+ * equality) becomes both the `queryParams` and the leading `(k == 'v')` clauses;
126
+ * `filters` (payload conditions) follow, each in parentheses, all AND-joined. No scope and
127
+ * no filters is `{}`, which is what the designer writes for an unfiltered subscription.
128
+ */
129
+ export function eventFilterBody(nodeId, scope, filters) {
130
+ const scopeEntries = Object.entries(scope ?? {}).filter(([, v]) => v !== undefined && v !== null && String(v) !== '');
131
+ const scopeClauses = scopeEntries.map(([k, v]) => `(${k} == '${String(v).replace(/'/g, "\\'")}')`);
132
+ const conditions = (filters ?? []).map((leaf, i) => {
133
+ const { read } = readEventFilter(leaf);
134
+ if (!read) {
135
+ throw new Error(`connector event "${nodeId}": filters[${i}] must carry a field and exactly ONE operator ` +
136
+ `(contains, startsWith, endsWith, equals, notEquals, lessThan, lessThanOrEqual, greaterThan, ` +
137
+ `greaterThanOrEqual) — got ${JSON.stringify(leaf)}.`);
138
+ }
139
+ const problem = eventFilterProblem(read);
140
+ if (problem)
141
+ throw new Error(`connector event "${nodeId}": filters[${i}] on \`${read.field}\`: ${problem}`);
142
+ return `(${eventFilterJmes(read)})`;
143
+ });
144
+ const expression = [...scopeClauses, ...conditions].join(' && ');
145
+ const body = {};
146
+ if (expression !== '')
147
+ body.filters = { expression };
148
+ if (scopeEntries.length > 0)
149
+ body.queryParams = Object.fromEntries(scopeEntries);
150
+ return body;
151
+ }
111
152
  /**
112
153
  * An external agent / workflow invocation.
113
154
  *
@@ -26,6 +26,20 @@ export declare function serialize(built: BuiltBpmn, opts?: BpmnSerializeOpts): P
26
26
  */
27
27
  export declare function finalizeBpmnXml(xml: string): string;
28
28
  export declare function processId(id: string): string;
29
+ /**
30
+ * The `args` keys every Studio Web script task carries, in the order the designer writes
31
+ * them. The platform runtime injects these — and only these — into the script's scope.
32
+ */
33
+ export declare const SCRIPT_ARGS_ENVELOPE: {
34
+ readonly vars: "=vars";
35
+ readonly metadata: "=metadata";
36
+ };
37
+ /**
38
+ * The `uipath:inputSchema` the product writes for a script task's `args`: `vars` and
39
+ * `metadata` as objects, plus an untyped property per extra key. Exported so the
40
+ * decompiler can recognise it and leave it out of the source.
41
+ */
42
+ export declare function defaultScriptInputSchema(args: Record<string, unknown>): Record<string, unknown>;
29
43
  /**
30
44
  * The variable declarations a scope's output-bearing nodes imply.
31
45
  *
@@ -444,21 +444,50 @@ function buildErrorMappingExt(ctx, rows) {
444
444
  }),
445
445
  });
446
446
  }
447
+ /**
448
+ * The `args` keys every Studio Web script task carries, in the order the designer writes
449
+ * them. The platform runtime injects these — and only these — into the script's scope.
450
+ */
451
+ export const SCRIPT_ARGS_ENVELOPE = { vars: '=vars', metadata: '=metadata' };
452
+ /**
453
+ * The `uipath:inputSchema` the product writes for a script task's `args`: `vars` and
454
+ * `metadata` as objects, plus an untyped property per extra key. Exported so the
455
+ * decompiler can recognise it and leave it out of the source.
456
+ */
457
+ export function defaultScriptInputSchema(args) {
458
+ const properties = {};
459
+ for (const k of Object.keys(args))
460
+ properties[k] = k in SCRIPT_ARGS_ENVELOPE ? { type: 'object' } : {};
461
+ return { type: 'object', properties };
462
+ }
447
463
  function createScriptTask(ctx, n) {
448
464
  const { m } = ctx;
465
+ // The `args` row is the product's fixed envelope — `vars` and `metadata`, always, in
466
+ // that order — with the author's `inputs` after it. The platform runtime injects the
467
+ // ENVELOPE into the script's scope and nothing else: in flow-v2-run-12 every script
468
+ // that read an `inputs` key as a bare identifier faulted on the cloud with
469
+ // `<key> is not defined` (3 of 3 cloud-executed script tasks; `check`, `compile` and
470
+ // `uip maestro bpmn validate` had all passed), while every Studio Web export in
471
+ // `research/bpmn-roundtrip` and every v1-authored process in the same run carries
472
+ // exactly this envelope and reads `vars.<id>`. The author's keys are kept so an import
473
+ // round-trips (`iterator[0]` is how the designer scopes a loop body) and so a key the
474
+ // author knows the runtime honours can still be passed; a bare read of one is what
475
+ // `check` reports as SCRIPT_BARE_INPUT_REF. See docs/BPMN_EVAL_RUN12_FINDINGS.md §4 P0-1.
476
+ const args = { ...SCRIPT_ARGS_ENVELOPE, ...n.inputs };
477
+ const inputSchema = n.inputSchema ?? defaultScriptInputSchema(args);
449
478
  const mapping = m.create('uipath:Mapping', {
450
479
  version: 'v1',
451
480
  type: m.create('uipath:Type', { value: n.type, version: 'v1' }),
452
- // The context holds only the schema here — a script task has no context ROWS.
453
- context: n.inputSchema === undefined
454
- ? undefined
455
- : m.create('uipath:Context', {
456
- inputSchema: m.create('uipath:InputSchema', {
457
- type: 'jsonSchema',
458
- body: typeof n.inputSchema === 'string' ? n.inputSchema : JSON.stringify(n.inputSchema),
459
- }),
481
+ // The context holds only the schema here — a script task has no context ROWS. Real
482
+ // script tasks all carry the schema; the product refuses to run without the envelope
483
+ // it declares, so an absent one is filled in rather than left out.
484
+ context: m.create('uipath:Context', {
485
+ inputSchema: m.create('uipath:InputSchema', {
486
+ type: 'jsonSchema',
487
+ body: typeof inputSchema === 'string' ? inputSchema : JSON.stringify(inputSchema),
460
488
  }),
461
- input: [m.create('uipath:Input', { name: 'args', type: 'json', target: 'bodyField', body: JSON.stringify(n.inputs) })],
489
+ }),
490
+ input: [m.create('uipath:Input', { name: 'args', type: 'json', target: 'bodyField', body: JSON.stringify(args) })],
462
491
  // Spelled-out rows win over derived ones: `outputs` names each row after the
463
492
  // variable it writes, and the product names it after what the script returns
464
493
  // while binding a differently-named variable. See ScriptTaskOpts.outputRows.
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { resolveFromRegistry } from './registry-fallback.js';
3
+ import { nestConnectorBody } from '../core/connector-inputs.js';
3
4
  import { types } from '../core/expr.js';
4
5
  const require = createRequire(import.meta.url);
5
6
  const REGISTRY = require('./registry-types.json');
@@ -64,7 +65,20 @@ export function isSnapshotType(type) {
64
65
  * constant for the one type known to need it rather than derived.
65
66
  */
66
67
  export const REGISTRY_GAPS = {
67
- 'Orchestrator.StartAgentJob': { resourceSubType: 'Agent' },
68
+ // `folderKey` — the job-start family's v1 addressing field. The registry declares
69
+ // `folderId`, but the engine's v1 branch reads `FolderKey` (`ExecuteApiWorkflowArgs`:
70
+ // `input.GetValueFromContext<string>(nameof(OrchestratorFolderContext.FolderKey))`), and
71
+ // the v1 skill verified live that `releaseKey` without it faults with `Could not get
72
+ // value for key:FolderKey from context in input`. Emitted only for a node addressed by
73
+ // `releaseKey` — see `OrchestratorOpts.folderKey`.
74
+ 'Orchestrator.StartAgentJob': { resourceSubType: 'Agent', extraContext: ['folderKey'] },
75
+ 'Orchestrator.StartJob': { extraContext: ['folderKey'] },
76
+ 'Orchestrator.ExecuteApiWorkflowAsync': { extraContext: ['folderKey'] },
77
+ 'Orchestrator.StartAgenticProcess': { extraContext: ['folderKey'] },
78
+ 'Orchestrator.StartAgenticProcessAsync': { extraContext: ['folderKey'] },
79
+ 'Orchestrator.StartCaseMgmtProcess': { extraContext: ['folderKey'] },
80
+ 'Orchestrator.StartCaseMgmtProcessAsync': { extraContext: ['folderKey'] },
81
+ 'Orchestrator.BusinessRules': { extraContext: ['folderKey'] },
68
82
  // `notEnforcedInputs` — required InputFields the platform does NOT actually enforce.
69
83
  //
70
84
  // MEASURED, on a real trigger that carries `connection`, `folderKey`, `connectorKey`,
@@ -226,9 +240,19 @@ function contextRows(m, type, spec, supplied, verbatim = false) {
226
240
  return rows;
227
241
  }
228
242
  for (const field of spec.context) {
229
- const value = supplied[field.name] ?? field.default;
243
+ let value = supplied[field.name] ?? field.default;
230
244
  if (value === undefined || value === null)
231
245
  continue;
246
+ // A json field whose REGISTRY default is the text `null` is unrunnable as declared:
247
+ // the platform parses the row to a JSON null and `IntSvcUnifiedHttpRequestArgs`
248
+ // throws `Body cannot be null` on it (incident 102018 — flow-v2-run-12
249
+ // slack-http-fallback, a GET with no body, faulted on exactly this after check,
250
+ // compile and validate had passed). Omitting the row is the same null. So a
251
+ // default of `null` on a json field is written as `{}`, the empty object the handler
252
+ // accepts; an author who wrote `null` themselves still gets it verbatim, and an
253
+ // import's `"null"` text round-trips untouched (see decompile's json-row rules).
254
+ if (field.type === 'json' && !(field.name in supplied) && field.default === 'null')
255
+ value = {};
232
256
  emitted.add(field.name);
233
257
  // A `json` field goes in the CDATA BODY, not a `value` attribute. The product
234
258
  // validator parses that field as JSON and reads the attribute WITHOUT XML
@@ -266,6 +290,28 @@ function contextRows(m, type, spec, supplied, verbatim = false) {
266
290
  }
267
291
  return rows;
268
292
  }
293
+ /**
294
+ * The `target` values a payload row can carry — `BpmnInputTarget` in PO.BpmnEngine
295
+ * (`InputResolutionActivities` routes `Body`/`BodyField`/`Header`/`PathVariable`/`Query`/
296
+ * `PathParameters`/`QueryParameters`/`File`), plus the connector emitter's `multipart`.
297
+ * An undeclared `separateInputs` key that is one of these addresses a slot; any other
298
+ * key is a field — see `inputRows`.
299
+ */
300
+ const PAYLOAD_TARGETS = new Set([
301
+ 'body',
302
+ 'bodyField',
303
+ 'header',
304
+ 'headers',
305
+ 'pathVariable',
306
+ 'pathParameters',
307
+ 'query',
308
+ 'queryParameters',
309
+ 'file',
310
+ 'multipart',
311
+ ]);
312
+ function isPlainRecord(value) {
313
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
314
+ }
269
315
  /**
270
316
  * The payload rows, per the type's `inputPattern`.
271
317
  *
@@ -308,11 +354,37 @@ function inputRows(m, spec, inputs) {
308
354
  // `recordId` as `"1111-…"`, quotes included, where every real export writes
309
355
  // `type="string" value="1111-…"`. A structured value is still a json body; a
310
356
  // scalar is written as the `value` attribute, the way a declared scalar is.
357
+ //
358
+ // An undeclared key is a TARGET, then — `body`, `pathParameters`, `queryParameters`
359
+ // — and only a target. A key that is none of the platform's targets is a request
360
+ // FIELD the author wrote where a slot belongs (`inputs: { script: '…' }` on
361
+ // `Intsvc.ActivityExecution`), and keyed as its own target it reaches a row the
362
+ // platform never reads: on flow-v2-run-12 the JDBC `execute-query-synchronously` node
363
+ // so written went out as `<uipath:input name="script" … target="script"/>` and every
364
+ // one of seven cloud runs faulted `Integration Services bad request`, while the v1 arm's
365
+ // `<uipath:input name="body" target="body">{"script":"…"}</uipath:input>` completed
366
+ // first time. Such keys fold into the type's own payload row (`inputName`/`inputTarget`,
367
+ // `body` for the Integration Service family), nested by their dotted ids, exactly as
368
+ // `.connector()` writes a request. A type with no payload row keeps keying them.
311
369
  const declared = new Map(spec.inputs.map((f) => [f.name, f]));
312
- return keys.map((k) => {
370
+ const fieldKeys = spec.inputName !== null && spec.inputTarget !== null
371
+ ? keys.filter((k) => !declared.has(k) && !PAYLOAD_TARGETS.has(k) && k !== spec.inputName)
372
+ : [];
373
+ const folded = new Set(fieldKeys);
374
+ const rows = keys
375
+ .filter((k) => !folded.has(k))
376
+ .map((k) => {
313
377
  const field = declared.get(k);
314
- return payloadInput(m, k, field?.target ?? k, inputs[k], field?.type ?? inferredInputType(inputs[k]));
378
+ let value = inputs[k];
379
+ if (k === spec.inputName && fieldKeys.length > 0 && isPlainRecord(value)) {
380
+ value = { ...nestConnectorBody(Object.fromEntries(fieldKeys.map((f) => [f, inputs[f]]))), ...value };
381
+ }
382
+ return payloadInput(m, k, field?.target ?? k, value, field?.type ?? inferredInputType(value));
315
383
  });
384
+ if (fieldKeys.length > 0 && !keys.includes(spec.inputName)) {
385
+ rows.push(payloadInput(m, spec.inputName, spec.inputTarget, nestConnectorBody(Object.fromEntries(fieldKeys.map((f) => [f, inputs[f]]))), 'json'));
386
+ }
387
+ return rows;
316
388
  }
317
389
  if (!spec.inputName)
318
390
  return [];
@@ -267,6 +267,16 @@
267
267
  "name": "skipCondition",
268
268
  "isAttr": true,
269
269
  "type": "String"
270
+ },
271
+ {
272
+ "name": "argumentOverrides",
273
+ "isAttr": true,
274
+ "type": "String"
275
+ },
276
+ {
277
+ "name": "argumentsInput",
278
+ "isAttr": true,
279
+ "type": "String"
270
280
  }
271
281
  ]
272
282
  },
@@ -301,6 +311,16 @@
301
311
  "name": "skipCondition",
302
312
  "isAttr": true,
303
313
  "type": "String"
314
+ },
315
+ {
316
+ "name": "argumentOverrides",
317
+ "isAttr": true,
318
+ "type": "String"
319
+ },
320
+ {
321
+ "name": "argumentsInput",
322
+ "isAttr": true,
323
+ "type": "String"
304
324
  }
305
325
  ]
306
326
  },
package/dist/check.js CHANGED
@@ -16,7 +16,7 @@ import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from '
16
16
  import { childStepLists, declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
17
17
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
18
18
  import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
19
- import { planFlowGlobals } from './flow-variables.js';
19
+ import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
20
20
  export function check(built, opts = {}) {
21
21
  let diags = [];
22
22
  const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts); });
@@ -81,11 +81,17 @@ function checkFlow(built, ancestors, opts = {}) {
81
81
  declared.add(built.triggerId);
82
82
  for (const ep of built.entryPoints ?? [])
83
83
  declared.add(ep.id);
84
+ // A variable an input feeds with `{ shared }` is declared by the SDK when the
85
+ // author did not declare it (#924), so it is a readable `$vars.<name>` too.
86
+ const sharedAutoVars = planSharedInputs({ roots: sharedInputRoots(built), outputs: built.outputs, vars: built.vars }).autoVars;
87
+ for (const a of sharedAutoVars)
88
+ declared.add(a.name);
84
89
  // Declared type per name, for the output/return category check below.
85
90
  const typeOf = new Map([
86
91
  ...built.inputs.map((x) => [x.name, String(x.type)]),
87
92
  ...(built.entryPoints ?? []).flatMap((ep) => ep.inputs.map((x) => [x.name, String(x.type)])),
88
93
  ...built.vars.map((x) => [x.name, String(x.type)]),
94
+ ...sharedAutoVars.map((x) => [x.name, x.type]),
89
95
  ]);
90
96
  const outputType = new Map(built.outputs.map((x) => [x.name, String(x.type)]));
91
97
  const stepNames = new Set();
@@ -1411,6 +1417,14 @@ function checkSubflow(step, child, inputs, parent, ancestors, diags) {
1411
1417
  at('SUBFLOW_CHILD_TRIGGER', `Subflow "${step}" runs a flow with its own \`.trigger(…)\`. A subflow is started by its caller, never by a `
1412
1418
  + `trigger of its own — the schedule would be dropped. Remove the .trigger() from "${child.id}".`);
1413
1419
  }
1420
+ // `{ shared }` merges one input across a flow's ROOTS (#924). A subflow has
1421
+ // one root, its caller, so there is nothing to merge; serialize refuses it.
1422
+ for (const inp of child.inputs) {
1423
+ if (inp.shared === undefined)
1424
+ continue;
1425
+ at('SHARED_INPUT_IN_SUBFLOW', `Subflow "${step}" runs "${child.id}", whose input "${inp.name}" has { shared: "${inp.shared}" }. A subflow has `
1426
+ + `one root — its caller — so there is nothing to merge. Read the input with input('${inp.name}') inside the child.`);
1427
+ }
1414
1428
  const declared = new Map(child.inputs.map((i) => [i.name, String(i.type)]));
1415
1429
  const given = inputs ?? {};
1416
1430
  for (const key of Object.keys(given)) {
@@ -1653,7 +1667,14 @@ const INTERVAL_WORDS = [
1653
1667
  * said `Errors: 0`, validate refused the file).
1654
1668
  */
1655
1669
  function checkGlobalNames(built, diags) {
1656
- for (const p of planFlowGlobals({ inputs: built.inputs ?? [], outputs: built.outputs ?? [], vars: built.vars ?? [] }).problems) {
1670
+ // `{ shared }` inputs (#924) may add `.var()`s; the plan is the serializer's
1671
+ // own (`emittableGlobals`), so check and the emitted file agree on them.
1672
+ const shared = planSharedInputs({ roots: sharedInputRoots(built), outputs: built.outputs ?? [], vars: built.vars ?? [] });
1673
+ for (const p of shared.problems) {
1674
+ diags.push({ level: p.level, code: p.code, message: p.message, ...(p.suggestion ? { suggestion: p.suggestion } : {}) });
1675
+ }
1676
+ const vars = [...(built.vars ?? []), ...shared.autoVars];
1677
+ for (const p of planFlowGlobals({ inputs: built.inputs ?? [], outputs: built.outputs ?? [], vars }).problems) {
1657
1678
  diags.push({ level: 'error', code: p.code, message: p.message, ...(p.suggestion ? { suggestion: p.suggestion } : {}) });
1658
1679
  }
1659
1680
  }
@@ -1665,7 +1686,12 @@ function checkGlobalNames(built, diags) {
1665
1686
  */
1666
1687
  function checkNodeUpdates(built, diags) {
1667
1688
  const inputs = new Set(built.inputs.map((v) => v.name));
1668
- const writable = new Set([...built.vars.map((v) => v.name), ...built.outputs.map((v) => v.name)]);
1689
+ const writable = new Set([
1690
+ ...built.vars.map((v) => v.name),
1691
+ ...built.outputs.map((v) => v.name),
1692
+ // A variable declared by `{ shared }` (#924) is a `.var()` like any other.
1693
+ ...planSharedInputs({ roots: sharedInputRoots(built), outputs: built.outputs, vars: built.vars }).autoVars.map((v) => v.name),
1694
+ ]);
1669
1695
  const visit = (steps) => {
1670
1696
  for (const s of steps) {
1671
1697
  const updates = s.options?.updates;
@@ -78,3 +78,23 @@ interface ConnectorSlots {
78
78
  };
79
79
  };
80
80
  }
81
+ /**
82
+ * Fold a body keyed by the registry's dotted field ids into the nested request
83
+ * object those ids describe: `{"fields.project.key":"CE"}` → `{"fields":{"project":{"key":"CE"}}}`.
84
+ *
85
+ * `buildConnectorInputs` keeps the DOTTED keys on purpose — Flow's `bodyParameters` is a
86
+ * flat map and Flow's runtime (`convertDotNotationToNested`) nests it before the call.
87
+ * The Maestro BPMN runtime does not: `IntSvcSendTaskHandler` sends the `body` input as
88
+ * written, so a dotted key reaches the connector as a literal property name. Measured on
89
+ * flow-v2-run-12: the SDK's Jira `create-issue` body went out flat and Integration
90
+ * Service answered 400 `project: Specify a valid project ID or key`; the v1 arm, which
91
+ * writes the nested form by hand, created the ticket. A BPMN body therefore goes through
92
+ * this before it is serialised.
93
+ *
94
+ * A `[*]` segment names an array. On a leaf (`users[*]`) an array value is kept and a
95
+ * scalar becomes a one-element list, as the product's own conversion does; on an inner
96
+ * segment (`attachments[*].url`) each element of an array value becomes one object, and
97
+ * a scalar one. Later keys win over earlier ones at the same leaf; a nested object the
98
+ * author already wrote under a plain key is merged into, not replaced.
99
+ */
100
+ export declare function nestConnectorBody(flat: Record<string, unknown>): Record<string, unknown>;
@@ -259,6 +259,57 @@ const SLOT_FOR_LOCATION = {
259
259
  query: 'queryParameters',
260
260
  path: 'pathParameters',
261
261
  };
262
+ /**
263
+ * Fold a body keyed by the registry's dotted field ids into the nested request
264
+ * object those ids describe: `{"fields.project.key":"CE"}` → `{"fields":{"project":{"key":"CE"}}}`.
265
+ *
266
+ * `buildConnectorInputs` keeps the DOTTED keys on purpose — Flow's `bodyParameters` is a
267
+ * flat map and Flow's runtime (`convertDotNotationToNested`) nests it before the call.
268
+ * The Maestro BPMN runtime does not: `IntSvcSendTaskHandler` sends the `body` input as
269
+ * written, so a dotted key reaches the connector as a literal property name. Measured on
270
+ * flow-v2-run-12: the SDK's Jira `create-issue` body went out flat and Integration
271
+ * Service answered 400 `project: Specify a valid project ID or key`; the v1 arm, which
272
+ * writes the nested form by hand, created the ticket. A BPMN body therefore goes through
273
+ * this before it is serialised.
274
+ *
275
+ * A `[*]` segment names an array. On a leaf (`users[*]`) an array value is kept and a
276
+ * scalar becomes a one-element list, as the product's own conversion does; on an inner
277
+ * segment (`attachments[*].url`) each element of an array value becomes one object, and
278
+ * a scalar one. Later keys win over earlier ones at the same leaf; a nested object the
279
+ * author already wrote under a plain key is merged into, not replaced.
280
+ */
281
+ export function nestConnectorBody(flat) {
282
+ const out = {};
283
+ for (const [name, value] of Object.entries(flat)) {
284
+ const parts = connectorPath(name);
285
+ place(out, parts, 0, value);
286
+ }
287
+ return out;
288
+ }
289
+ function place(target, parts, index, value) {
290
+ const part = parts[index];
291
+ const last = index === parts.length - 1;
292
+ if (last) {
293
+ target[part.key] = part.array && !Array.isArray(value) ? [value] : value;
294
+ return;
295
+ }
296
+ if (part.array) {
297
+ const elements = Array.isArray(value) ? value : [value];
298
+ const existing = Array.isArray(target[part.key]) ? target[part.key] : [];
299
+ target[part.key] = elements.map((element, i) => {
300
+ const slot = isRecord(existing[i]) ? existing[i] : {};
301
+ place(slot, parts, index + 1, element);
302
+ return slot;
303
+ });
304
+ return;
305
+ }
306
+ const next = isRecord(target[part.key]) ? target[part.key] : {};
307
+ target[part.key] = next;
308
+ place(next, parts, index + 1, value);
309
+ }
310
+ function isRecord(value) {
311
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
312
+ }
262
313
  function connectorPath(name) {
263
314
  return name.split('.').map((raw) => ({
264
315
  key: raw.endsWith('[*]') ? raw.slice(0, -3) : raw,
@@ -9,12 +9,14 @@
9
9
  * `.waitForEvent()` node carries the same facts under its own names, and
10
10
  * `bpmn/check.ts` adapts one to the other.
11
11
  *
12
- * What BPMN does NOT carry is `filters` — payload filtering is a Flow surface —
13
- * so the `EVENT_FILTER_*` family simply does not fire there. The rules that do
14
- * are the ones about the SUBSCRIPTION: whether it resolves at all, whether a
15
- * generic event named its object, and whether its scope names parameters the
16
- * operation actually declares. `skill-bpmn-outlook-waitfor-email` in
17
- * `flow-v2-run-8` failed on exactly that last question.
12
+ * BPMN carries `filters` too, since flow-v2-run-12: both families write payload
13
+ * conditions into the subscription the platform reads (Flow's `filterExpression`,
14
+ * BPMN's `body.filters.expression`), lowered by the same grammar, so the
15
+ * `EVENT_FILTER_*` family fires for both. The subscription rules are the rest:
16
+ * whether it resolves at all, whether a generic event named its object, and whether
17
+ * its scope names parameters the operation actually declares.
18
+ * `skill-bpmn-outlook-waitfor-email` in `flow-v2-run-8` failed on exactly that last
19
+ * question, and in run 12 on having nowhere to put a payload condition.
18
20
  */
19
21
  import { type LookupResolutions } from './lookups.js';
20
22
  import { type ByoaEventFacts, type ByoaLedger } from './byoa.js';
@@ -9,12 +9,14 @@
9
9
  * `.waitForEvent()` node carries the same facts under its own names, and
10
10
  * `bpmn/check.ts` adapts one to the other.
11
11
  *
12
- * What BPMN does NOT carry is `filters` — payload filtering is a Flow surface —
13
- * so the `EVENT_FILTER_*` family simply does not fire there. The rules that do
14
- * are the ones about the SUBSCRIPTION: whether it resolves at all, whether a
15
- * generic event named its object, and whether its scope names parameters the
16
- * operation actually declares. `skill-bpmn-outlook-waitfor-email` in
17
- * `flow-v2-run-8` failed on exactly that last question.
12
+ * BPMN carries `filters` too, since flow-v2-run-12: both families write payload
13
+ * conditions into the subscription the platform reads (Flow's `filterExpression`,
14
+ * BPMN's `body.filters.expression`), lowered by the same grammar, so the
15
+ * `EVENT_FILTER_*` family fires for both. The subscription rules are the rest:
16
+ * whether it resolves at all, whether a generic event named its object, and whether
17
+ * its scope names parameters the operation actually declares.
18
+ * `skill-bpmn-outlook-waitfor-email` in `flow-v2-run-8` failed on exactly that last
19
+ * question, and in run 12 on having nowhere to put a payload condition.
18
20
  */
19
21
  import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './lookups.js';
20
22
  import { prepareCommand } from './cli-spelling.js';