@uipath/maestro-builder-sdk 6.68.4 → 6.68.6

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.
@@ -437,9 +437,10 @@ export type ConnectorSpecData = Extract<ActionSpec, {
437
437
  * and `hybrid` as `Async…Execution`, and writes the value itself into the task's
438
438
  * `executionType` context.
439
439
  *
440
- * `hybrid` is refused for now (EXTERNAL_TASK_HYBRID_UNSUPPORTED): the Designer
441
- * also writes an event pair for it (`operation`, `eventMode`) that the connector
442
- * library does not carry (#1387). Add a task on a hybrid object in Studio Web.
440
+ * An `async` or `hybrid` object also answers through an event, which the
441
+ * Designer saves next to it; pass that pair as {@link ExternalTaskOptions.event}.
442
+ * A `hybrid` task without it is refused (EXTERNAL_TASK_EVENT), since the task
443
+ * would have nothing to wait on.
443
444
  *
444
445
  * It does not decide whether the stage waits for the task. A task the stage must
445
446
  * not wait on is `.required(false)`, whatever its execution type. For example, a
@@ -447,6 +448,27 @@ export type ConnectorSpecData = Extract<ActionSpec, {
447
448
  * it.
448
449
  */
449
450
  export type ExternalExecutionMode = 'sync' | 'async' | 'hybrid';
451
+ /**
452
+ * The event an `async` or `hybrid` external object answers with, as Integration
453
+ * Service reports it for the connector object.
454
+ *
455
+ * @remarks
456
+ * Like the execution type, a property of the connector object, not a choice:
457
+ * the Case Designer reads it from the object's metadata (its first event mode
458
+ * and that mode's first event type) and saves it as the task's `operation` and
459
+ * `eventMode` context rows, and as `eventType` / `eventMode` in the metadata's
460
+ * `activityPropertyConfiguration`. Copy it from the same two rows of the
461
+ * activity's `case spec` result (`caseShape.context`). `decompile` lifts it from
462
+ * a saved task. For
463
+ * example, the Power Automate "Invoke a Flow" object (`hybrid`) answers with
464
+ * `{ operation: 'TRIGGER_CREATED', mode: 'polling' }`.
465
+ */
466
+ export interface ExternalEventPair {
467
+ /** The event operation, e.g. `TRIGGER_CREATED`: the task's `operation` context row. */
468
+ operation: string;
469
+ /** How the event is received: the task's `eventMode` context row. */
470
+ mode: 'polling' | 'webhooks';
471
+ }
450
472
  /** Required wiring and typed inputs for an external agent/workflow invocation. */
451
473
  export interface ExternalTaskOptions<I extends Record<string, unknown>> {
452
474
  /** Symbolic Integration Service connection name declared in `bindings.json`. */
@@ -460,6 +482,12 @@ export interface ExternalTaskOptions<I extends Record<string, unknown>> {
460
482
  * `.required(false)`. See {@link ExternalExecutionMode}.
461
483
  */
462
484
  mode: ExternalExecutionMode;
485
+ /**
486
+ * The event an `async` or `hybrid` object answers with. Required for `hybrid`;
487
+ * optional for `async`; refused for `sync`, which answers inline. See
488
+ * {@link ExternalEventPair}.
489
+ */
490
+ event?: ExternalEventPair;
463
491
  /** Inputs statically checked by the generated connector descriptor. */
464
492
  inputs: I;
465
493
  }
@@ -467,6 +495,8 @@ export interface ExternalTaskOptions<I extends Record<string, unknown>> {
467
495
  export interface ExternalTaskSpecData {
468
496
  /** The connector object's `executionType`. */
469
497
  mode: ExternalExecutionMode;
498
+ /** The event an `async` or `hybrid` object answers with, when given. */
499
+ event?: ExternalEventPair;
470
500
  /** Shared connector action identity, inputs, and symbolic bindings. */
471
501
  connector: ConnectorSpecData;
472
502
  }
@@ -683,9 +713,10 @@ export interface CaseVarDecl {
683
713
  body?: unknown;
684
714
  /**
685
715
  * In-args only: the trigger this argument is bound to (its value arrives when
686
- * that trigger fires). Set via `.input(shape, { from })`. When present, the arg
687
- * emits the full three-entry binding (formal slot + companion + trigger-output
688
- * bridge); when absent it stays a bare declaration.
716
+ * that trigger fires). Set via `.input(shape, { from })`. Every In-arg emits the
717
+ * full three-entry binding (formal slot + companion + trigger-output bridge):
718
+ * when this is absent the arg binds to the primary trigger, and a case with
719
+ * none refuses it (CASE_IN_ARG_UNBOUND). See `.input()`.
689
720
  */
690
721
  sourceTrigger?: BuiltTrigger;
691
722
  }
@@ -1431,10 +1462,28 @@ declare class CaseBuilder {
1431
1462
  layout(layout: CaseLayout): this;
1432
1463
  /**
1433
1464
  * Declare case In-args. Each value is a {@link TypeDesc}, or `{ type, default }`
1434
- * to set a default. Pass `{ from: <trigger> }` to bind the args to a trigger —
1435
- * their value arrives when it fires, readable as `=vars.<name>` — and they are
1436
- * projected into that trigger's `entry-points.json` input schema. A declared
1437
- * In-arg is readable as `=vars.<name>` (its `inputOutputs` companion resolves it).
1465
+ * to set a default. An In-arg is always bound to a trigger: its value arrives
1466
+ * when that trigger starts the case, it is readable as `=vars.<name>` (its
1467
+ * `inputOutputs` companion, keyed to the trigger, resolves it), and it is
1468
+ * projected into that trigger's `entry-points.json` input schema.
1469
+ *
1470
+ * Which trigger:
1471
+ * - `{ from: <trigger> }` binds the args to that trigger.
1472
+ * - Without `{ from }`, they bind to the primary trigger. That is the case's
1473
+ * only trigger (the one added with `.trigger()`, or the implicit manual
1474
+ * trigger when none was added), or, when there are several, the one that is
1475
+ * not a timer. `uip maestro case sdd convert` reads a blank
1476
+ * `sourceTriggers` the same way.
1477
+ * - Without `{ from }` in a case with no primary trigger (several non-timer
1478
+ * triggers, or only timers), `check` and `compile` refuse the plan with
1479
+ * `CASE_IN_ARG_UNBOUND`. Pass `{ from }`, or use `.var()` for a value no
1480
+ * trigger supplies.
1481
+ *
1482
+ * @example
1483
+ * **Declare an In-arg on the primary trigger (here the implicit manual one)**
1484
+ * ```ts
1485
+ * casePlan('x').input({ invoiceNumber: { type: 'string', default: 'INV-1042' } })
1486
+ * ```
1438
1487
  *
1439
1488
  * @example
1440
1489
  * **Bind case In-args to a trigger's payload**
@@ -1445,7 +1494,8 @@ declare class CaseBuilder {
1445
1494
  * ```
1446
1495
  *
1447
1496
  * @param shape - In-arg names to types, or `{ type, default }`.
1448
- * @param opts - `{ from: <trigger> }` binds the args to a trigger's payload.
1497
+ * @param opts - `{ from: <trigger> }` binds the args to that trigger. Required
1498
+ * when the case has no primary trigger.
1449
1499
  * @returns This builder, so calls chain.
1450
1500
  */
1451
1501
  input(shape: Record<string, TypeDesc | JsonSchemaType | {
@@ -334,6 +334,7 @@ class TaskBuilder {
334
334
  this.setKind(kind);
335
335
  this._external = {
336
336
  mode: opts.mode,
337
+ ...(opts.event !== undefined ? { event: { ...opts.event } } : {}),
337
338
  connector: makeConnector(descriptor, opts.inputs, {
338
339
  connection: opts.connection,
339
340
  folder: opts.folder,
@@ -955,10 +956,28 @@ class CaseBuilder {
955
956
  }
956
957
  /**
957
958
  * Declare case In-args. Each value is a {@link TypeDesc}, or `{ type, default }`
958
- * to set a default. Pass `{ from: <trigger> }` to bind the args to a trigger —
959
- * their value arrives when it fires, readable as `=vars.<name>` — and they are
960
- * projected into that trigger's `entry-points.json` input schema. A declared
961
- * In-arg is readable as `=vars.<name>` (its `inputOutputs` companion resolves it).
959
+ * to set a default. An In-arg is always bound to a trigger: its value arrives
960
+ * when that trigger starts the case, it is readable as `=vars.<name>` (its
961
+ * `inputOutputs` companion, keyed to the trigger, resolves it), and it is
962
+ * projected into that trigger's `entry-points.json` input schema.
963
+ *
964
+ * Which trigger:
965
+ * - `{ from: <trigger> }` binds the args to that trigger.
966
+ * - Without `{ from }`, they bind to the primary trigger. That is the case's
967
+ * only trigger (the one added with `.trigger()`, or the implicit manual
968
+ * trigger when none was added), or, when there are several, the one that is
969
+ * not a timer. `uip maestro case sdd convert` reads a blank
970
+ * `sourceTriggers` the same way.
971
+ * - Without `{ from }` in a case with no primary trigger (several non-timer
972
+ * triggers, or only timers), `check` and `compile` refuse the plan with
973
+ * `CASE_IN_ARG_UNBOUND`. Pass `{ from }`, or use `.var()` for a value no
974
+ * trigger supplies.
975
+ *
976
+ * @example
977
+ * **Declare an In-arg on the primary trigger (here the implicit manual one)**
978
+ * ```ts
979
+ * casePlan('x').input({ invoiceNumber: { type: 'string', default: 'INV-1042' } })
980
+ * ```
962
981
  *
963
982
  * @example
964
983
  * **Bind case In-args to a trigger's payload**
@@ -969,7 +988,8 @@ class CaseBuilder {
969
988
  * ```
970
989
  *
971
990
  * @param shape - In-arg names to types, or `{ type, default }`.
972
- * @param opts - `{ from: <trigger> }` binds the args to a trigger's payload.
991
+ * @param opts - `{ from: <trigger> }` binds the args to that trigger. Required
992
+ * when the case has no primary trigger.
973
993
  * @returns This builder, so calls chain.
974
994
  */
975
995
  input(shape, opts = {}) {
@@ -1,4 +1,5 @@
1
1
  import { businessEventProblems } from './business-event-shape.js';
2
+ import { isUnboundInArg, unboundInArgMessage } from './in-arg-binding.js';
2
3
  import { ruleShapeProblems } from './rule-options.js';
3
4
  import { types } from '../core/expr.js';
4
5
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS, authoredCaseVersion, caseVersionMismatch } from './format-profile.js';
@@ -6,7 +7,7 @@ import { schemaVersionRefusal } from '../schema-version.js';
6
7
  import { caseJsonPreservation, caseJsonPreservationErrors, preservedBindings } from './preserve.js';
7
8
  import { EQUALS_EXPR_DIALECT, checkBindingLabels, checkConnectorContract, checkConnectorLookups, checkConnectorSchema, } from '../core/connector-checks.js';
8
9
  import { checkEventResolution } from '../core/event-checks.js';
9
- import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_HYBRID_UNSUPPORTED, externalHybridRefusal, isExternalExecutionMode } from './external.js';
10
+ import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_EVENT, externalEventRefusal, isExternalExecutionMode } from './external.js';
10
11
  import { isResourceLabelRefusal, resolveResourceRef } from './resource-identity.js';
11
12
  import { taskShapeProblems } from './task-shape.js';
12
13
  import { isConnectorPlaceholder } from './connector-placeholder.js';
@@ -279,6 +280,11 @@ export function check(built, opts = {}) {
279
280
  diags.push({ level: 'error', code: 'UNKNOWN_TASK_REF', message: `Rule "selected-tasks-completed" references unknown task "${n}".`, where });
280
281
  }
281
282
  };
283
+ // An In-arg with no `{ from }` binds to the primary trigger; when the case has
284
+ // none, `serialize` refuses it with the same code (in-arg-binding.ts).
285
+ for (const vd of built.vars)
286
+ if (isUnboundInArg(vd, built.triggers))
287
+ diags.push({ level: 'error', code: 'CASE_IN_ARG_UNBOUND', message: unboundInArgMessage(vd.name, built.triggers), where: vd.name });
282
288
  // ── `=js:vars.<name>` resolvability — REMOVED, superseded by #257 ───────────
283
289
  // This file briefly carried a VAR_NOT_REFERENCEABLE error asserting that only a
284
290
  // trigger-bound In-arg (`.input(shape, { from })`) could be read as `=js:vars.x`,
@@ -623,13 +629,11 @@ function checkStage(stage, diags, refStage, refPlacement, bindings) {
623
629
  message: `${family} task "${t.name}" mode must be the connector object's executionType: "sync", "async" or "hybrid".`,
624
630
  where: t.name,
625
631
  });
626
- else if (t.external.mode === 'hybrid')
627
- diags.push({
628
- level: 'error',
629
- code: EXTERNAL_TASK_HYBRID_UNSUPPORTED,
630
- message: externalHybridRefusal(`${family} task "${t.name}"`),
631
- where: t.name,
632
- });
632
+ else {
633
+ const refused = externalEventRefusal(`${family} task "${t.name}"`, t.external.mode, t.external.event);
634
+ if (refused)
635
+ diags.push({ level: 'error', code: EXTERNAL_TASK_EVENT, message: refused, where: t.name });
636
+ }
633
637
  if (!t.external.connector?.key?.trim() || !t.external.connector?.action?.trim())
634
638
  diags.push({
635
639
  level: 'error',
@@ -1,7 +1,8 @@
1
1
  import { commentText } from '../core/comment-text.js';
2
+ import { implicitInArgTrigger } from './in-arg-binding.js';
2
3
  import { tsLiteral, tsNumber } from '../core/ts-literal.js';
3
4
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
4
- import { EXTERNAL_TASK_CONTEXT, externalDescriptorRefusal, externalHybridRefusal, externalServiceType, isExternalExecutionMode } from './external.js';
5
+ import { EXTERNAL_TASK_CONTEXT, EXTERNAL_TASK_EVENT_CONTEXT, externalDescriptorRefusal, externalEventRefusal, externalServiceType, isExternalExecutionMode } from './external.js';
5
6
  import { preserveSchemaVersion } from '../schema-version.js';
6
7
  import { ownEntry } from '../core/own-entry.js';
7
8
  import { isUnrestorableSlotId, taskFingerprint } from './preserve.js';
@@ -618,7 +619,9 @@ function connectorCall(task, refs, library) {
618
619
  /**
619
620
  * Lift an External Agent / External Workflow task saved in the Case Designer's
620
621
  * shape (the one `serialize` emits): a connector activity's context and input
621
- * containers plus the object's `executionType`, which becomes `mode`. The
622
+ * containers plus the object's `executionType`, which becomes `mode`, and the
623
+ * event pair an async / hybrid object answers with (`operation` and `eventMode`
624
+ * rows), which becomes `event`. The
622
625
  * service type must be the one that execution type gives, so a plan whose two
623
626
  * disagree is refused rather than lifted to a source that would compile to
624
627
  * something else.
@@ -637,19 +640,20 @@ function externalCall(task, refs, library) {
637
640
  throw new Error(`${owner} data.bindings must be an empty array; bindings live at case root.`);
638
641
  if (!Array.isArray(data.context))
639
642
  throw new Error(`${owner} requires a resolved context array.`);
640
- // A hybrid task carries an event pair the SDK cannot write back (#1387): refuse
641
- // it by name, before its extra context rows are reported as unknown.
642
- if (data.context.some((row) => row?.name === 'executionType' && row.value === 'hybrid'))
643
- throw new Error(externalHybridRefusal(owner));
644
- // Exactly the context rows serialize writes (the Designer's set): an unknown
645
- // or repeated row would be dropped by the lift and lost on recompile.
643
+ // Exactly the context rows serialize writes (the Designer's set, plus the
644
+ // event pair an async / hybrid object answers with): an unknown or repeated
645
+ // row would be dropped by the lift and lost on recompile.
646
646
  const contextNames = data.context.map((row) => String(row?.name ?? ''));
647
- const unknownContext = contextNames.find((name, at) => !EXTERNAL_TASK_CONTEXT.includes(name) || contextNames.indexOf(name) !== at);
647
+ const unknownContext = contextNames.find((name, at) => !(EXTERNAL_TASK_CONTEXT.includes(name) || EXTERNAL_TASK_EVENT_CONTEXT.includes(name)) || contextNames.indexOf(name) !== at);
648
648
  if (unknownContext !== undefined)
649
649
  throw new Error(`${owner} has unsupported or repeated context row "${unknownContext}".`);
650
650
  const absentContext = EXTERNAL_TASK_CONTEXT.find((name) => !contextNames.includes(name));
651
651
  if (absentContext)
652
652
  throw new Error(`${owner} is missing context row "${absentContext}".`);
653
+ // The pair is written whole or not at all, so half of one cannot be lifted.
654
+ const eventRows = EXTERNAL_TASK_EVENT_CONTEXT.filter((name) => contextNames.includes(name));
655
+ if (eventRows.length === 1)
656
+ throw new Error(`${owner} carries context row "${eventRows[0]}" without "${EXTERNAL_TASK_EVENT_CONTEXT.find((name) => name !== eventRows[0])}"; the event pair is saved whole.`);
653
657
  const context = contextMap(data);
654
658
  const stringValue = (name) => {
655
659
  const row = context.get(name);
@@ -672,6 +676,18 @@ function externalCall(task, refs, library) {
672
676
  const serviceType = externalServiceType(kind, mode);
673
677
  if (data.serviceType !== serviceType)
674
678
  throw new Error(`${owner} has serviceType "${String(data.serviceType ?? '')}", but its executionType "${mode}" saves as "${serviceType}".`);
679
+ // The event the object answers with, as saved: the same rule compile applies,
680
+ // so a lifted task always compiles back (a hybrid task without one does not).
681
+ const event = eventRows.length ? { operation: stringValue('operation'), mode: stringValue('eventMode') } : undefined;
682
+ // A saved plan has no `event` option to drop, so a sync task with event rows
683
+ // gets its own text naming the rows; every other case is compile's rule.
684
+ if (mode === 'sync' && event)
685
+ throw new Error(`${owner} has context rows "operation" (${JSON.stringify(event.operation)}) and "eventMode" ` +
686
+ `(${JSON.stringify(event.mode)}) on executionType "sync"; a sync object answers inline and has no event. ` +
687
+ `Remove the two rows, or fix executionType and serviceType if the object is async or hybrid.`);
688
+ const eventRefused = externalEventRefusal(owner, mode, event);
689
+ if (eventRefused)
690
+ throw new Error(eventRefused);
675
691
  const identity = library.identifyAction({
676
692
  connectorKey: stringValue('connectorKey'),
677
693
  httpMethod: stringValue('method'),
@@ -691,6 +707,7 @@ function externalCall(task, refs, library) {
691
707
  ['connection', tsLiteral(binding('connection'))],
692
708
  ['folder', tsLiteral(binding('folderKey'))],
693
709
  ['mode', tsLiteral(mode)],
710
+ ['event', event ? opts([['operation', tsLiteral(event.operation)], ['mode', tsLiteral(event.mode)]]) : undefined],
694
711
  ['inputs', tsLiteral(inputs)],
695
712
  ]);
696
713
  const method = kind === 'external-agent' ? 'externalAgent' : 'externalWorkflow';
@@ -1072,13 +1089,25 @@ function reassignedOwnNameTargets(plan) {
1072
1089
  }
1073
1090
  return targets;
1074
1091
  }
1075
- function emitVariables(out, depth, plan, triggerNameById) {
1092
+ function emitVariables(out, depth, plan, triggerNameById, triggerIsTimer) {
1076
1093
  const variables = plan.variables ?? {};
1077
1094
  const produced = producedVariableNames(plan);
1078
1095
  for (const input of variables.inputs ?? []) {
1079
- const from = input.elementId && triggerNameById.get(input.elementId);
1096
+ let from = input.elementId && triggerNameById.get(input.elementId);
1097
+ // An In-arg with no trigger is a shape only older SDKs wrote (builder-sdk
1098
+ // #1368): no companion, so nothing binds it. A bare `.input()` now binds to
1099
+ // the primary trigger, which is what this source recompiles to. When the
1100
+ // case has no primary trigger a bare `.input()` is refused
1101
+ // (CASE_IN_ARG_UNBOUND), so the source names the first trigger and says so.
1102
+ if (!input.elementId && implicitInArgTrigger(triggerIsTimer) === undefined) {
1103
+ from = triggerNameById.values().next().value;
1104
+ out.line(depth, `// The source document left In-arg "${commentText(input.name)}" unbound and the case has no primary trigger; it is bound to the first trigger here (builder-sdk#1368).`);
1105
+ }
1080
1106
  const fromOpts = from ? `, { from: ${from} }` : '';
1081
- out.line(depth, `.input({ ${key(input.name)}: ${varSpec(input)} }${fromOpts})`);
1107
+ // Compile writes `default: ""` on every In-arg that authored none, so the
1108
+ // empty default is not authored content and the source leaves it out.
1109
+ const spec = varSpec(input.default === '' ? { ...input, default: undefined } : input);
1110
+ out.line(depth, `.input({ ${key(input.name)}: ${spec} }${fromOpts})`);
1082
1111
  }
1083
1112
  for (const output of variables.outputs ?? [])
1084
1113
  out.line(depth, `.output({ ${key(output.name)}: ${varSpec(output)} })`);
@@ -1114,6 +1143,10 @@ function bridgeRows(plan, triggerId, bag) {
1114
1143
  }
1115
1144
  return bridges;
1116
1145
  }
1146
+ function isTimerTrigger(node) {
1147
+ const serviceType = triggerData(node).bag?.serviceType;
1148
+ return serviceType === 'Intsvc.TimerTrigger' || serviceType === 'timer';
1149
+ }
1117
1150
  function triggerSource(node, index, plan, refs) {
1118
1151
  const { label, description, bag } = triggerData(node);
1119
1152
  const name = label !== `Trigger ${index + 1}` ? tsLiteral(label) : undefined;
@@ -1561,7 +1594,7 @@ export function decompileWithDiagnostics(plan, options = {}) {
1561
1594
  throw new Error('allowAdhocOptionalStageTasks must be boolean.');
1562
1595
  out.line(depth, metadata.allowAdhocOptionalStageTasks ? '.allowAdhocTasks()' : '.allowAdhocTasks(false)');
1563
1596
  }
1564
- emitVariables(out, depth, plan, triggerNameById);
1597
+ emitVariables(out, depth, plan, triggerNameById, triggers.map(isTimerTrigger));
1565
1598
  triggers.forEach((_trigger, index) => out.line(depth, `.trigger(trigger${index + 1})`));
1566
1599
  (metadata.slaRules ?? []).forEach((sla, index) => out.line(depth, `.sla(${slaSource(sla, index)})`));
1567
1600
  for (const event of metadata.businessEvents ?? [])
@@ -8,32 +8,43 @@
8
8
  * `buildConnectorInputs`, which `check` already runs as `connectorInputs`.
9
9
  */
10
10
  import type { ResolvedConnector } from '../core/library.js';
11
- import type { ExternalExecutionMode } from './case-sdk.js';
11
+ import type { ExternalEventPair, ExternalExecutionMode } from './case-sdk.js';
12
12
  /**
13
13
  * The context rows `serialize` writes on an external task: the set the Case
14
14
  * Designer saves for a sync object (`uip`'s
15
15
  * `tests/fixtures/external-tasks/designer-saved.json`, the Snowflake Cortex
16
- * agent). `decompile` refuses a task with any other row, since the lift would
17
- * drop it without a word. A `hybrid` task (which also carries `operation` and
18
- * `eventMode`) is refused before this check; see {@link externalHybridRefusal}.
16
+ * agent). An `async` or `hybrid` task may also carry
17
+ * {@link EXTERNAL_TASK_EVENT_CONTEXT}. `decompile` refuses a task with any other
18
+ * row, since the lift would drop it without a word.
19
19
  */
20
20
  export declare const EXTERNAL_TASK_CONTEXT: readonly string[];
21
- /** Check's code for {@link externalHybridRefusal}. */
22
- export declare const EXTERNAL_TASK_HYBRID_UNSUPPORTED = "EXTERNAL_TASK_HYBRID_UNSUPPORTED";
23
21
  /**
24
- * Why an external task on a `hybrid` object cannot be compiled or decompiled
25
- * yet. The Case Designer saves such a task with an event pair (context
26
- * `operation`, e.g. `TRIGGER_CREATED`, and `eventMode`, e.g. `polling`, also in
27
- * the metadata's `activityPropertyConfiguration`) that it reads from live
28
- * Integration Service metadata. The connector library does not carry that pair
29
- * (#1387), so the SDK cannot write the task faithfully, and a guessed pair could
30
- * deploy a task that never completes. `check`, `serialize` and `decompile` all
31
- * refuse with this text.
22
+ * The two context rows that carry the event an `async` / `hybrid` object
23
+ * answers with (`ExternalEventPair`): `operation` (e.g. `TRIGGER_CREATED`) and
24
+ * `eventMode` (e.g. `polling`). The Designer writes them after `objectName`,
25
+ * before `executionType` (the Power Automate flow in `designer-saved.json`).
26
+ */
27
+ export declare const EXTERNAL_TASK_EVENT_CONTEXT: readonly string[];
28
+ /** Check's and compile's code for {@link externalEventRefusal}. */
29
+ export declare const EXTERNAL_TASK_EVENT = "EXTERNAL_TASK_EVENT";
30
+ /** The event modes Integration Service reports for an object's events. */
31
+ export declare function isExternalEventMode(value: unknown): value is ExternalEventPair['mode'];
32
+ /**
33
+ * Why this task's event pair cannot be written, or `undefined`.
34
+ *
35
+ * A `hybrid` object answers through an event, and the Case Designer saves that
36
+ * event with the task (context `operation` and `eventMode`, and the same pair in
37
+ * the metadata's `activityPropertyConfiguration`). Without it the task has
38
+ * nothing to wait on, so `hybrid` needs the pair. An `async` object takes it
39
+ * when given. A `sync` object answers inline and has none, so a pair there is a
40
+ * wrong `mode` or a wrong pair. `check`, `serialize` and `decompile` all use this.
32
41
  *
33
42
  * @param owner - The task as the message names it, e.g. `External Workflow task "Run"`.
34
- * @returns The refusal.
43
+ * @param mode - The task's execution type (already known to be valid).
44
+ * @param event - The authored or saved pair, if any.
45
+ * @returns The refusal, or `undefined`.
35
46
  */
36
- export declare function externalHybridRefusal(owner: string): string;
47
+ export declare function externalEventRefusal(owner: string, mode: ExternalExecutionMode, event: unknown): string | undefined;
37
48
  /** The execution types an external task accepts: the values Integration Service reports. */
38
49
  export declare function isExternalExecutionMode(value: unknown): value is ExternalExecutionMode;
39
50
  /**
@@ -3,34 +3,61 @@ import { CASE_FORMAT_PROFILE } from './format-profile.js';
3
3
  * The context rows `serialize` writes on an external task: the set the Case
4
4
  * Designer saves for a sync object (`uip`'s
5
5
  * `tests/fixtures/external-tasks/designer-saved.json`, the Snowflake Cortex
6
- * agent). `decompile` refuses a task with any other row, since the lift would
7
- * drop it without a word. A `hybrid` task (which also carries `operation` and
8
- * `eventMode`) is refused before this check; see {@link externalHybridRefusal}.
6
+ * agent). An `async` or `hybrid` task may also carry
7
+ * {@link EXTERNAL_TASK_EVENT_CONTEXT}. `decompile` refuses a task with any other
8
+ * row, since the lift would drop it without a word.
9
9
  */
10
10
  export const EXTERNAL_TASK_CONTEXT = [
11
11
  'connectorKey', 'connection', 'resourceKey', 'folderKey', 'objectName', 'method', 'path',
12
12
  'executionType', 'metadata', 'activityConfigurationVersion',
13
13
  ];
14
- /** Check's code for {@link externalHybridRefusal}. */
15
- export const EXTERNAL_TASK_HYBRID_UNSUPPORTED = 'EXTERNAL_TASK_HYBRID_UNSUPPORTED';
16
14
  /**
17
- * Why an external task on a `hybrid` object cannot be compiled or decompiled
18
- * yet. The Case Designer saves such a task with an event pair (context
19
- * `operation`, e.g. `TRIGGER_CREATED`, and `eventMode`, e.g. `polling`, also in
20
- * the metadata's `activityPropertyConfiguration`) that it reads from live
21
- * Integration Service metadata. The connector library does not carry that pair
22
- * (#1387), so the SDK cannot write the task faithfully, and a guessed pair could
23
- * deploy a task that never completes. `check`, `serialize` and `decompile` all
24
- * refuse with this text.
15
+ * The two context rows that carry the event an `async` / `hybrid` object
16
+ * answers with (`ExternalEventPair`): `operation` (e.g. `TRIGGER_CREATED`) and
17
+ * `eventMode` (e.g. `polling`). The Designer writes them after `objectName`,
18
+ * before `executionType` (the Power Automate flow in `designer-saved.json`).
19
+ */
20
+ export const EXTERNAL_TASK_EVENT_CONTEXT = ['operation', 'eventMode'];
21
+ /** Check's and compile's code for {@link externalEventRefusal}. */
22
+ export const EXTERNAL_TASK_EVENT = 'EXTERNAL_TASK_EVENT';
23
+ /** The event modes Integration Service reports for an object's events. */
24
+ export function isExternalEventMode(value) {
25
+ return value === 'polling' || value === 'webhooks';
26
+ }
27
+ /**
28
+ * Why this task's event pair cannot be written, or `undefined`.
29
+ *
30
+ * A `hybrid` object answers through an event, and the Case Designer saves that
31
+ * event with the task (context `operation` and `eventMode`, and the same pair in
32
+ * the metadata's `activityPropertyConfiguration`). Without it the task has
33
+ * nothing to wait on, so `hybrid` needs the pair. An `async` object takes it
34
+ * when given. A `sync` object answers inline and has none, so a pair there is a
35
+ * wrong `mode` or a wrong pair. `check`, `serialize` and `decompile` all use this.
25
36
  *
26
37
  * @param owner - The task as the message names it, e.g. `External Workflow task "Run"`.
27
- * @returns The refusal.
38
+ * @param mode - The task's execution type (already known to be valid).
39
+ * @param event - The authored or saved pair, if any.
40
+ * @returns The refusal, or `undefined`.
28
41
  */
29
- export function externalHybridRefusal(owner) {
30
- return (`${owner} is on a "hybrid" object, which Case cannot compile yet: the connector library does not carry ` +
31
- `the event pair the Case Designer writes for it (operation, e.g. "TRIGGER_CREATED", and eventMode, e.g. "polling"), ` +
32
- `so the task cannot be written faithfully. Add this task in Studio Web for now; ` +
33
- `https://github.com/UiPath/builder-sdk/issues/1387 tracks the library change.`);
42
+ export function externalEventRefusal(owner, mode, event) {
43
+ if (event === undefined) {
44
+ if (mode !== 'hybrid')
45
+ return undefined;
46
+ return (`${owner} is on a "hybrid" object, which answers through an event: pass the event the object reports as ` +
47
+ `event: { operation, mode } (for example { operation: "TRIGGER_CREATED", mode: "polling" }, ` +
48
+ `the "operation" and "eventMode" rows of the case spec result's caseShape.context). Without it the task has nothing to wait on.`);
49
+ }
50
+ if (mode === 'sync')
51
+ return `${owner} is on a "sync" object, which answers inline and has no event; drop event, or fix mode.`;
52
+ const pair = event;
53
+ if (typeof pair !== 'object' || pair === null || typeof pair.operation !== 'string' || !pair.operation.trim())
54
+ return `${owner} event.operation must be the object's non-empty event operation, e.g. "TRIGGER_CREATED".`;
55
+ // Written as given, and Integration Service matches it exactly: " TRIGGER_CREATED" would never match.
56
+ if (pair.operation !== pair.operation.trim())
57
+ return `${owner} event.operation ${JSON.stringify(pair.operation)} has surrounding whitespace; write it exactly as the object reports it, e.g. "TRIGGER_CREATED".`;
58
+ if (!isExternalEventMode(pair.mode))
59
+ return `${owner} event.mode must be "polling" or "webhooks"; got ${JSON.stringify(pair.mode)}.`;
60
+ return undefined;
34
61
  }
35
62
  /** The execution types an external task accepts: the values Integration Service reports. */
36
63
  export function isExternalExecutionMode(value) {
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Which trigger a case In-arg (`.input()`) binds to — the rule `check` reports,
3
+ * `serialize` applies and `decompile` follows, kept in one place so they cannot
4
+ * drift (builder-sdk#1368).
5
+ *
6
+ * An In-arg's value arrives from a trigger: the caseplan carries a formal slot
7
+ * in `variables.inputs[]`, a same-name `variables.inputOutputs[]` companion
8
+ * keyed to the trigger node, and a bridge on that node. That is the only shape
9
+ * the Case Designer writes, and `uip maestro case validate --strict` refuses an
10
+ * In-arg with no companion (STRICT_INPUTOUTPUTS_COMPANION). So an In-arg always
11
+ * binds:
12
+ *
13
+ * - `.input(shape, { from: t })` binds to `t`;
14
+ * - with no `{ from }`, it binds to the PRIMARY trigger: the case's only
15
+ * trigger (the one `.trigger()` added, or the implicit manual trigger when
16
+ * none was added, whatever its kind); with several, the one trigger that is
17
+ * not a timer — the one a caller starts the case through;
18
+ * - with no `{ from }` and no single such trigger (several non-timer
19
+ * triggers, or only timers) there is no right guess, and the case is
20
+ * refused with CASE_IN_ARG_UNBOUND.
21
+ *
22
+ * This is the rule `uip maestro case sdd convert` applies to an `In` row with a
23
+ * blank `sourceTriggers` cell (cli `sdd-convert/variables.ts`,
24
+ * `boundTriggerFor`), and the one its `in_arg_trigger_bind` grader checks.
25
+ */
26
+ import type { BuiltTrigger, CaseVarDecl } from './case-sdk.js';
27
+ /**
28
+ * The index, among the triggers the case emits, of the one an In-arg without
29
+ * `{ from }` binds to.
30
+ *
31
+ * @param isTimer - For each emitted trigger, in order, whether it is a timer.
32
+ * An empty list stands for the implicit manual trigger.
33
+ * @returns The trigger's index, or `undefined` when no single trigger qualifies.
34
+ */
35
+ export declare function implicitInArgTrigger(isTimer: readonly boolean[]): number | undefined;
36
+ /**
37
+ * {@link implicitInArgTrigger} over a built case's declared triggers.
38
+ *
39
+ * @param triggers - The case's `.trigger()` list.
40
+ * @returns The index of the trigger a `{ from }`-less In-arg binds to, or `undefined`.
41
+ */
42
+ export declare function implicitInArgBuiltTrigger(triggers: readonly BuiltTrigger[]): number | undefined;
43
+ /**
44
+ * Whether an In-arg declared without `{ from }` has no trigger to bind to.
45
+ *
46
+ * @param vd - The declared variable.
47
+ * @param triggers - The case's `.trigger()` list.
48
+ * @returns `true` when the In-arg must name its trigger with `{ from }`.
49
+ */
50
+ export declare function isUnboundInArg(vd: CaseVarDecl, triggers: readonly BuiltTrigger[]): boolean;
51
+ /**
52
+ * The CASE_IN_ARG_UNBOUND message.
53
+ *
54
+ * @param name - The In-arg's name.
55
+ * @param triggers - The case's `.trigger()` list.
56
+ * @returns The message both `check` and `serialize` report.
57
+ */
58
+ export declare function unboundInArgMessage(name: string, triggers: readonly BuiltTrigger[]): string;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The index, among the triggers the case emits, of the one an In-arg without
3
+ * `{ from }` binds to.
4
+ *
5
+ * @param isTimer - For each emitted trigger, in order, whether it is a timer.
6
+ * An empty list stands for the implicit manual trigger.
7
+ * @returns The trigger's index, or `undefined` when no single trigger qualifies.
8
+ */
9
+ export function implicitInArgTrigger(isTimer) {
10
+ if (isTimer.length <= 1)
11
+ return 0;
12
+ const callable = isTimer.flatMap((timer, i) => (timer ? [] : [i]));
13
+ return callable.length === 1 ? callable[0] : undefined;
14
+ }
15
+ /**
16
+ * {@link implicitInArgTrigger} over a built case's declared triggers.
17
+ *
18
+ * @param triggers - The case's `.trigger()` list.
19
+ * @returns The index of the trigger a `{ from }`-less In-arg binds to, or `undefined`.
20
+ */
21
+ export function implicitInArgBuiltTrigger(triggers) {
22
+ return implicitInArgTrigger(triggers.map((t) => t.kind === 'timer'));
23
+ }
24
+ /**
25
+ * Whether an In-arg declared without `{ from }` has no trigger to bind to.
26
+ *
27
+ * @param vd - The declared variable.
28
+ * @param triggers - The case's `.trigger()` list.
29
+ * @returns `true` when the In-arg must name its trigger with `{ from }`.
30
+ */
31
+ export function isUnboundInArg(vd, triggers) {
32
+ return vd.direction === 'in' && !vd.sourceTrigger && implicitInArgBuiltTrigger(triggers) === undefined;
33
+ }
34
+ /**
35
+ * The CASE_IN_ARG_UNBOUND message.
36
+ *
37
+ * @param name - The In-arg's name.
38
+ * @param triggers - The case's `.trigger()` list.
39
+ * @returns The message both `check` and `serialize` report.
40
+ */
41
+ export function unboundInArgMessage(name, triggers) {
42
+ const callable = triggers.filter((t) => t.kind !== 'timer').length;
43
+ const why = callable === 0
44
+ ? `all ${triggers.length} of the case's triggers are timers`
45
+ : `${callable} of the case's ${triggers.length} triggers are not timers`;
46
+ return `In-arg "${name}" has no { from }, and ${why}, so there is no primary trigger to bind it to. `
47
+ + `Pass .input({ ${name}: ... }, { from: <trigger> }) to name the trigger whose start supplies it, or declare it with .var() if no trigger supplies it.`;
48
+ }