@uipath/maestro-builder-sdk 6.15.3 → 6.15.5

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.
package/dist/check.js CHANGED
@@ -12,11 +12,11 @@ import { checkEventResolution, checkEventSubscription, eventParamsFor, } from '.
12
12
  import { FLOW_EXPR_DIALECT, checkBindingLabels, checkBindingPresence, checkConnectorContract as coreCheckConnectorContract, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, collectConnectionUses, connectionConnectorConflicts, } from './core/connector-checks.js';
13
13
  import { prepareCommand } from './core/cli-spelling.js';
14
14
  import { isAnswerField } from './core/hitl-answers.js';
15
- import { globalNameCollisionMessage, globalNameCollisions } from './core/global-names.js';
16
15
  import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
17
16
  import { childStepLists, declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
18
17
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
19
18
  import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
19
+ import { planFlowGlobals } from './flow-variables.js';
20
20
  export function check(built, opts = {}) {
21
21
  let diags = [];
22
22
  const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts); });
@@ -36,12 +36,8 @@ function checkFlow(built, ancestors, opts = {}) {
36
36
  if (!built.id)
37
37
  diags.push({ level: 'error', code: 'NO_ID', message: 'flow() requires an id.' });
38
38
  checkSchemaVersion(built, ancestors, diags);
39
+ checkGlobalNames(built, diags);
39
40
  checkNodeUpdates(built, diags);
40
- // Inputs, outputs and vars are one namespace in the .flow (variables.globals[]);
41
- // `serialize` throws the same sentence (flow-builder-sdk#890).
42
- for (const collision of globalNameCollisions(built.inputs ?? [], built.outputs ?? [], built.vars ?? [])) {
43
- diags.push({ level: 'error', code: 'VARIABLE_NAME_COLLISION', message: globalNameCollisionMessage(collision) });
44
- }
45
41
  const maybeCustomTrigger = built.trigger;
46
42
  if (maybeCustomTrigger !== undefined && !('kind' in maybeCustomTrigger) && maybeCustomTrigger instanceof FlowNode) {
47
43
  // A custom FlowTrigger subclass reports through its own diagnostics() hook;
@@ -945,7 +941,12 @@ function checkFlow(built, ancestors, opts = {}) {
945
941
  }
946
942
  }
947
943
  // Check $vars.<name>[.<field>] references resolve to a declared name or step.
944
+ const inputNames = new Set(built.inputs.map((x) => x.name));
948
945
  for (const { js, step, errorSource } of exprs) {
946
+ // Comments and string-literal text blanked out, offsets preserved: the
947
+ // input-read rule below fires on a VALID name, so it has to tell a read
948
+ // from a mention, the same way `uip maestro flow validate` does.
949
+ const code = codeOnly(js);
949
950
  // `rest` is the remaining dotted tail after `<name>.<field>` — needed for a
950
951
  // subflow read, where the value lives one level deeper
951
952
  // (`$vars.<call>.output.<childOutput>`). Every other rule reads only the
@@ -963,6 +964,36 @@ function checkFlow(built, ancestors, opts = {}) {
963
964
  });
964
965
  continue;
965
966
  }
967
+ // `$vars.<input>` — a flow INPUT read by its bare name.
968
+ //
969
+ // Inputs, outputs and vars share one namespace in `variables.globals[]`,
970
+ // but they are not read the same way. A var or output is read as
971
+ // `$vars.<name>`. An input is not: serialize gives EVERY input global a
972
+ // `triggerNodeId` (the root trigger, an entry point, or a subflow child's
973
+ // own Start), and the runtime surfaces such an input only under that
974
+ // trigger's output — `$vars.<trigger>.output.<name>`, which is exactly what
975
+ // `input('<name>')` lowers to. The bare read resolves only by accident (a
976
+ // manual trigger also emits a process-level input of the same name) and is
977
+ // null under any other trigger. `uip maestro flow validate` refuses it
978
+ // (UV-16316, UiPath/cli#4236); this rule used to treat every declared name
979
+ // as readable bare, so check and compile passed what validate refused
980
+ // (five shipped examples, found with flow-builder-sdk#904).
981
+ //
982
+ // A step with the same name wins, as it does in validate, and so does a
983
+ // mention in a comment or a string literal (template `${…}` is code).
984
+ if (inputNames.has(name) && !stepNames.has(name) && code.startsWith('$vars', m.index ?? 0)) {
985
+ diags.push({
986
+ level: 'error',
987
+ code: 'INPUT_READ_WITHOUT_TRIGGER',
988
+ message: `"$vars.${name}" in "${step}" reads flow input "${name}" by its bare name. A flow input is `
989
+ + `published under the trigger's output, so the runtime value is "$vars.${triggerId}.output.${name}" `
990
+ + `— the bare read is null under any trigger but a manual one, and \`uip maestro flow validate\` `
991
+ + `refuses it. In an expression, write input('${name}'); \`v()\` is for a .var() or an output.`,
992
+ step,
993
+ suggestion: `$vars.${triggerId}.output.${name}`,
994
+ });
995
+ continue;
996
+ }
966
997
  // A BARE `$vars.<step>` — the step itself, not its output.
