Skip to content

Commit 9f5eca5

Browse files
fix(spec/automation)!: refuse a $ name at every remaining flow binding — loop / map iterator and index, screen idVariable and field name, declared variables, assignment targets (#22746)
Fixes #22572 Clause-②: no (narrowing) ## What this does This is the close-out of the family "the `$` names are the flow engine's at every binding door", after #22477 (the read side) and #22502 (`outputVariable`, `errorVariable`). Every remaining place a flow binds a variable by name now refuses a name whose first non-blank character is `$`. It uses the one rule PR #22569 added, `flowBoundVariableNameSchema` in `packages/spec/src/automation/flow-bound-variable-name.ts`. The rule stays package-internal, so there is no new export (`check:api-surface` is green). | binding position | contract | how the published JSON Schema states it | |:--|:--|:--| | `loop` `iteratorVariable` / `indexVariable` | `LoopConfigSchema` | `pattern` (default `item` and `minLength: 1` kept) | | `map` `iteratorVariable` / `indexVariable` | `MapConfigSchema` | `pattern` (default `item` kept) | | object-form `screen` `idVariable` | `ScreenConfigSchema` | `pattern` | | `screen` field `name` (an in-place addition, see below) | `ScreenFieldConfigSchema` | `pattern` | | declared variable `name` | `FlowVariableSchema` | `pattern` | | `assignment` target, a key of the `assignments` map | `AssignmentConfigSchema` (key schema) | `propertyNames.pattern` | | `assignment` target, a top-level key of a bare config | `AssignmentConfigSchema` (`superRefine`) | not stated: one new declared site in `dropped-refinements.baseline.json` | | `assignment` target, a legacy `[{ variable, value }]` item | the `FlowSchema` arm | not stated (the contract refuses the array form whole) | **One sentence at every door.** The refusal is the rule's own message. It says the `$` names are reserved for the engine, names the key, and gives the remedy: the same name without the `$`, read as `{{ name }}`. - **`loop`, `map` and `screen` keys.** These are refused through `flowNodeConfigRefusals`, so `FlowSchema.parse`, `registerFlow`, `objectstack validate` and the run itself (`parseNodeConfig`) all refuse them at `nodes.N.config.KEY`. - **`FlowVariableSchema.name`.** `FlowSchema` refuses it directly, at `variables.N.name`. - **`assignment` targets.** No executor contract parses an `assignment` config, because `getBuiltinNodeConfigContracts()` has no `assignment` entry. So `FlowSchema.parse` never applied `AssignmentConfigSchema`. A new arm in the `FlowSchema` superRefine walks `collectFlowGraphs`, region bodies included. It judges `flowAssignmentTargets(config)`, which covers the three shapes the executor binds (`service-automation` `builtin/logic-nodes.ts`) and only those. **The first non-blank character, at every binding key.** The `screen` and `script` executors trim a name before they bind it (`cfg.idVariable.trim()`, `cfg.outputVariable?.trim()`). So `idVariable: ' $id'` passed a first-character rule. The rule's regex is now `^\s*(?:[^$\s][\s\S]*)?$` (with `\$error|` for `errorVariable`). JavaScript's `\s` is exactly the set `String.prototype.trim` removes, so the rule refuses a name exactly when its trimmed form starts with `$`. This applies to `outputVariable` and `errorVariable` too. #22502's changeset is still unreleased on the same `18.0.0-next` line. ## Additions beyond the card's list (bounded in-place fixes; all four conditions hold) The fixes below meet all four conditions: the same defect class, a mechanical fix (compose the one rule), the claim's own file with no other claim on it, and the same gate family. 1. **`ScreenFieldConfigSchema.name`.** Its own describe says "the flow variable the value binds to". The resume that submits a flat screen writes each field's value under its `name`. A `$` name there makes the screen impossible to submit (measured below). 2. **The leading blank**, as above. Measured: under the first-character rule, `idVariable: ' $id'` registered. The paused screen then named `$id`, and its resume answered `INVALID_SIGNAL`. The claim's file surface needs two more entries: `ScreenFieldConfigSchema` (in `builtin-node-config.zod.ts`, already listed) and `packages/spec/dropped-refinements.baseline.json`. That file is a forced debt of the bare-shape key rule: `build-schemas.ts` refuses the build until the site is declared, and its header total moves from 704 to 705. The four regenerated reference pages are within "as `check:generated` decides". ## The PM's mechanism assumptions, measured at `origin/main` `0f77ff5202` 1. **The sites.** Every one accepted a `$` name, measured with each contract's `safeParse` and `FlowSchema.safeParse` against the built dist: | position | at `0f77ff5202` | input | result | |:--|:--|:--|:--| | `LoopConfigSchema.iteratorVariable` | `control-flow.zod.ts:220`, `z.string().min(1).default('item')` | `'$row'` | accepted | | `LoopConfigSchema.indexVariable` | `:222` | `'$i'` | accepted | | `MapConfigSchema.iteratorVariable` | `builtin-node-config.zod.ts:1059`, `z.string().default('item')` (where #19939 S1 left the block) | `'$row'` | accepted | | `MapConfigSchema.indexVariable` | `:1061` | `'$i'` | accepted | | `ScreenConfigSchema.idVariable` | `:862` | `'$id'` | accepted | | `ScreenFieldConfigSchema.name` | `:700` (not on the card) | `'$f'` | accepted | | `AssignmentConfigSchema`, the `assignments` map key | `:1163`, `z.record(z.string().min(1), ...)` | `'$y'` | accepted | | `AssignmentConfigSchema`, a bare top-level key | `.catchall()` sees values, never keys | `'$y'` | accepted | | `FlowVariableSchema.name` | `flow.zod.ts:218`, `z.string()` | `'$x'` | accepted | | `FlowSchema` with `variables: [{ name: '$x' }]` and `assignment` `{ assignments: { $y: 1 } }` | | | accepted | 2. **Defaults.** Both `iteratorVariable` defaults stay `item` (pinned). The published JSON carries `pattern` beside `default: "item"`. `check:authorable-surface` is green, and `authorable-defaults/automation.json` is unchanged. 3. **The engine's own names.** No shipped flow or fixture binds a `$` name on these positions (item 4). The run-time effect was measured with `AutomationEngine` at `0f77ff5202`, through a scratch test that is not committed: - `loop` with `iteratorVariable: '$record'` and `indexVariable: '$runId'`: after the loop, a screen titled `record is {{ $record }}, runId is {{ $runId }}` rendered `record is B, runId is 1`. - `map` with `iteratorVariable: '$record'`: `record is B`. - `assignment` with `{ assignments: { $record: 'clobbered' } }`: `record is clobbered`. - a declared `$record` with `defaultValue: 'mine'`: the engine's own seeding overwrote it, and the title showed the trigger record. - object-form `screen` with `idVariable: '$id'`: the run paused, and the resume the console sends (`{ $id: id }`, `FlowRunner.tsx` `onObjectFormSaved` at the pinned objectui `20c6d351ad`) answered `INVALID_SIGNAL`. - flat `screen` field `name: '$x'`: the same `INVALID_SIGNAL`. - After this change, `registerFlow` refuses all six flows: a `ZodError` from `canonicalizeStoredFlow` at the binding path. 4. **The reach: zero newly refused bindings.** - **The real parse.** All 35 flows the examples ship (`app-crm` 1, `app-todo` 4, `app-showcase` 30) were run through `FlowSchema.safeParse`. All parse OK, and the before and after listings are byte-identical. - **An AST scan.** The scanner reads TypeScript object-literal properties, plus a text arm for JSON and YAML. On a control fixture it found 11 of 11 planted positives. It covered `examples` (234 files), `packages/platform-objects` (167 files; it ships no flow), `packages/qa/dogfood` (280), the rest of `packages`, `skills`, `apps` and `scripts`, and `hotcrm` at `1d7148bf2d` (570 files; a read-only clone, deleted afterwards). It found no `$`-led binding on any position. Its only hits were `errorVariable: '$error'`, which stays legal, and filter operator keys. - **The pinned objectui `20c6d351ad`.** None. The control (`errorVariable: '$error'`) hit. 5. **The enumeration pin.** The pin finds binding keys by name. It walks the JSON Schema of every Zod export of `automation/index.ts` (72 schemas) for any property spelled `*Variable`, at any depth. Each one must publish the rule's `pattern`, be in the rule's vocabulary, and have a row in the pin's site table. A floor keeps the walk from passing over nothing. - **The honest limit.** A binding position not spelled `*Variable` cannot be found this way: a field's `name`, a variable's `name`, an `assignments` key. Those three are hand-listed. They are the rule's exported vocabulary `FLOW_BINDING_KEYS`, which the pin holds equal to the site table. ## ADR-0087 disposition - **D3 entry.** `packages/spec/src/migrations/entries/semantic/18.flow-binding-name-dollar-refused.ts`, on protocol 18. Its step-18 rationale fragment has `order: 93`, after #22502's `92`. `registry.ts` was regenerated with `gen:migration-registry`. It is still tracked, because PR #22706 had not landed at `f59a73c395`. - **No D2 conversion.** The bare name may already be bound in the flow, and the reads of the old name sit in every dialect a flow string speaks. - **Changeset.** `.changeset/22572-flow-binding-name-dollar-refused.md`: `@objectstack/spec` `major` in pre mode, with `Clause-②: no (narrowing)` and the `registered flow-binding-name-dollar-refused` marker. `check-adr-0087-registration` reads `[major+BREAKING+clause-②-narrowing] registered flow-binding-name-dollar-refused (new here)`. ## Tests (at `af752544fb`) - **New pins**, in `packages/spec/src/automation/flow-bound-variable-name.test.ts`. The file went from 26 to 115 tests. The pin table has 18 binding sites, and each site: - is refused by its contract, with the remedy; - is refused by `FlowSchema` at exactly its path, and nowhere else; - refuses the engine's names `$record`, `$runId`, `$loopItems` and `$`; - refuses a leading blank (`' $x'`, a tab, a newline); - accepts the controls `x`, `a$b` and `' x'`. The file also pins: - the defaults; - `defineStack` refusing a declared `$total` with `{ code: 'STACK_SCHEMA_INVALID', status: 422 }` at `flows.0.variables.0.name`; - the remedy reading clean (`{{ row.name }}` in a loop body); - `flowAssignmentTargets` over the three shapes, and an assignment inside a region body; - the vocabulary pin, the discovery pin and the published-pattern pin; - the D3 entry and its rationale fragment. - **`@objectstack/spec`**, `vitest run --project local --maxWorkers=2`: **642 files, 19286 passed, 1 todo**. `pnpm --filter @objectstack/spec typecheck` exits 0, with `check:test-typecheck: OK`. - **`@objectstack/service-automation`**, the whole suite against the rebuilt spec: **185 files, 2368 passed**. Its typecheck exits 0. - **`@objectstack/lint`**, the whole suite: **135 files, 6321 passed**. Its typecheck exits 0. - **The CLI guidance test**, `vitest run --project integration test/migrate-meta-engine-guidance.test.ts`, after the CLI closure build (`turbo run build --filter=@objectstack/cli^...`): **3 passed**. It holds the new entry's printed guidance. - **`pnpm --filter @objectstack/spec check:generated`**: all 15 generated artifacts are up to date, after a rebuild at the final source. ## Ablation Each ablation ran against the committed fix, with the mutation and restore going through `scripts/ablation-replace.mjs` in wrap mode. The spec tests import `src`, so no dist leg applies. 1. **The rule dropped from `MapConfigSchema.indexVariable`.** The anchor went from 1 to 0, and the blob from `cbe18e1969b2` to `ca9af2dd1eb3`. - Result: **4 failed, 95 passed**. Red: the map `indexVariable` contract, flow-door and engine-name pins, and the discovery pin with `MapConfigSchema.indexVariable publishes no pattern`. - Restore: the blob equals HEAD (`cbe18e1969b2`), and `git diff HEAD` is empty. 2. **The leading-blank half dropped** (`[^$\s]` back to `[^$]`). The blob went from `b7564636d07e` to `ae5b2c3c1277`. - Result: **18 failed, 97 passed**: all 16 leading-blank rows, plus both published-pattern pins. - Restore: the blob equals HEAD (`b7564636d07e`), and `git diff HEAD` is empty. ## Gates (at `af752544fb`) - **The derived gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths, merge base `f59a73c39`) derived 114 commands. 112 were run, all exit 0. `check:skill-examples` first exited 3 (PREREQUISITE NOT MET: `client-react` was not built). After building `client` and `client-react`, its re-run read `262 prose examples type-check`. - **The reconciliation.** `--ran` reads `114 derived, 112 run, 0 NOT-MEASURED, 2 UNRUN`. - **NOT MEASURED: `check:dual-build-cjs-loads`.** Reason: it needs a whole-workspace build, which the dispatch rules out. - **NOT MEASURED: `check:type-check-debt`.** Reason: its script is `--re-measure`, which the dispatch rules out. - **NOT MEASURED: the workspace typecheck lane.** Reason: it needs a whole-workspace build. The type surface is byte-unchanged (`check:api-surface` is green), because a `regex` check changes no TypeScript type. - **The artifact-roster block.** It is run separately. Its three PR-context guards are wired to this PR after it opened, and their readings are in the report on #22572. ## Acceptance notes - **The two strays triage named** stay noted: the `service-automation` executor `configSchema` descriptors, and the `engine.ts` `buildSubflowResumeSignal` comment. This PR touches no `service-automation` file. - **A body-less legacy `loop`.** Its values are not judged at the flow door. This is the contract map's `parsedWhen`, unchanged since #21982. Its `iteratorVariable` is never read; the executor sets `$loopItems` and `$loopIndex` itself. - **`map.input` and `subflow.input` keys** name the callee's variables, so they are not bindings in this flow. A callee can no longer declare a `$` variable, so a `$` key there binds nothing. - **`api/automation-api.zod.ts` `ScreenSpec.idVariable`** is a response shape the engine produces, not an authoring door. It carries no rule. - **`content/docs/automation/flows.mdx`** (hand-written) still names only `outputVariable` beside `errorVariable` in its paragraph on where the `$` names belong. The paragraph is still true, and it is outside the claim's surface. It is not widened here. --- _Generated by [Claude Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c74d843 commit 9f5eca5

13 files changed

Lines changed: 706 additions & 69 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'@objectstack/spec': major
3+
---
4+
5+
Every remaining place a flow binds a variable by name refuses a name that starts with `$`: a `loop` or `map` node's `iteratorVariable` and `indexVariable`, an object-form `screen` node's `idVariable`, a `screen` field's `name`, a declared flow variable's `name`, and an `assignment` node's targets. The refusal names the remedy: the same name without the `$`, read as `{{ name }}`.
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: registered flow-binding-name-dollar-refused -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `major` on the v18 line (`.changeset/pre.json` is open on `main` in `next` pre mode, so the release is `18.0.0-next.*`).
12+
13+
**Why.** The `$` names are the flow engine's own variables: it binds `$record`, `$runId`, `$flowName`, `$flowLabel` and `$error`, a flat-graph `loop` binds `$loopItems` and `$loopIndex`, and a resume signal may not write any `$` name. A flow text slot refuses a `{{ }}` hole over a `$` name the engine does not bind, and `outputVariable` / `errorVariable` already refuse one. Every other binding took any string, and on the run a `$` binding was worse than unreadable (measured with the engine on this release's `main`):
14+
15+
- `iteratorVariable: '$record'` on a `loop` or `map` left `$record` holding the last item for the rest of the run, and `indexVariable: '$runId'` left `$runId` holding an index;
16+
- `assignments: { $record: … }` overwrote the trigger record;
17+
- a declared variable named `$record` was overwritten by the engine at run start, so its `defaultValue` never reached the run;
18+
- a `screen` whose `idVariable` or field `name` was a `$` name paused, and then could never be submitted: the resume carrying the value was refused with `INVALID_SIGNAL`;
19+
- a leading blank hid the `$` from a first-character rule, while the `screen` and `script` executors trim a name before they bind it: under that rule `idVariable: ' $id'` registered, the paused screen named `$id`, and its resume was refused the same way; a `script`'s `outputVariable: ' $record'` passes the protocol-18 rule too and is trimmed to `$record` by its executor (read from the executor, not run).
20+
21+
**What is refused.**
22+
23+
- `iteratorVariable` and `indexVariable` on `LoopConfigSchema` and `MapConfigSchema`, `idVariable` on `ScreenConfigSchema`, `name` on `ScreenFieldConfigSchema` and on `FlowVariableSchema`: a name that starts with `$`. Each key states the rule as a JSON Schema `pattern`, so the published `json-schema/**` refuses what the parse refuses. The parse issue is an `invalid_format` (regex) issue at the key, and its message names the remedy.
24+
- Every binding key — `outputVariable` and `errorVariable` included — now judges the first NON-BLANK character, so `' $x'` is refused like `'$x'`. The pattern's `\s` is exactly the set `String.prototype.trim` removes.
25+
- An `assignment` node's targets, in each shape its executor binds: a key of the `assignments` map (`AssignmentConfigSchema` states it as `propertyNames.pattern`), a top-level key of the bare legacy config, and the `variable` (or `name`, `key`) of a legacy `assignments: [{ variable, value }]` item.
26+
- `FlowSchema.parse`, `registerFlow` and `objectstack validate` refuse the flow where the name was written — `nodes.N.config.iteratorVariable`, `nodes.N.config.fields.M.name`, `nodes.N.config.assignments.NAME`, `variables.N.name` — inside a region body too. A stored flow carrying one is skipped at boot with a warn naming it, and the `loop`, `map` and `screen` executors' own contract parse refuses the node at run time.
27+
28+
**Unchanged.** Any name whose first non-blank character is not `$`, a `$` later in the name (`a$b`) included; the defaults (`iteratorVariable` is still `item`); an empty string where the key took one; a body-less legacy `loop`, whose `iteratorVariable` nothing reads; and every hole the text-slot judge already admits. Non-string values keep the type refusal they had.
29+
30+
## FROM → TO
31+
32+
| you wrote | write instead |
33+
|:--|:--|
34+
| `iteratorVariable: '$row'` with `title: '{{ $row.name }}'` | `iteratorVariable: 'row'` with `title: '{{ row.name }}'` |
35+
| `idVariable: '$account'` | `idVariable: 'account_id'`, read as `{{ account_id }}` |
36+
| `variables: [{ name: '$total', type: 'number' }]` | `variables: [{ name: 'total', type: 'number' }]`, read as `{{ total }}` |
37+
| `assignments: { $total: … }` | `assignments: { total: … }` |
38+
39+
**The one-line fix: name the variable without the `$`, and rename every read of it with it.**
40+
41+
No D2 conversion rewrites the name: the bare name may already be bound in the flow, and the reads of the old name sit in every dialect a flow string speaks (a text-slot hole, a CEL expression, a single-brace token in a value position), so the rename is the author's.
42+
43+
**Who is affected, measured.** The last published spec, `@objectstack/spec@17.7.0` (npm `latest`), types every one of these keys as a plain string. Measured on `main` at this change's base: the 35 flows `examples/app-crm`, `examples/app-todo` and `examples/app-showcase` ship all parse clean under the new rule, and a TypeScript-AST scan of `examples` (234 files), `packages/platform-objects` (167, which ships no flow), `packages/qa/dogfood` (280), the rest of `packages` and the `hotcrm` app found no `$`-led binding on any of these positions. The pinned `objectui` checkout binds none either. Deployed metadata and other repositories were not measured.
44+
45+
### The kit
46+
47+
- **The rule.** `flowBoundVariableNameSchema` in `automation/flow-bound-variable-name.ts`, package-internal (no new public export), now composed into every binding position. A `$` binding is refused with one sentence whichever door judges it.
48+
- **The ledger.** The D3 semantic entry `flow-binding-name-dollar-refused` (protocol 18), with no D2 conversion.

