Skip to content

Commit b90a6f7

Browse files
fix(runtime): a flow start input declared a record id is loaded in the caller's own scope from the declared object at every door that starts the flow by name (#30628)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgVwQPWq8KXvAdGshdRR8iSy)_ domain:server#2 · os-bill · session_01Utp6CbNMpDS92ifzhDw9vf ## 维护者速读 - 改了什么:流程启动输入上写了 `objectName`(声明“这个输入是某对象一条记录的 id”)后,凡是按名字启动流程的入口(触发路由新旧两种写法、REST 与 MCP 的流程型动作、声明的流程型端点),都会先用调用者自己的权限从声明的对象里加载这条记录:读不到或不存在答 404 `RECORD_NOT_FOUND`,不是 id 答 400 并点名该输入,两种情况都不会创建运行。没写声明的输入保持原样。 - 为什么改:执行 #29425 裁定 A(6663860762,C 撤回):声明的记录引用要和流程定义里“明显当作 id 读”的输入一样被检查。 - 风险与回滚:收窄。以前能启动的流程,若声明的输入给了调用者读不到的 id,现在被拒。仓内已发布的流程声明这个键的数量为 0。回滚即撤销本 PR。 - 先看:`packages/runtime/src/flow-start-admission.ts` 的 `declaredReadsOf` 新增的声明读取(与可见读取取并集、跟随子流程);`packages/spec/liveness/flow.json` 的 `variables.objectName` 行翻为 live;测试文件末尾新 describe。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/flow-start-declared-record-reads.test.ts`,预期 1 个文件 90 个用例全绿,其中 7 号 describe 每个入口下 7 个。 - 需要你做的:无,席位落地。 Fixes #29425 Clause-②: yes (narrowing) — a start that ran before is now refused when it hands a declared record-id input an id the caller cannot read in the declared object, an id that names no record there, or a value that is not one id. Metadata-protocol: none Skills-docs: none — the published skills do not teach the start-door record rule or the `objectName` input declaration: `git grep -n -i -E "record reference|isInput: true, objectName|declared record|liveness-planned" -- 'skills/**'` has one unrelated hit (`skills/objectstack-ui/references/react-blocks.md:52`, a redirect URL's record fields). ## What changes The rule: **a flow start input that declares it is the id of a record of a named object (`objectName` on the variable) is loaded, at every door that starts the flow by name, from the declared object in the caller's own scope before the run exists. An unreadable or nonexistent id is refused `404 RECORD_NOT_FOUND`; a value that is not one id is refused `400` naming the input. An undeclared input keeps the visible-read rule unchanged.** - **One helper, no door edits.** Every door already asks the shared check that PR #29365 introduced: `refuseUnreadableFlowStartRecords` (`action-execution.ts`), which loads each input from every object `flowDeclaredRecordReads` (`flow-start-admission.ts`) says the run reads it from. `declaredReadsOf` now adds the declaration as one more read, `{ object: objectName, many: false }`. Door population, measured on `main` `2ffdb649` and unchanged since #29365: | Door | Entrances | Calls the shared check at | |:--|:--|:--| | trigger route | `POST /automation/:name/trigger` and the legacy `POST /automation/trigger/:name`, both through `respondToFlowTrigger` | `packages/runtime/src/domains/automation.ts:2824` | | action of `type: 'flow'` | REST `POST /actions/:object/:action` and MCP `run_action`, both through `dispatchFlowAction` | `packages/runtime/src/action-execution.ts:2685` | | declared endpoint of `type: 'flow'` | the endpoint executor | `packages/runtime/src/endpoint-executor.ts:590` | Other `execute(flowName, …)` callers are engine-internal (the `subflow` and `map` nodes, trigger bindings) or the verify harness, none of them a door. - **Declared and also read in plain sight against another object: both are judged.** The declaration is added beside every visible read, never in place of one. If it replaced them, a run that reads object B under the same id would read a row nobody checked, which is the gap #29365 closed. So a caller must be able to read the row in both objects. - **Followed into callees.** A parent that hands its input on, unchanged, to a `subflow` or `map` callee whose input carries the declaration is judged by that declaration, through the same recursion as other callee reads. - **A `loop` or `map` that rebinds the name does not withdraw a declaration.** The shadowing rule exists because a rebound name no longer holds the caller's value when a node reads it. The declaration is about the value the caller sent, so it stands. - **`packages/spec` (seat-authorised, minimal).** The `objectName` liveness row (`packages/spec/liveness/flow.json` › `variables.objectName`) flips from `planned` + `authorWarn` to `live`, with this reader as its evidence. The describe and docblock on `FlowVariableSchema.objectName` drop their planned sentences. Both the row and the docblock said to do this "in the commit that lands the reader". Left as they were, `os validate` would go on warning `liveness-planned-property` on every flow that declares the key, and the describe would say that no door applies it. The regenerated artifacts are `content/docs/references/{automation/flow,api/automation-api}.mdx` and `packages/spec/liveness/state-counts/flow.md`. No key, type or accepted value changes. - **Docs.** `content/docs/automation/flows.mdx`: the "Planned, no door applies this yet" callout under *A start input that is a record id* is replaced by the rule, and the `404` row under *Run a flow via API* names declared inputs. - **Changeset.** `.changeset/29425-declared-record-input.md`, patch for `@objectstack/runtime` and `@objectstack/spec`, ADR-0087 `not-required (no-migration-prescription)`. **Why the title carries no `!`.** No flow shipped in this repository declares the key today. `git grep` over `examples/`, `packages/` and `apps/` for an input variable literal that carries `objectName` finds only CHANGELOG example text. An input without the declaration answers exactly as before. ## Why Ruling A on #29425 (6580257050, A half kept by 6663860762, which withdrew C): a flow start input may declare a record reference, and every door that starts the flow by name loads a declared input in the caller's scope before the run exists, with the refusals #29365 gives a visibly read input. The spec half (#29442) landed the key. This is the server half. ## Tests New top-level describe 7, *a start input declared a record id is loaded from the declared object at every door, before the run exists*, in `packages/runtime/src/flow-start-declared-record-reads.test.ts`, the shared harness of #29365. It uses that file's row-scoped double, where each row is visible only to its owner. Per door (both trigger spellings, the endpoint, REST action, MCP `run_action`): - a declared input naming a readable record: the run starts, and the declared object was loaded (the firing control); - naming a record the caller cannot read, and an id that names nothing: `404 RECORD_NOT_FOUND`, `execute()` never called; - given an object, a list or a boolean: `400` naming `contractId`, nothing loaded from the declared object, no run; - the same input undeclared and read by no node: not judged, the run starts on an id the caller cannot read (today's behaviour, the control). Also pinned: the reading (`flowDeclaredRecordReads`) for a declaration, a declaration beside a visible read of another object, a callee's declaration, and a loop that rebinds the name; the union at a door (readable in the declared object only → `404`; readable in both → run); a parent handing its input to a callee's declared input → `404`; the system principal → run, nothing loaded. **Forced test edits** (`packages/lint/src/lint-liveness-properties.test.ts`, test-only). Two pins read the shipped ledger's `planned` state of this row. Flipping the row is what makes them false. - The #29442 pin *warns on a flow input declared a record reference* becomes *no longer warns … live since the start doors read it*. It follows the same shape the file already uses for the `params.widget` flip. - The `params.widget` pin used this row as its control ("the walk still warns on a drilled planned row"). Both pins now take that control from the other shipped `planned` + `authorWarn` drilled row, an app navigation entry's `dataSource`. ## Acceptance notes - **At an action door, a param's own type answers first.** A declared action param of type `text` that holds an object, a list or a boolean is refused by the action's param contract (ADR-0104 D2), `400 VALIDATION_ERROR` naming the param, before the start check is reached. The trigger routes and the endpoint answer `400 VALIDATION_FAILED` from the start check. Either way the refusal is a 400 that names the input, nothing is loaded and no run starts. The pins assert the code per door. - **Author contradiction, not judged here.** An input declared one id that the definition also reads as a list (`filter.id.$in`) gets both readings, so no value satisfies it and every start is refused `400`. This is loud, not silent, and it is a lint candidate. It is not part of this rule. - **The object-less key.** `objectName: 'global'` passes the spec's name pattern. The shared subject load treats `global` and `*` as the object-less action key and loads nothing, so an input declared against `global` is not judged. This is the same pre-existing behaviour as a visible read of an object named `global`, and it is noted, not changed. ## Verification Readings on head `469106e8f` (after merging `origin/main`) unless stated; the ablations and the first runtime suite ran on `c55e8047`, before the merge. <details><summary>Pins, ablations</summary> - `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/flow-start-declared-record-reads.test.ts` under the verify lock: `Tests 90 passed (90)`, `VERDICT command-exit 0`. - Ablation 1, through `scripts/ablation-replace.mjs --delete`, on the line that adds the declaration (`add(variable.name, { object: variable.objectName, many: false });`). The anchor went from 1 to 0 and the blob from `2b18eb885049` to `945bcd4535b1`. Result: `Tests 30 failed | 60 passed (90)`. Red: every door's readable / unreadable / nonexistent pin, the trigger and endpoint 400 pins, the union and the callee pins, and the four reading pins. Green as designed: the action-door 400 pins (the param contract answers first), every undeclared control, and the system principal. Restored: blob equals HEAD, `git diff HEAD` empty. - Ablation 2, a declaration that replaces the visible reads (`inputs.add(variable.name);` made conditional on no `objectName`). Blob `2b18eb885049` to `cbe9b748c523`. Result: `Tests 2 failed | 88 passed (90)`, the two union pins. Restored, blob equals HEAD. - The test imports the helper from `src/` by relative path, so no `dist` leg applies. </details> <details><summary>Suites, typecheck, lint</summary> - `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2` (before merging `main`): `Test Files 547 passed (547)`, `Tests 9911 passed | 19 skipped (9930)`. - `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2`: `Test Files 195 passed (195)`, `Tests 8103 passed (8103)` on `469106e8f`; the declared-input pin file again `Tests 90 passed (90)`. - `pnpm --filter @objectstack/runtime typecheck`, `pnpm --filter @objectstack/lint typecheck`: exit 0 (after building `@objectstack/driver-turso`, which a runtime kernel test imports and the closure build had not produced). - `eslint --no-inline-config` on the five changed source and test files: exit 0. The repo-wide `pnpm lint` is CI's. - `pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated`: all 14 artifacts current after `gen:docs` and `gen:liveness-counts`, and again after merging `main`. </details> <details><summary>Full dogfood battery</summary> - `469106e8f` · closure build `turbo run build --filter='@objectstack/dogfood^...'` exit 0, then `pnpm --filter @objectstack/dogfood test` under the verify lock: `VERDICT command-exit 0` · `Test Files 420 passed | 3 skipped (423)` · `Tests 3496 passed | 36 skipped (3532)` · no failures. </details> <details><summary>Dispatch gates</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `469106e8f`: 105 commands (11 paths, 417 changed lines, under the 3000-line threshold). Each ran with stdin from /dev/null and its exit code captured before any pipe: 105 exit 0. - `--ran`: `105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN`. - On the pre-merge head `c55e8047`, three had refused with exit 3 for prerequisites: `check-plugin-teardown-shape --self-test` (shallow clone; fixed by fetching its fixture commit), `check:skill-examples` (`client-react` unbuilt) and `check:dual-build-cjs-loads` (workspace unbuilt). All three read exit 0 on `469106e8f` after the closure build. - CI check-runs: not read at the time of writing. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Describe text and the liveness ledger only.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the changed files; the repo-wide run is CI's.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. (The two reference pages are generator output.) - [x] 8. Title, body and comments are in English. (The 维护者速读 section is Chinese by the repo's maintainer rule.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CbNMpDS92ifzhDw9vf)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 482e089 commit b90a6f7

11 files changed

Lines changed: 338 additions & 79 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/runtime': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
fix(runtime): a flow start input declared a record id (`objectName`) is loaded in the caller's own scope from the declared object at every door that starts the flow by name, before the run exists
7+
8+
Clause-②: yes (narrowing) — a start that ran before is now refused when it hands a declared record-id input an id the caller cannot read in the declared object, an id that names no record there, or a value that is not one id. No flow shipped in this repository declares the key, and an input without the declaration answers exactly as before.
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped: `objectName` on a flow variable already parses, and what moves is the answer a start door gives to a caller who hands such an input a value it cannot read. Nothing at rest records that answer, so there is nothing for a migration to rewrite. -->
12+
13+
**The rule.** A flow start input may declare that its value is the id of a record of a named object, with `objectName` on the variable (`{ name: 'accountId', type: 'text', isInput: true, objectName: 'crm_account' }`). Every door that starts the flow by name now loads such an input from the declared object in the caller's own scope before the run exists, whether or not the flow's definition reads the input in plain sight. A record the caller cannot read there and an id that names no record get the same answer, `404 RECORD_NOT_FOUND` (the action door's own refusal), and no run is created. A value that is not one id (a string or a number) is refused `400` naming the input: `VALIDATION_FAILED` from the start check, or, at an action whose param declares a type, `VALIDATION_ERROR` from that param's own contract, which answers first. The system principal is not judged.
14+
15+
**Which doors.** `POST /api/v1/automation/:name/trigger` and the legacy `POST /api/v1/automation/trigger/:name`; a declared endpoint of `type: 'flow'`; an action of `type: 'flow'` over the REST action route and the MCP `run_action` tool. All of them ask the one check they already shared for inputs a definition reads in plain sight.
16+
17+
**Beside the definition's own reads, never in place of them.** An input declared a record id of one object and also read by the definition as an id of another (a `get_record` node's `filter.id`, say) is loaded from both, so the caller must be able to read both rows. A parent flow that hands its input on, unchanged, to a `subflow` or `map` callee whose input carries the declaration is judged by that declaration too. A `loop` or `map` that later rebinds the name does not withdraw the declaration: it is the author's word about the value the caller sent.
18+
19+
**What stays as it was.** An input without the declaration keeps the visible-read rule: it is judged only where the definition reads it as a record id. A start whose caller can read the declared record starts as before.
20+
21+
**If a start now answers 404 or 400.** Hand the declared input the id of a record the caller can read in the object the input names, or start the flow as the system principal.
22+
23+
`@objectstack/spec`: the `objectName` describe on a flow variable no longer says that no door applies it, and its liveness row is `live`, so `os validate` no longer warns `liveness-planned-property` on a flow that declares it.

‎content/docs/automation/flows.mdx‎

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2184,23 +2184,25 @@ export const CloseAccount = defineFlow({
21842184
});
21852185
```
21862186

2187-
<Callout type="warn">
2188-
**Planned — no door applies this yet.** The declaration parses, is refused on a
2189-
variable that is not an input, and is linted, but no start door loads a
2190-
declared input today: a start that hands it an id the caller cannot read still
2191-
runs. `os validate` warns on every flow that writes it
2192-
(`liveness-planned-property`). The contract below is what the start doors will
2193-
enforce.
2194-
</Callout>
2195-
2196-
The contract: every door that starts the flow by name (the trigger route, an
2197-
action of `type: 'flow'` over REST and MCP `run_action`, a declared endpoint of
2187+
Every door that starts the flow by name (the trigger route, an action of
2188+
`type: 'flow'` over REST and MCP `run_action`, a declared endpoint of
21982189
`type: 'flow'`) loads a declared input from the named object in the caller's
2199-
own scope before the run exists. An id the caller cannot read there is refused
2200-
`404` `RECORD_NOT_FOUND`; a value that is not an id is refused `400`
2201-
`VALIDATION_FAILED` naming the input. An input without the declaration is
2202-
judged as a record only where the flow's own definition reads it as a record id
2203-
(see the `404` row under [running a flow via the API](#run-a-flow-via-api)).
2190+
own scope before the run exists. An id the caller cannot read there, or one
2191+
that names no record, is refused `404` `RECORD_NOT_FOUND`, and no run is
2192+
created; a value that is not an id is refused `400` naming the input
2193+
(`VALIDATION_FAILED`; at an action, a param's own declared type answers first
2194+
with `VALIDATION_ERROR`). An input without the declaration is judged as a
2195+
record only where the flow's own definition reads it as a record id (see the
2196+
`404` row under [running a flow via the API](#run-a-flow-via-api)). The system
2197+
principal is not judged.
2198+
2199+
- **Beside the definition's own reads, never in place of them.** When the flow
2200+
also reads the input as an id of another object (a `get_record` node's
2201+
`filter.id`, say), the door loads it from both, and the caller must be able
2202+
to read both rows.
2203+
- **Followed into sub-flows.** A parent that hands its input on, unchanged, to
2204+
a `subflow` or `map` callee whose input carries the declaration is judged by
2205+
that declaration too.
22042206

22052207
- **Inputs only.** A variable that is not `isInput: true` receives nothing from
22062208
a caller, so `objectName` there is refused at parse.
@@ -2606,7 +2608,7 @@ door's own admission check, made before anything is dispatched:
26062608
| `404` | — | No flow by that name in this deployment | Check the name; nothing was dispatched |
26072609
| `403` | `PERMISSION_DENIED` | The caller is not the system principal, and the flow is declared `runAs: 'system'` with a self-triggered `type` (`autolaunched`, `record_change`, `schedule`). The same answer at every door that starts a flow by name: this route, a `type: 'flow'` action (REST and MCP `run_action`) and a declared `type: 'flow'` endpoint | Let it run on its own trigger, or call it from a parent flow's `subflow` node. If members are meant to start it, declare it `type: 'screen'` (or `'api'` for a signed inbound hook) |
26082610
| `403` | `PERMISSION_DENIED` | The flow declares `requiredPermissions` and the caller, not the system principal, does not hold every capability it names. Checked after the row above, at the same doors; the message names the declared and the missing capabilities | Grant the capability through a permission set's `systemPermissions`, or start the flow as someone who holds it |
2609-
| `404` | `RECORD_NOT_FOUND` | The start names a record (`recordId`, or the object's id alias such as `crmLeadId`) that the caller, not the system principal, cannot read in its own scope, or that does not exist: the two get one answer, the one the action door gives. Checked after the two rows above, at the same doors. A record key that is not one id, or a record named with no `objectName` for a flow declared `runAs: 'system'`, is refused `400` `VALIDATION_FAILED` naming the key. Where the flow's own definition reads a start input as the id of a record of a fixed object (a `get_record`, `update_record` or `delete_record` node with a literal `objectName` whose `filter.id` or `filter.id.$eq` is the input, or whose `filter.id.$in` is; a `screen` node with a literal `objectName` whose `recordId` is the input; a `subflow` or `map` node that hands the input to a callee input the callee reads so), that input is also loaded in the caller's own scope from THAT object, whatever object the start names, or none, and whatever the flow runs as: a value the caller cannot read there gets this `404`, and a value that is not one id (or a list of ids, where the definition reads a list) is refused `400` `VALIDATION_FAILED` naming the input | Start the flow on a record the caller can read, and name the record's object (`objectName`) beside `recordId`. Hand an input the flow reads as a record id the id of a record the caller can read in the object the flow reads it from, in the shape the flow reads (one id, or a list of ids) |
2611+
| `404` | `RECORD_NOT_FOUND` | The start names a record (`recordId`, or the object's id alias such as `crmLeadId`) that the caller, not the system principal, cannot read in its own scope, or that does not exist: the two get one answer, the one the action door gives. Checked after the two rows above, at the same doors. A record key that is not one id, or a record named with no `objectName` for a flow declared `runAs: 'system'`, is refused `400` `VALIDATION_FAILED` naming the key. Where the flow's own definition reads a start input as the id of a record of a fixed object (a `get_record`, `update_record` or `delete_record` node with a literal `objectName` whose `filter.id` or `filter.id.$eq` is the input, or whose `filter.id.$in` is; a `screen` node with a literal `objectName` whose `recordId` is the input; a `subflow` or `map` node that hands the input to a callee input the callee reads so), that input is also loaded in the caller's own scope from THAT object, whatever object the start names, or none, and whatever the flow runs as: a value the caller cannot read there gets this `404`, and a value that is not one id (or a list of ids, where the definition reads a list) is refused `400` `VALIDATION_FAILED` naming the input. An input the flow declares a record id (`objectName` on the variable, [above](#a-start-input-that-is-a-record-id)) is loaded from the declared object the same way, whether or not the definition reads it | Start the flow on a record the caller can read, and name the record's object (`objectName`) beside `recordId`. Hand an input the flow reads as a record id the id of a record the caller can read in the object the flow reads it from, in the shape the flow reads (one id, or a list of ids) |
26102612
| `409` | `FLOW_DISABLED` | The flow exists but is switched off | Enable it — the identical request then succeeds |
26112613
| `422` | `FLOW_NO_START_NODE` | The stored definition has no `start` node | Fix the flow; retrying cannot help |
26122614
| `422` | `FLOW_INPUT_SCHEMA_INVALID` | A node's `config` violates the `inputSchema` its own definition declares | Fix the flow; the refusal is non-retryable — even under `errorHandling.strategy: 'retry'` the engine refuses once and never enters the retry loop |

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ const result = AutomationApiErrorCode.parse(data);
200200
| **isInput** | `boolean` | optional (default: `false`) | Is input parameter |
201201
| **isOutput** | `boolean` | optional (default: `false`) | Is output parameter |
202202
| **defaultValue** | `any` | optional | Value bound at run start when no parameter supplies one — this is what makes a declared variable always bound. An explicitly supplied param wins, including `false` and `null`; the boundary is `params[name] !== undefined`. |
203-
| **objectName** | `string` | optional | [planned] Declares this input a record reference: its value is the id of one record of the named object (the object's snake_case name, e.g. `crm_account`). Only on an input (`isInput: true`). The contract: every door that starts the flow by name (the trigger route, an action of type flow over REST and MCP run_action, a declared endpoint of type flow) loads a declared input from this object in the caller's own scope before the run exists; an id the caller cannot read there is refused 404 RECORD_NOT_FOUND, and a value that is not an id is refused 400 VALIDATION_FAILED naming the input. No door applies it yet: today the key is parsed and checked, and a start with an id the caller cannot read still runs. An input without this declaration is judged as a record only where the flow's own definition reads it as a record id. `type` stays the data type: the variable holds the id the caller sent. |
203+
| **objectName** | `string` | optional | Declares this input a record reference: its value is the id of one record of the named object (the object's snake_case name, e.g. `crm_account`). Only on an input (`isInput: true`). The contract: every door that starts the flow by name (the trigger route, an action of type flow over REST and MCP run_action, a declared endpoint of type flow) loads a declared input from this object in the caller's own scope before the run exists; an id the caller cannot read there is refused 404 RECORD_NOT_FOUND, and a value that is not an id is refused 400 naming the input. The load is added beside any read the flow's own definition makes of the input, never in place of one. An input without this declaration is judged as a record only where the flow's own definition reads it as a record id. `type` stays the data type: the variable holds the id the caller sent. |
204204

205205
### Nested Shape: `CreateFlowRequest.nodes[number]`
206206

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const result = FlowSchema.parse(data);
6161
| **isInput** | `boolean` | optional (default: `false`) | Is input parameter |
6262
| **isOutput** | `boolean` | optional (default: `false`) | Is output parameter |
6363
| **defaultValue** | `any` | optional | Value bound at run start when no parameter supplies one — this is what makes a declared variable always bound. An explicitly supplied param wins, including `false` and `null`; the boundary is `params[name] !== undefined`. |
64-
| **objectName** | `string` | optional | [planned] Declares this input a record reference: its value is the id of one record of the named object (the object's snake_case name, e.g. `crm_account`). Only on an input (`isInput: true`). The contract: every door that starts the flow by name (the trigger route, an action of type flow over REST and MCP run_action, a declared endpoint of type flow) loads a declared input from this object in the caller's own scope before the run exists; an id the caller cannot read there is refused 404 RECORD_NOT_FOUND, and a value that is not an id is refused 400 VALIDATION_FAILED naming the input. No door applies it yet: today the key is parsed and checked, and a start with an id the caller cannot read still runs. An input without this declaration is judged as a record only where the flow's own definition reads it as a record id. `type` stays the data type: the variable holds the id the caller sent. |
64+
| **objectName** | `string` | optional | Declares this input a record reference: its value is the id of one record of the named object (the object's snake_case name, e.g. `crm_account`). Only on an input (`isInput: true`). The contract: every door that starts the flow by name (the trigger route, an action of type flow over REST and MCP run_action, a declared endpoint of type flow) loads a declared input from this object in the caller's own scope before the run exists; an id the caller cannot read there is refused 404 RECORD_NOT_FOUND, and a value that is not an id is refused 400 naming the input. The load is added beside any read the flow's own definition makes of the input, never in place of one. An input without this declaration is judged as a record only where the flow's own definition reads it as a record id. `type` stays the data type: the variable holds the id the caller sent. |
6565

6666
### Nested Shape: `Flow.nodes[number]`
6767

@@ -231,7 +231,7 @@ const result = FlowSchema.parse(data);
231231
| **isInput** | `boolean` | optional (default: `false`) | Is input parameter |
232232
| **isOutput** | `boolean` | optional (default: `false`) | Is output parameter |
233233
| **defaultValue** | `any` | optional | Value bound at run start when no parameter supplies one — this is what makes a declared variable always bound. An explicitly supplied param wins, including `false` and `null`; the boundary is `params[name] !== undefined`. |
234-
| **objectName** | `string` | optional | [planned] Declares this input a record reference: its value is the id of one record of the named object (the object's snake_case name, e.g. `crm_account`). Only on an input (`isInput: true`). The contract: every door that starts the flow by name (the trigger route, an action of type flow over REST and MCP run_action, a declared endpoint of type flow) loads a declared input from this object in the caller's own scope before the run exists; an id the caller cannot read there is refused 404 RECORD_NOT_FOUND, and a value that is not an id is refused 400 VALIDATION_FAILED naming the input. No door applies it yet: today the key is parsed and checked, and a start with an id the caller cannot read still runs. An input without this declaration is judged as a record only where the flow's own definition reads it as a record id. `type` stays the data type: the variable holds the id the caller sent. |
234+
| **objectName** | `string` | optional | Declares this input a record reference: its value is the id of one record of the named object (the object's snake_case name, e.g. `crm_account`). Only on an input (`isInput: true`). The contract: every door that starts the flow by name (the trigger route, an action of type flow over REST and MCP run_action, a declared endpoint of type flow) loads a declared input from this object in the caller's own scope before the run exists; an id the caller cannot read there is refused 404 RECORD_NOT_FOUND, and a value that is not an id is refused 400 naming the input. The load is added beside any read the flow's own definition makes of the input, never in place of one. An input without this declaration is judged as a record only where the flow's own definition reads it as a record id. `type` stays the data type: the variable holds the id the caller sent. |
235235

236236

237237
---

0 commit comments

Comments
 (0)