diff --git a/.changeset/29418-record-node-operand-value-docs.md b/.changeset/29418-record-node-operand-value-docs.md new file mode 100644 index 00000000000..613aec2d65b --- /dev/null +++ b/.changeset/29418-record-node-operand-value-docs.md @@ -0,0 +1,11 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): objectstack-automation says a flow's record nodes treat a computed filter operand as a value, never as a query operator + +Clause-②: no +Metadata-protocol: none + +- **Now.** § Filter operands states the rule from the service-automation change: a computed operand is a string, number, bigint, boolean or date, or at an `$in` / `$nin` / `$between` comparand a list of them with no empty entry. Anything else fails the node as a guard before the query runs. The fix: make the expression evaluate to the compared value (for a record, its `id`), and write conditions in the filter itself. The table of failures a `fault` edge does not route now names that refusal beside the `null` operand. +- `SKILL.md` stays under its token ceiling (no ceiling moves). No schema, key, command or behaviour changes. diff --git a/content/docs/automation/flows.mdx b/content/docs/automation/flows.mdx index 82691bcd986..95862d871a2 100644 --- a/content/docs/automation/flows.mdx +++ b/content/docs/automation/flows.mdx @@ -431,6 +431,12 @@ than guarding it to `null`. To match an empty field, write the literal `null` operator map, `{ dialect: { $eq: 'x' } }`, since an object naming a string `dialect` at an operand is read as an envelope. +**A computed operand is a value, never a query operator**: a string, number, +bigint, boolean or date, or at an `$in` / `$nin` / `$between` comparand a list +of them with no empty entry. Anything else fails the node as a guard before the +query runs. Make the expression evaluate to the compared value (for a record, +its `id`), and write conditions in the filter itself. + **Where an envelope is data, the remedy names none there.** An envelope evaluates only as the top-level value of a value slot — `fields`, `assignments`, a callee's `input` / `inputs`, a screen's `defaults`, @@ -1983,7 +1989,8 @@ succeed, **and** the fix is to edit metadata. In practice that is a missing required config key (a data node with no `objectName`, an `http` node with no `url`, a `subflow` or `map` with no `flowName`, a `connector_action` with no `connectorId`/`actionId`), a filter operand that evaluated to nothing (`null`), -so the condition would have widened the query, a graph that recurses past the nesting +so the condition would have widened the query, a filter operand that evaluated to +something other than a value, a graph that recurses past the nesting ceiling, or a run that would execute unscoped. The split is deliberate. A dropped filter condition does not narrow a query, it diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index d0fe8c444f1..a8958938451 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -94,7 +94,7 @@ Flows are built from **built-in node types** (plugins register more): | Node | Purpose | |:-----|:--------| | `http` | Call an external HTTP API — always set `timeoutMs` (unset = no timeout). `url` and `headers` are served as written with the definition to every member who can read flows — only `signingSecret` and a start node's `secret` are withheld — so never a credential there: a credential means a declared connector called via `connector_action` | -| `notify` | Send a notification through the messaging service (inbox by default) | +| `notify` | Send a notification through the messaging service | | `connector_action` | Dispatch an action of a declared connector (§ *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is a value-slot map like `subflow` `input`: `registerFlow` and `os validate` refuse a `{…}` token at `connectorConfig.input.`. Once evaluated, it is judged against the action's `inputSchema` (`required`, `type`, `properties`, `items`) before dispatch: a breach fails the step `VALIDATION_ERROR` (`fault`-routable), nothing sent; `os validate` does not judge the `inputSchema` fit yet | | `script` | Call a **registered** function named by `config.function` (see *Valid-but-silently-wrong* #3) | | `screen` | Display a UI form to the user (screen flows only). `config.fields[]`: `{ name, label, type, required, options, defaultValue }`, `type` an open widget hint; `'file'` renders the upload widget and resumes with the `sys_file` id bound to `name` — a later `create_record` / `update_record` writes it into a `file` field, which claims the file: collect first, write after the last screen. `'record_link'` (never `'record'`, an object field type) takes no input — it shows the `reference` record whose id `defaultValue` evaluates to, by display name, as a link to its record page: the result screen of a flow that created records, `{ name: 'account_link', type: 'record_link', reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } }` over a `create_record` `outputVariable: 'account'`. Both keys required; `required` / `options` / `placeholder` / `min` / `max` refused; a resume value under its name refused (`400 INVALID_SCREEN_INPUT`). A completed `end` `message` is refused and a markdown link in a screen `description` renders literally. Next / Submit and "Step N of M" are not authored: the engine reads the graph when the screen pauses and serves `progress` (`nextScreen: 'none' | 'possible' | 'certain'`; `step` / `stepCount` only when every path agrees), which alone labels the button and draws the counter. A `decision`, `fault` edge, loop back, non-screen pause (`approval`, `wait`) or `subflow` after a screen answers `possible` — "Next", no count. `progress` in a node `config` is refused; never hand-roll a button label or step counter into `fields` | @@ -148,7 +148,7 @@ type: 'text', isInput: true }, { name: 'approval_result', type: 'boolean', isOut | ROUTES (runtime failure) | Does NOT route (fatal either way) | |:--|:--| -| 404, rate-limit, rejected write, failed subflow | missing required config key (`objectName`, `url`, `flowName`, `connectorId`/`actionId`); filter operand that evaluated to `null`; graph past the nesting ceiling; unscoped run | +| 404, rate-limit, rejected write, failed subflow | missing required config key (`objectName`, `url`, `flowName`, `connectorId`/`actionId`); filter operand that evaluated to `null` or a non-value; graph past the nesting ceiling; unscoped run | > **Writing a `readonly` field? Set `runAs: 'system'`.** Under the default > `runAs: 'user'` the engine **strips** it from `create_record` / `update_record` @@ -195,7 +195,7 @@ type: 'text', isInput: true }, { name: 'approval_result', type: 'boolean', isOut ### Filter operands (`config.filter`) - **Each operand is a value slot** — a literal or a CEL envelope evaluated - before the query (`filter: { id: { dialect: 'cel', source: 'record.id' } }`). + before the query. A whole-string **query placeholder** (`{current_user_id}`, `{today}`, `{current_year_start}`) passes through verbatim for the query engine; a flow variable never shadows it, and only `filter` gets this hand-off. @@ -206,6 +206,11 @@ type: 'text', isInput: true }, { name: 'approval_result', type: 'boolean', isOut literal `null`. A relation hop (`record.owner.email`) needs the start node's `config.expand` to list the field — else the fault names the missing hop, not the remedy. +- **A computed operand is a value, never a query operator**: a string, number, + bigint, boolean or date, or at an `$in` / `$nin` / `$between` comparand a list + of them with no empty entry. Anything else fails the node as a guard before the + query runs. Make the expression evaluate to the compared value (for a record, + its `id`), and write conditions in the filter itself. --- @@ -226,7 +231,6 @@ write them right the first time: CEL refuses an absent key where the template wrote nothing, so nothing / `null` / a default is now YOUR call; `'{record.tags.0}'` → `'record.tags[0]'`; a deep path guards each step: `has(vars.source) && has(vars.source.id) ? vars.source.id : null` - - dates and money: the legacy → CEL table in **objectstack-formula** - run user: `{$User.Id}` → `current_user.id`; a flow that can run without a user guards it, `current_user != null ? current_user.id : null`. `current_user` holds `id`, `positions`, `organizationId`, `isPlatformAdmin` @@ -280,10 +284,6 @@ write them right the first time: } }); ``` -4. **Conditions are bare CEL; only the stdlib (`CEL_STDLIB_FUNCTIONS`, - **objectstack-formula**) is callable bare.** An unknown function (`PRIOR()`, a - typo) and a `{…}`-wrapped field ref both **fail the build** — write `record.x`. - --- → State Machines & Approvals: [references/state-machines-and-approvals.md](./references/state-machines-and-approvals.md) — its state-introspection route is `GET /api/v1/meta/object/:name/state/:field?from=:state`.