‎content/docs/references/api/automation-api.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ const result = AutomationApiErrorCode.parse(data);
135135

136136
| Property | Type | Required | Description |
137137
| :--- | :--- | :--- | :--- |
138-
| **name** | `string` | ✅ | Variable name |
138+
| **name** | `string` | ✅ | Variable name — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
139139
| **type** | `string` | ✅ | Data type (text, number, boolean, object, list) |
140140
| **isInput** | `boolean` | optional (default: `false`) | Is input parameter |
141141
| **isOutput** | `boolean` | optional (default: `false`) | Is output parameter |

‎content/docs/references/automation/builtin-node-config.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
208208
| :--- | :--- | :--- | :--- |
209209
| **collection** | `string \| any[]` | ✅ | Template/variable resolving to the array to process (an inline array is accepted) |
210210
| **flowName** | `string` | ✅ | Subflow run for each item — it may pause (e.g. an approval) |
211-
| **iteratorVariable** | `string` | optional (default: `"item"`) | Variable holding the current item |
212-
| **indexVariable** | `string` | optional | Optional variable holding the current index |
211+
| **iteratorVariable** | `string` | optional (default: `"item"`) | Variable holding the current item — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
212+
| **indexVariable** | `string` | optional | Optional variable holding the current index — a name without a leading `$` (the `$` names are the flow engine's own) |
213213
| **itemObject** | `string` | optional | When items are records, the object they belong to (exposes each item as the child's record) |
214214
| **input** | `Record<string, any>` | optional | Params passed to each item's subflow, keyed by its input variables: each value a CEL value envelope `{ dialect: 'cel', source }` evaluated per item (the item variable is in scope), or a literal written as it is — a `{…}` template token is refused |
215215
| **outputVariable** | `string` | optional | Each item's subflow output, collected in order — bound to a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
@@ -228,7 +228,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
228228
| **fields** | `{ name: string; label?: string; type?: string; required?: boolean; … }[]` | optional | Input fields collected on this screen |
229229
| **waitForInput** | `boolean` | optional | Pause to show the screen even with no fields; false forces a server pass-through |
230230
| **objectName** | `string` | optional | Render this object's full create/edit form instead of a flat field list |
231-
| **idVariable** | `string` | optional | Object form only: variable bound to the saved record's id |
231+
| **idVariable** | `string` | optional | Object form only: variable bound to the saved record's id — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
232232
| **mode** | `Enum<'create' \| 'edit'>` | optional (default: `"create"`) | Object form only: create (default) or edit; 'edit' needs a recordId to name its target |
233233
| **recordId** | `string` | optional | Object form only: id of the record to edit (required for mode: 'edit' to be useful) |
234234
| **defaults** | `Record<string, any>` | optional | Object form only: prefilled values |
@@ -237,7 +237,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
237237

238238
| Property | Type | Required | Description |
239239
| :--- | :--- | :--- | :--- |
240-
| **name** | `string` | ✅ | Field name (the flow variable the value binds to) |
240+
| **name** | `string` | ✅ | Field name (the flow variable the value binds to) — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
241241
| **label** | `string` | optional | Display label |
242242
| **type** | `string` | optional | Input type |
243243
| **required** | `boolean` | optional | Whether a value is required to submit |
@@ -259,7 +259,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
259259

260260
| Property | Type | Required | Description |
261261
| :--- | :--- | :--- | :--- |
262-
| **name** | `string` | ✅ | Field name (the flow variable the value binds to) |
262+
| **name** | `string` | ✅ | Field name (the flow variable the value binds to) — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
263263
| **label** | `string` | optional | Display label |
264264
| **type** | `string` | optional | Input type |
265265
| **required** | `boolean` | optional | Whether a value is required to submit |

‎content/docs/references/automation/control-flow.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ const result = FlowRegionSchema.parse(data);
145145
| Property | Type | Required | Description |
146146
| :--- | :--- | :--- | :--- |
147147
| **collection** | `string \| any[]` | ✅ | Template/variable resolving to the array to iterate (an inline array is accepted) |
148-
| **iteratorVariable** | `string` | optional (default: `"item"`) | Loop variable holding the current item |
149-
| **indexVariable** | `string` | optional | Optional loop variable holding the current index |
148+
| **iteratorVariable** | `string` | optional (default: `"item"`) | Loop variable holding the current item — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
149+
| **indexVariable** | `string` | optional | Optional loop variable holding the current index — a name without a leading `$` (the `$` names are the flow engine's own) |
150150
| **maxIterations** | `integer` | optional | Hard cap on iterations (clamped to the engine ceiling) |
151151
| **body** | `{ nodes: object[]; edges?: object[] }` | optional | Loop body region (omit for legacy flat-graph loops) |
152152

‎content/docs/references/automation/flow.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const result = FlowSchema.parse(data);
5656

5757
| Property | Type | Required | Description |
5858
| :--- | :--- | :--- | :--- |
59-
| **name** | `string` | ✅ | Variable name |
59+
| **name** | `string` | ✅ | Variable name — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
6060
| **type** | `string` | ✅ | Data type (text, number, boolean, object, list) |
6161
| **isInput** | `boolean` | optional (default: `false`) | Is input parameter |
6262
| **isOutput** | `boolean` | optional (default: `false`) | Is output parameter |
@@ -223,7 +223,7 @@ const result = FlowSchema.parse(data);
223223

224224
| Property | Type | Required | Description |
225225
| :--- | :--- | :--- | :--- |
226-
| **name** | `string` | ✅ | Variable name |
226+
| **name** | `string` | ✅ | Variable name — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
227227
| **type** | `string` | ✅ | Data type (text, number, boolean, object, list) |
228228
| **isInput** | `boolean` | optional (default: `false`) | Is input parameter |
229229
| **isOutput** | `boolean` | optional (default: `false`) | Is output parameter |

‎packages/spec/dropped-refinements.baseline.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"measured": {
44
"zod": "4.4.3",
55
"publishedSchemasWithDroppedRefinements": 226,
6-
"droppedRefinementSites": 704,
6+
"droppedRefinementSites": 705,
77
"refinementSitesThatDidProject": 369,
88
"refinementSitesWithNoJsonFormToCompare": 0
99
},
@@ -462,6 +462,7 @@
462462
},
463463
"automation/AssignmentConfig": {
464464
"sites": [
465+
"out",
465466
"out.assignments.out.valueType",
466467
"out.catchall"
467468
]

0 commit comments

Comments
 (0)