967
998
  //
968
999
  // **The mechanism is now MEASURED, not inferred.** This comment used to say
@@ -1612,6 +1643,20 @@ const INTERVAL_WORDS = [
1612
1643
  * Without a library (`uip maestro flow check` run with no `--library`) the fields
1613
1644
  * are unknown, so the warning keeps the generic wording.
1614
1645
  */
1646
+ /**
1647
+ * Every declared name must become exactly one global, because `flow validate`
1648
+ * rejects a repeated id (`[variables.globals] Duplicate variable ID`;
1649
+ * VARIABLE_NAME_COLLISION, flow-builder-sdk#890). The planning — including the one collision that is NOT an error, a same-named
1650
+ * `.output()` + `.var()`, which merges into one `out` global carrying the var's
1651
+ * default — lives in `planFlowGlobals`, shared with the serializer, so `check`
1652
+ * and the emitted file cannot disagree about it (flow-builder-sdk#899: `check`
1653
+ * said `Errors: 0`, validate refused the file).
1654
+ */
1655
+ function checkGlobalNames(built, diags) {
1656
+ for (const p of planFlowGlobals({ inputs: built.inputs ?? [], outputs: built.outputs ?? [], vars: built.vars ?? [] }).problems) {
1657
+ diags.push({ level: 'error', code: p.code, message: p.message, ...(p.suggestion ? { suggestion: p.suggestion } : {}) });
1658
+ }
1659
+ }
1615
1660
  /**
1616
1661
  * `{ updates }` writes a declared flow variable when the node completes. The
1617
1662
  * assignable targets are `.var()` variables and flow outputs — assigning to an
@@ -1761,6 +1806,97 @@ function declaredRefName(js, triggerId) {
1761
1806
  const viaTrigger = new RegExp(`^\\$vars\\.${triggerId}\\.output\\.([A-Za-z0-9_]+)$`).exec(js.trim());
1762
1807
  return viaTrigger?.[1];
1763
1808
  }
1809
+ /**
1810
+ * The JavaScript text with `//` and `/* *\/` comments and the text of string and
1811
+ * template literals replaced by spaces — same length, so a match offset in the
1812
+ * original indexes the same character here. A template's `${…}` bodies are code
1813
+ * and are kept (nested templates and strings inside them are blanked in turn).
1814
+ *
1815
+ * This is the read-versus-mention line `uip maestro flow validate` draws in its
1816
+ * `$vars` extraction (`collectVarsRefs`, `inProse`): a rule that fires on a VALID
1817
+ * name must not fire on `"pass $vars.x here"` or a commented-out line. Regex
1818
+ * literals are not recognised; a `$vars` reference inside one is vanishingly rare.
1819
+ */
1820
+ function codeOnly(src) {
1821
+ const out = src.split('');
1822
+ const n = src.length;
1823
+ let i = 0;
1824
+ const blank = (from, to) => {
1825
+ for (let k = from; k < to && k < n; k++)
1826
+ if (out[k] !== '\n')
1827
+ out[k] = ' ';
1828
+ };
1829
+ /** Scan code from `i`; with `stopAtBrace`, return at the `}` closing a `${`. */
1830
+ const code = (stopAtBrace) => {
1831
+ let depth = 0;
1832
+ while (i < n) {
1833
+ const c = src[i];
1834
+ if (c === '/' && src[i + 1] === '/') {
1835
+ const end = src.indexOf('\n', i);
1836
+ const stop = end === -1 ? n : end;
1837
+ blank(i, stop);
1838
+ i = stop;
1839
+ continue;
1840
+ }
1841
+ if (c === '/' && src[i + 1] === '*') {
1842
+ const end = src.indexOf('*/', i + 2);
1843
+ const stop = end === -1 ? n : end + 2;
1844
+ blank(i, stop);
1845
+ i = stop;
1846
+ continue;
1847
+ }
1848
+ if (c === '"' || c === "'") {
1849
+ const start = i++;
1850
+ while (i < n && src[i] !== c && src[i] !== '\n')
1851
+ i += src[i] === '\\' ? 2 : 1;
1852
+ blank(start + 1, i);
1853
+ i++;
1854
+ continue;
1855
+ }
1856
+ if (c === '`') {
1857
+ template();
1858
+ continue;
1859
+ }
1860
+ if (stopAtBrace) {
1861
+ if (c === '{')
1862
+ depth++;
1863
+ else if (c === '}') {
1864
+ if (depth === 0)
1865
+ return;
1866
+ depth--;
1867
+ }
1868
+ }
1869
+ i++;
1870
+ }
1871
+ };
1872
+ const template = () => {
1873
+ let start = ++i;
1874
+ while (i < n) {
1875
+ const c = src[i];
1876
+ if (c === '\\') {
1877
+ i += 2;
1878
+ continue;
1879
+ }
1880
+ if (c === '`') {
1881
+ blank(start, i);
1882
+ i++;
1883
+ return;
1884
+ }
1885
+ if (c === '$' && src[i + 1] === '{') {
1886
+ blank(start, i);
1887
+ i += 2;
1888
+ code(true);
1889
+ i++; // the `}` closing this `${`
1890
+ start = i;
1891
+ continue;
1892
+ }
1893
+ i++;
1894
+ }
1895
+ blank(start, n);
1896
+ };
1897
+ code(false);
1898
+ return out.join('');
1899
+ }
1764
1900
  const GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1765
1901
  /**
1766
1902
  * The four published-resource factories once each named the release key
@@ -3940,19 +4076,46 @@ function checkHttp(step, inputs, diags) {
3940
4076
  // Moving to the managed node is not a rename. A plain step keeps a non-2xx on
3941
4077
  // the SUCCESS path (`out(step,'statusCode')`); the managed one fails on it, so
3942
4078
  // a status branch becomes an error handler reading the envelope's `status`.
3943
- // That rewrite is the author's call, so the warning states the trade rather
3944
- // than pretending it is mechanical.
4079
+ //
4080
+ // It is not a free upgrade either, and for a connection-less URL it is usually
4081
+ // the wrong move (flow-builder-sdk#898). With no `connection` the managed node
4082
+ // still goes through Integration Service: serialize emits the `uipath-uipath-http`
4083
+ // connector on `ImplicitConnection`, so the request leaves from IS's SHARED
4084
+ // egress. Measured on slack-weather-pipeline (3/3 runs): agents took the old
4085
+ // `managed: true` hint, and Open-Meteo answered that egress 429 "Daily API
4086
+ // request limit exceeded" while direct calls from the same box got 200. The
4087
+ // plain node goes direct. So the hint only steers toward `managed: true` when
4088
+ // the step was given connection settings; the two other real reasons to move
4089
+ // (a binding, an `.onError()`) already fire as errors with that suggestion
4090
+ // (HTTP_BINDING_REQUIRES_MANAGED, HTTP_ONERROR_V1).
3945
4091
  if (inputs && !inputs.managed) {
3946
- diags.push({
3947
- level: 'warning',
3948
- code: 'HTTP_V1_RETIRED',
3949
- message: `Step "${step}" emits core.action.http, which the tenant registry no longer serves (only `
3950
- + `core.action.http.v2). It still runs today. Moving to managed: true changes behaviour rather than just `
3951
- + `the node type: a non-2xx stops arriving on the success path with statusCode and fails the step instead, `
3952
- + `so a status branch becomes an .onError() handler reading err('${step}', 'status').`,
3953
- step,
3954
- suggestion: `http({ url: …, managed: true })`,
3955
- });
4092
+ const asked = inputs;
4093
+ const wantsConnection = asked.connection !== undefined || asked.folder !== undefined
4094
+ || asked.targetConnector !== undefined;
4095
+ diags.push(wantsConnection
4096
+ ? {
4097
+ level: 'warning',
4098
+ code: 'HTTP_V1_RETIRED',
4099
+ message: `Step "${step}" emits core.action.http, which the tenant registry no longer serves (only `
4100
+ + `core.action.http.v2). It sets Integration Service connection settings, which only the managed node `
4101
+ + `can use. Moving to managed: true changes behaviour rather than just the node type: a non-2xx stops `
4102
+ + `arriving on the success path with statusCode and fails the step instead, so a status branch becomes `
4103
+ + `an .onError() handler reading err('${step}', 'status').`,
4104
+ step,
4105
+ suggestion: `http({ url: …, managed: true, connection, folder })`,
4106
+ }
4107
+ : {
4108
+ level: 'warning',
4109
+ code: 'HTTP_V1_RETIRED',
4110
+ message: `Step "${step}" emits core.action.http, which the tenant registry no longer serves (only `
4111
+ + `core.action.http.v2). It still compiles, validates and runs, and it calls the URL directly. For a URL `
4112
+ + `that needs no Integration Service connection, keep managed: false. managed: true is not a drop-in `
4113
+ + `replacement: even without a connection it sends the request through Integration Service's shared `
4114
+ + `egress (rate limits there are shared, and public APIs can answer 429 where a direct call succeeds), `
4115
+ + `and a non-2xx fails the step instead of arriving with statusCode. Switch only if the call needs an `
4116
+ + `Integration Service connection or an .onError() handler.`,
4117
+ step,
4118
+ });
3956
4119
  }
3957
4120
  const connection = inputs?.connection;
3958
4121
  const folder = inputs?.folder;
@@ -10,6 +10,20 @@
10
10
  * @param fields - JSON-serializable values, e.g. `{ path, nodes, edges }`.
11
11
  */
12
12
  export declare function reportResult(fields: Record<string, unknown>): void;
13
+ /**
14
+ * Print text that IS the verb's result (a pretty-printed object, a captured
15
+ * path), as opposed to a status line such as `compile: wrote X`.
16
+ *
17
+ * Under `--json` this prints nothing: the result is already the JSON object on
18
+ * stdout, and the umbrella routes a tool's stdout to stderr meanwhile, so a
19
+ * plain `console.log` of the result would land on stderr as a second copy.
20
+ * A caller that merges the streams (`2>&1`, an agent's shell tool) then gets
21
+ * two documents and `json.load` fails with `Extra data` (#901). Status lines
22
+ * keep using `console.log`; they are what stderr is for under `--json`.
23
+ *
24
+ * @param text - the result as text; a trailing newline is added.
25
+ */
26
+ export declare function printResult(text: string): void;
13
27
  /**
14
28
  * Report a check's diagnostics as `{ errors, warnings, diagnostics }`, the one
15
29
  * shape every family's check (and compile's check pass) uses.
@@ -18,6 +18,24 @@ function slot() {
18
18
  export function reportResult(fields) {
19
19
  Object.assign(slot().fields, fields);
20
20
  }
21
+ /**
22
+ * Print text that IS the verb's result (a pretty-printed object, a captured
23
+ * path), as opposed to a status line such as `compile: wrote X`.
24
+ *
25
+ * Under `--json` this prints nothing: the result is already the JSON object on
26
+ * stdout, and the umbrella routes a tool's stdout to stderr meanwhile, so a
27
+ * plain `console.log` of the result would land on stderr as a second copy.
28
+ * A caller that merges the streams (`2>&1`, an agent's shell tool) then gets
29
+ * two documents and `json.load` fails with `Extra data` (#901). Status lines
30
+ * keep using `console.log`; they are what stderr is for under `--json`.
31
+ *
32
+ * @param text - the result as text; a trailing newline is added.
33
+ */
34
+ export function printResult(text) {
35
+ if (slot().json)
36
+ return;
37
+ process.stdout.write(`${text}\n`);
38
+ }
21
39
  /**
22
40
  * Report a check's diagnostics as `{ errors, warnings, diagnostics }`, the one
23
41
  * shape every family's check (and compile's check pass) uses.
@@ -27,9 +27,15 @@ export declare class Expr {
27
27
  */
28
28
  export declare function lit(value: unknown): Expr;
29
29
  /**
30
- * Reference a flow-level variable or input by name → `$vars.<name>`.
30
+ * Reference a flow variable or output by name → `$vars.<name>`.
31
31
  *
32
- * @param name - The variable or input as declared with `.var(...)` / `.input(...)`.
32
+ * @remarks
33
+ * Not for a flow INPUT: an input is published under the trigger's output, so read
34
+ * it with {@link input} (`$vars.start.output.<name>`). A bare `$vars.<input>` is
35
+ * null under any trigger but a manual one, and `check` and `uip maestro flow
36
+ * validate` both refuse it.
37
+ *
38
+ * @param name - The variable or output as declared with `.var(...)` / `.output(...)`.
33
39
  * @returns An {@link Expr} reading that variable at run time.
34
40
  */
35
41
  export declare function v(name: string): Expr;
package/dist/core/expr.js CHANGED
@@ -33,9 +33,15 @@ export function lit(value) {
33
33
  return new Expr(JSON.stringify(value), true, value);
34
34
  }
35
35
  /**
36
- * Reference a flow-level variable or input by name → `$vars.<name>`.
36
+ * Reference a flow variable or output by name → `$vars.<name>`.
37
37
  *
38
- * @param name - The variable or input as declared with `.var(...)` / `.input(...)`.
38
+ * @remarks
39
+ * Not for a flow INPUT: an input is published under the trigger's output, so read
40
+ * it with {@link input} (`$vars.start.output.<name>`). A bare `$vars.<input>` is
41
+ * null under any trigger but a manual one, and `check` and `uip maestro flow
42
+ * validate` both refuse it.
43
+ *
44
+ * @param name - The variable or output as declared with `.var(...)` / `.output(...)`.
39
45
  * @returns An {@link Expr} reading that variable at run time.
40
46
  */
41
47
  export function v(name) {
@@ -13,13 +13,11 @@
13
13
  * `Duplicate variable ID: "x" (appears 2 times)`). Before flow-builder-sdk#890,
14
14
  * `check` and `compile` were silent and wrote both rows.
15
15
  *
16
- * The common shape is `.output({ summary }) + .var('summary')` — "the output
17
- * the arms assign". The remedy keeps the output and renames the var:
18
- * `.var('draftSummary')` + `.return({ summary: v('draftSummary') })`. The output
19
- * then stays a declared `out` global — the direction the Flow docs and the eval
20
- * checkers (`direction == "out"`) treat as the flow's output contract. Dropping the name from `.output()` also
21
- * validates — a `.var()` is an `inout` global, which the product counts as an
22
- * output too — but the flow then has no declared `out` for it.
16
+ * One pair is NOT a collision: `.output({ x }) + .var('x')` with the same type is
17
+ * the loop-accumulator shape ("declare the result, then seed it"), and
18
+ * `planFlowGlobals` (`flow-variables.ts`) merges it into ONE `out` global that
19
+ * carries the var's default (flow-builder-sdk#899). Every other repeat of a name
20
+ * is reported with the sentence below.
23
21
  */
24
22
  export type GlobalKind = 'input' | 'output' | 'var';
25
23
  export interface GlobalNameCollision {
@@ -13,13 +13,11 @@
13
13
  * `Duplicate variable ID: "x" (appears 2 times)`). Before flow-builder-sdk#890,
14
14
  * `check` and `compile` were silent and wrote both rows.
15
15
  *
16
- * The common shape is `.output({ summary }) + .var('summary')` — "the output
17
- * the arms assign". The remedy keeps the output and renames the var:
18
- * `.var('draftSummary')` + `.return({ summary: v('draftSummary') })`. The output
19
- * then stays a declared `out` global — the direction the Flow docs and the eval
20
- * checkers (`direction == "out"`) treat as the flow's output contract. Dropping the name from `.output()` also
21
- * validates — a `.var()` is an `inout` global, which the product counts as an
22
- * output too — but the flow then has no declared `out` for it.
16
+ * One pair is NOT a collision: `.output({ x }) + .var('x')` with the same type is
17
+ * the loop-accumulator shape ("declare the result, then seed it"), and
18
+ * `planFlowGlobals` (`flow-variables.ts`) merges it into ONE `out` global that
19
+ * carries the var's default (flow-builder-sdk#899). Every other repeat of a name
20
+ * is reported with the sentence below.
23
21
  */
24
22
  /** The names declared more than once across `.input()`, `.output()` and `.var()`. */
25
23
  export function globalNameCollisions(inputs, outputs, vars) {
@@ -49,12 +47,5 @@ export function globalNameCollisionMessage({ name, kinds }) {
49
47
  const head = `"${name}" is declared ${kinds.length} times (${declared}). The .flow keeps inputs, outputs and `
50
48
  + `variables in one list, variables.globals[], so this compiles to ${kinds.length} globals with the id "${name}" `
51
49
  + `and \`uip maestro flow validate\` refuses it: Duplicate variable ID: "${name}".`;
52
- const distinct = new Set(kinds);
53
- if (distinct.size === 2 && distinct.has('output') && distinct.has('var')) {
54
- return `${head} Keep the output and give the variable its own name: `
55
- + `.var('${name}Value', …) with { updates: { ${name}Value: … } }, then .return({ ${name}: v('${name}Value') }). `
56
- + `(Dropping "${name}" from .output() also validates — a .var() compiles to an \`inout\` global, which the `
57
- + `product counts as an output — but then the flow declares no \`out\` variable for it.)`;
58
- }
59
50
  return `${head} Rename one of them.`;
60
51
  }
package/dist/decompile.js CHANGED
@@ -1762,7 +1762,7 @@ function emitVariables(e, depth, flow, imp) {
1762
1762
  if (inputs.length)
1763
1763
  e.line(depth, `.input(${shape(inputs, true)})`);
1764
1764
  if (outputs.length)
1765
- e.line(depth, `.output(${shape(outputs, false)})`);
1765
+ e.line(depth, `.output(${shape(outputs, true)})`);
1766
1766
  for (const g of inouts) {
1767
1767
  const rich = declExpr(g, false, imp);
1768
1768
  const def = g.defaultValue !== undefined ? `, ${literal(g.defaultValue)}` : '';
@@ -2538,7 +2538,7 @@ function emitChildFlow(name, child, ctx) {
2538
2538
  };
2539
2539
  const parts = [`${ctx.imp.use('flow')}(${str(name)})`];
2540
2540
  // Variables.
2541
- const shape = (dir) => `{ ${globals.filter((g) => g.direction === dir).map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, dir === 'in', ctx.imp)}`).join(', ')} }`;
2541
+ const shape = (dir) => `{ ${globals.filter((g) => g.direction === dir).map((g) => `${key(g.name ?? g.id)}: ${declExpr(g, dir !== 'inout', ctx.imp)}`).join(', ')} }`;
2542
2542
  if (globals.some((g) => g.direction === 'in'))
2543
2543
  parts.push(`.input(${shape('in')})`);
2544
2544
  if (globals.some((g) => g.direction === 'out'))
@@ -71,7 +71,11 @@ export interface VarSpec {
71
71
  subType?: string;
72
72
  /** JSON schema for complex type validation (object/array shapes). */
73
73
  schema?: Record<string, unknown>;
74
- /** Default value — inputs and vars only; the format forbids it on outputs. */
74
+ /**
75
+ * Default value — the variable's value before anything assigns it. On an
76
+ * output it is the starting value a `{ updates }` accumulator reads, e.g.
77
+ * `.output({ product: { type: types.number, default: 1 } })`.
78
+ */
75
79
  default?: unknown;
76
80
  }
77
81
  /**
@@ -377,7 +381,7 @@ export interface ReturnOptions extends NodeOptions {
377
381
  * `done` become `done` and `done2`.
378
382
  *
379
383
  * ```ts
380
- * .branch('checkTemperature', js`$vars.tempF > 60`,
384
+ * .branch('checkTemperature', js`${input('tempF')} > 60`,
381
385
  * (t) => t.return({}, { name: 'endNiceDay', label: 'Nice Day' }),
382
386
  * (e) => e.return({}, { name: 'endBringJacket', label: 'Bring a Jacket' }))
383
387
  * ```
@@ -741,7 +745,7 @@ declare class StepList {
741
745
  * ```ts
742
746
  * export default flow('draft-review')
743
747
  * .output({ summary: types.string })
744
- * // Not `.var('summary')`: an output and a var share one namespace (VARIABLE_NAME_COLLISION).
748
+ * // A working variable the arms assign; the return maps it onto the output.
745
749
  * .var('result', types.string)
746
750
  * .stepSwitch('review', hitl({ fields: [], outcomes: ['Publish', 'Revise'] }), [
747
751
  * { value: 'Publish', body: (b) => b.step('publish', script({ code: 'return "published";', returns: 'string' }), { updates: { result: out('publish') } }) },
@@ -1323,6 +1327,25 @@ declare class FlowBuilder extends StepList {
1323
1327
  /**
1324
1328
  * Declare the flow's outputs — what `.return(...)` binds values to.
1325
1329
  *
1330
+ * @remarks
1331
+ * An output is also a flow variable: `v('<name>')` reads it and a step's
1332
+ * `{ updates }` assigns it, so an accumulator needs no separate `.var()`. Give
1333
+ * it a starting value with the rich spelling, `{ type, default }`. A `.var()`
1334
+ * of the same name and type is merged into the output rather than emitted as a
1335
+ * second variable (its default becomes the output's); a different type is a
1336
+ * `check` error, `OUTPUT_VAR_MISMATCH`.
1337
+ *
1338
+ * @example
1339
+ * **A loop accumulator**
1340
+ * ```ts
1341
+ * flow('multiply')
1342
+ * .output({ product: { type: types.number, default: 1 } })
1343
+ * .loop('each', lit([2, 3, 4]), (b) => b
1344
+ * .step('mul', script({ code: 'return $vars.product * $vars.each.currentItem;', returns: 'number' }),
1345
+ * { updates: { product: out('mul') } }))
1346
+ * .return({ product: v('product') })
1347
+ * ```
1348
+ *
1326
1349
  * @param shape - Output names to type descriptors, e.g. `{ total: types.number }`.
1327
1350
  * @returns This builder, so calls chain.
1328
1351
  */
@@ -1384,7 +1407,7 @@ export declare function flow(id: string): FlowBuilder;
1384
1407
  * const reverse = flow('reverse-text')
1385
1408
  * .input({ text: types.string })
1386
1409
  * .output({ reversed: types.string })
1387
- * .step('rev', script({ code: 'return $vars.text.split("").reverse().join("");' }))
1410
+ * .step('rev', script({ code: 'return $vars.start.output.text.split("").reverse().join("");' }))
1388
1411
  * .return({ reversed: out('rev') });
1389
1412
  *
1390
1413
  * export default flow('reverse-outer')
package/dist/flow-sdk.js CHANGED
@@ -63,16 +63,14 @@ function assertShape(method, shape) {
63
63
  }
64
64
  /**
65
65
  * Normalize a `TypeDesc | VarSpec` declaration value into VarDecl fields.
66
- * Outputs reject `default` — the format only persists defaults on `in`/`inout`.
66
+ * Every direction takes a `default`: the format's `workflowVariableSchema` allows
67
+ * `defaultValue` on `in`, `out` and `inout` alike, and the v1 CLI's
68
+ * `variable add --direction out --default-value` writes one on an output.
67
69
  */
68
70
  function varDeclOf(method, name, value) {
69
71
  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
70
72
  const spec = value;
71
73
  assertType(method, name, spec.type);
72
- if (method === 'output' && spec.default !== undefined) {
73
- throw new TypeError(`.output() declared ${JSON.stringify(name)} with a default, but the .flow format only persists `
74
- + `defaults on inputs and vars. Bind the value with .return() instead.`);
75
- }
76
74
  return {
77
75
  type: spec.type,
78
76
  ...(spec.default !== undefined ? { default: spec.default } : {}),
@@ -332,7 +330,7 @@ class StepList {
332
330
  * ```ts
333
331
  * export default flow('draft-review')
334
332
  * .output({ summary: types.string })
335
- * // Not `.var('summary')`: an output and a var share one namespace (VARIABLE_NAME_COLLISION).
333
+ * // A working variable the arms assign; the return maps it onto the output.
336
334
  * .var('result', types.string)
337
335
  * .stepSwitch('review', hitl({ fields: [], outcomes: ['Publish', 'Revise'] }), [
338
336
  * { value: 'Publish', body: (b) => b.step('publish', script({ code: 'return "published";', returns: 'string' }), { updates: { result: out('publish') } }) },
@@ -1096,6 +1094,25 @@ class FlowBuilder extends StepList {
1096
1094
  /**
1097
1095
  * Declare the flow's outputs — what `.return(...)` binds values to.
1098
1096
  *
1097
+ * @remarks
1098
+ * An output is also a flow variable: `v('<name>')` reads it and a step's
1099
+ * `{ updates }` assigns it, so an accumulator needs no separate `.var()`. Give
1100
+ * it a starting value with the rich spelling, `{ type, default }`. A `.var()`
1101
+ * of the same name and type is merged into the output rather than emitted as a
1102
+ * second variable (its default becomes the output's); a different type is a
1103
+ * `check` error, `OUTPUT_VAR_MISMATCH`.
1104
+ *
1105
+ * @example
1106
+ * **A loop accumulator**
1107
+ * ```ts
1108
+ * flow('multiply')
1109
+ * .output({ product: { type: types.number, default: 1 } })
1110
+ * .loop('each', lit([2, 3, 4]), (b) => b
1111
+ * .step('mul', script({ code: 'return $vars.product * $vars.each.currentItem;', returns: 'number' }),
1112
+ * { updates: { product: out('mul') } }))
1113
+ * .return({ product: v('product') })
1114
+ * ```
1115
+ *
1099
1116
  * @param shape - Output names to type descriptors, e.g. `{ total: types.number }`.
1100
1117
  * @returns This builder, so calls chain.
1101
1118
  */
@@ -1196,7 +1213,7 @@ export function flow(id) {
1196
1213
  * const reverse = flow('reverse-text')
1197
1214
  * .input({ text: types.string })
1198
1215
  * .output({ reversed: types.string })
1199
- * .step('rev', script({ code: 'return $vars.text.split("").reverse().join("");' }))
1216
+ * .step('rev', script({ code: 'return $vars.start.output.text.split("").reverse().join("");' }))
1200
1217
  * .return({ reversed: out('rev') });
1201
1218
  *
1202
1219
  * export default flow('reverse-outer')
@@ -11,3 +11,69 @@ export interface FlowGlobalDeclaration {
11
11
  }
12
12
  /** Read both supported Flow variable spellings into one declaration table. */
13
13
  export declare function readFlowGlobals(variables: unknown, strict?: boolean): FlowGlobalDeclaration[];
14
+ /** The declaration fields `planFlowGlobals` reads (structurally a `VarDecl`). */
15
+ export interface DeclaredVariable {
16
+ name: string;
17
+ type: string;
18
+ default?: unknown;
19
+ description?: string;
20
+ subType?: string;
21
+ schema?: Record<string, unknown>;
22
+ }
23
+ /** One reason the declared names cannot become one global each. */
24
+ export interface FlowGlobalsProblem {
25
+ code: 'VARIABLE_NAME_COLLISION' | 'OUTPUT_VAR_MISMATCH';
26
+ name: string;
27
+ message: string;
28
+ suggestion?: string;
29
+ }
30
+ /** What `variables.globals` will hold, before directions and trigger ids are stamped. */
31
+ export interface FlowGlobalsPlan<D extends DeclaredVariable = DeclaredVariable> {
32
+ /** Every `.output()`, in order. One with a same-named `.var()` carries that var's default. */
33
+ outputs: D[];
34
+ /** The `.var()` declarations NOT folded into an output, in order. */
35
+ vars: D[];
36
+ /** Names declared by both `.output()` and `.var()` and merged into one `out` global. */
37
+ merged: string[];
38
+ /** Collisions that would emit two globals with one id. Empty when the plan is emittable. */
39
+ problems: FlowGlobalsProblem[];
40
+ }
41
+ /**
42
+ * Turn a flow's `.input()` / `.output()` / `.var()` declarations into the
43
+ * globals the `.flow` file will carry — one per id, because the product's
44
+ * `flow validate` rejects a repeated id (`[variables.globals] Duplicate variable
45
+ * ID`), and the three methods share that one namespace.
46
+ *
47
+ * **An output and a var with the same name are one variable** (flow-builder-sdk#899).
48
+ * The natural loop accumulator is
49
+ * `.output({ product: types.number }).var('product', types.number, 1)`: declare
50
+ * the result, then seed it. Both declarations describe the same value, so they
51
+ * merge into ONE `direction: 'out'` global that carries the var's default.
52
+ *
53
+ * Why `out` and not `inout`, measured against `@uipath/flow-converter` 0.69 and
54
+ * `@uipath/maestro-sdk` (the code that turns a `.flow` into what runs):
55
+ * - only an `out` global is a flow OUTPUT at the root — `buildNodeOutputMappings`
56
+ * drops an End node's mapping for any id that is not `direction: 'out'`;
57
+ * - `inout` is published as an INPUT too — solution discovery puts it on the
58
+ * entry point's input schema — so `inout` would widen the caller's contract;
59
+ * - `{ updates }` on an `out` global is the same output mapping (`var: <id>`) as
60
+ * on an `inout` one, so a loop body can read and rewrite it mid-run;
61
+ * - the `default` on `<uipath:output>` is serialized (`collectVariablesFromNodes`)
62
+ * and a subflow seeds its `out` globals from `defaultValue ?? null`
63
+ * (`buildSeedTask`). The v1 CLI's `variable add --direction out
64
+ * --default-value` writes the same shape.
65
+ *
66
+ * Metadata: the output's `description` / `subType` / `schema` win, falling back
67
+ * to the var's; the default is whichever side declared one. A disagreement the
68
+ * merge would have to resolve by picking a side — a different `type`, two
69
+ * different defaults, two different `subType`s or `schema`s — is
70
+ * `OUTPUT_VAR_MISMATCH` instead, because silently choosing one changes what the
71
+ * flow computes. Every other repeated name (two vars, two outputs, an input
72
+ * reused as an output or var) stays `VARIABLE_NAME_COLLISION`
73
+ * (`core/global-names.ts`, flow-builder-sdk#890).
74
+ */
75
+ export declare function planFlowGlobals<D extends DeclaredVariable>(decl: {
76
+ inputs: readonly D[];
77
+ outputs: readonly D[];
78
+ vars: readonly D[];
79
+ }): FlowGlobalsPlan<D>;