Skip to content

Commit ac8f2c5

Browse files
fix(spec)!: close the shared retry policy, and judge try_catch config keys at the build doors (#22380)
Fixes #22343 Clause-②: no (narrowing: an undeclared key on the shared retry policy is refused at parse wherever it is written, a job's retryPolicy and a try_catch node's retry, and an undeclared try_catch config key at the build doors and the save door, where each passed) The claim (`6069618073`) declared `yes`. A pure narrowing is `no` (`references/execution-duties.md:105`), so the seat corrected the spelling after the contract review `6072339018`; the parenthesis is unchanged. It is worded to what the census decided: no writer relies on the strip, so the shared `RetryPolicySchema` is closed and every one of its parsers narrows, not the `try_catch` slot alone. Per triage `6068085630`, the importers named that way are `job.retryPolicy` and a `try_catch` node's `retry`. ## What changes - **Spec, `shared/retry-policy.zod.ts`.** `RetryPolicySchema` is a `strictObject` (it was a plain `z.object`). A key it does not declare is refused at parse, naming the key, with a curated near-miss table: `maxRetry`, `retries` and `attempts` → `maxRetries`; `initialDelayMs` and `baseDelayMs` → `backoffMs`; `maxDelayMs` → `maxRetryDelayMs`. A `maxAttempts` gets a prescription instead of an alias, because it counts the first attempt. The `retryDelayMs` tombstone stays, with its rename. The docblock records the census. - **Spec, `automation/flow-node-config-refusals.ts`.** `BUILTIN_KEYS_JUDGED_AT_REGISTRATION` is deleted. `try_catch` was its only member, and an empty exception set would be dead code. `builtinNodeConfigKeysJudged(type)` is now `getBuiltinNodeConfigContracts().has(type)`, so the key arm judges all 13 builtins. The key arm also refuses a tombstoned key the author wrote, at its own path and in the tombstone's words. `script` is excluded (`RETIRED_KEYS_JUDGED_ELSEWHERE`), because the lint names its retired keys and no door before the run ever judged them. See "Route change" below. - **`service-automation` `engine.ts`: comments only.** `validateNodeConfigKeys` already asks `builtinNodeConfigKeysJudged`, so it now stands aside for `try_catch` and judges plugin node types only. Its docblocks are corrected. - **Ledger.** A step-18 D3 entry, `try-catch-and-retry-policy-undeclared-keys-refused`, plus a `STEP18_RATIONALE` fragment with order 90; the highest order on `main` was 89. `migrations/registry.ts` was regenerated with `gen:migration-registry`. The step-18 entry `flow-builtin-node-config-undeclared-keys-refused` and its fragment said `try_catch` "stays registration's", which this change makes false in the same unreleased step, so both now point at the new entry. - **Changeset.** `.changeset/22343-retry-policy-try-catch-undeclared-keys-refused.md` bumps `@objectstack/spec` `minor` (BREAKING, ADR-0087 `registered`) under the same pre-mode convention as `2f70c2222` (`.changeset/pre.json`: mode `pre`, tag `next`). It carries the Clause-② line above. It has no `@objectstack/service-automation` entry: that package's diff is comments plus one test, and no gate asked for one (`check-changeset-no-major`, `check-adr-0087-registration` and `check-empty-changeset` are green). - **Docs.** `content/docs/automation/flows.mdx`: the `config` row and the strictness callout now list `try_catch` among the judged builtins, and one sentence says `retry` is closed. `jobs.mdx` gets one sentence saying `retryPolicy` is closed. The strictness ledger's audit row (`docs/audits/2026-07-unknown-key-strictness-ledger.md`) said the policy "is still non-strict", which is no longer true, so it is corrected. - **In-place fix, `automation/flow.zod.ts` (seat order `6071818310`).** `Flow.errorHandling`'s refusal of `maxAttempts` said a bare rename to `maxRetries` would run "one attempt fewer" than asked for. It runs one more: `maxAttempts: 3` is 3 runs, and `maxRetries: 3` is 1 + 3 = 4. The clause now says "one attempt more". The prescription `maxRetries: <maxAttempts - 1>` is unchanged. The `flow.test.ts` case is now the pin, and its comment, which carried the same inversion, is corrected. The `RetryPolicySchema` pin in `retry-policy.test.ts` gains the same two assertions, so both surfaces assert the same four things: the prescription, "one attempt more", never "fewer", and no bare rename. The changeset gains one `Also corrected:` line. ## Census (H1), at `origin/main` `b7fdd5085` Parsers of `RetryPolicySchema`: | site | parses | authored by | |:--|:--|:--| | `system/job.zod.ts:366` `JobSchema.retryPolicy` | yes | stack `jobs` | | `automation/control-flow.zod.ts:330` `TryCatchConfigSchema.retry` | yes (the executor, and the key/value arms) | flow `try_catch` nodes | | `automation/flow.zod.ts:1224` `Flow.errorHandling` | spreads `retryPolicyShape()` into its own `strictObject`, already closed | unaffected | | `integration/connector-fetch-policy.ts:49` | comment only | no parse | | `contracts/job-service.ts:131` `JobRetryPolicy` | TS interface mirror | no parse | | `service-job` `run-with-policy.ts:17` | mirrors the defaults, reads an already-parsed policy | no parse | Writers checked for a key outside the five (`maxRetries`, `backoffMs`, `backoffMultiplier`, `maxRetryDelayMs`, `jitter`): - **In this repo:** `examples/app-showcase` jobs (`:22`) and flows (`:1212`, `:1425`), `examples/app-todo`, `content/docs/automation/flows.mdx` (`:798`, `:1856`) and `jobs.mdx` (`:25`, `:243`), the `service-automation` and `service-job` READMEs, every test fixture that writes `retry:` under a `try_catch` or a job `retryPolicy:` (lint, service-automation, service-job, spec), and the `retry-policy-converged` conversion's output and fixtures. **0 undeclared keys.** - **objectui at the pinned `a58626c88d`:** `preview-samples.ts:331` (`retryPolicy: { maxRetries: 3 }`); the flow inspector's fallback `try_catch` form, which writes `errorVariable` only; and the descriptor-driven form, which reads the descriptor that closes `retry` to the five keys. **0 undeclared keys.** - **Out of reach:** no connector manifest, seed or `sys_metadata` writer of this shape was found. hotcrm and deployed metadata were not measured. **Verdict:** no writer relies on the strip, so the shared schema is closed. No writer sits outside `packages/spec`, `service-automation` or `service-job`, so the stop condition does not apply. ## Hypotheses, measured - **H1, confirmed.** See the census above. - **H2, confirmed, with one addition (see "Route change").** With `retry` strict, the key arm judges `try_catch` like the other 12. Docblocks that named the exception were corrected in `flow-node-config-refusals.ts` (module header, the value-arm carve-out, the predicate, the main arm's bullet, the inline comment), in `flow.zod.ts` (two comments), in `engine.ts` (three comments), and in the two spec test headers. - **H3, confirmed.** A probe over `installBuiltinNodes` lists the 17 builtin descriptors with their `configSchema` and judged state. Before: the walk still reached `["try_catch"]`. After: `[]`. The remaining builtins publish no `configSchema` (`decision`, `wait`, `connector_action`) or are exempt (`assignment`). "Refused once" is pinned in `config-unknown-keys.test.ts`: one parse issue, and no `undeclared config key(s)` text from the walk. - **H4, falsified as stated: the did-you-mean needed a curated row.** The distance fallback cannot reach `maxRetry`. It is 3 edits from `maxRetries` (y→i, +e, +s) against a budget of 2 for an 8-letter key: `findClosestMatches('maxRetry', …, 2)` returns `[]`. The measured door first printed the refusal with no did-you-mean. It now answers `` `maxRetry` → `maxRetries` `` through an `aliases` row, the mechanism `Flow.errorHandling` already uses for the same vocabulary. The refusal is located (`nodes.N.config.retry.maxRetry`, and `jobs.N.retryPolicy` for a job) and uses the `node-config-refused-by-contract` shape from #21982. ## Route change: the `retryDelayMs` tombstone (registration would otherwise have widened) The key arm judged `unrecognized_keys` only, and both arms skip a tombstone (`invalid_type` expecting `never`). The `retry-policy-converged` conversion renames `retryDelayMs` to `backoffMs`, but it keeps both spellings when the values differ, and it leaves a `null`. The walk refused what survived, as an undeclared key. With the walk standing aside and the arms unchanged, `registerFlow` would have accepted those two variants and the run would have refused them. So the key arm now refuses a tombstoned key the author wrote on every judged type. `script` is the exception, so this change narrows nothing on any other builtin: only `script` and `try_catch` carry `retiredKey()` in their contracts, measured by grep over the five contract modules. The ruling's intent ("one judge", and `registerFlow` widens nowhere) is what decided this. It is named here for the contract review. ## Registration verdicts, before and after (10-variant probe on the built packages) | `try_catch` config | before: `registerFlow` | after: `registerFlow` | |:--|:--|:--| | declared `retry` | registers | registers | | `retry.bogusKey` | refused, walk | refused, parse at `nodes.1.config.retry.bogusKey` | | `retry.maxRetry` | refused, walk | refused, parse at `nodes.1.config.retry.maxRetry` | | `retryDelayMs` alone | registers (converted) | registers (converted) | | `retryDelayMs` equal to `backoffMs` | registers (twin dropped) | registers (twin dropped) | | `retryDelayMs` differing from `backoffMs` | refused, walk | refused, parse at `…retry.retryDelayMs` | | `retryDelayMs: null` | refused, walk | refused, parse at `…retry.retryDelayMs` | | top-level `bogusKey` | refused, walk | refused, parse at `nodes.1.config.bogusKey` | | `try.bogusKey` (region) | refused, region check | refused, region check | | `retry: 5` | refused, parse | refused, parse | Every variant registers or is refused exactly as before; only the judge moved. A direct `FlowSchema.parse` or `defineFlow()` (no conversion) now meets the tombstone for a pre-17 `retryDelayMs`, like every other retired spelling. ## Measured at the CLI door Fixture projects (`defineStack(…, { strict: false })`), at `9f71db481`, with the built CLI closure: | fixture | `os validate --json` | |:--|:--| | `try_catch` `retry: { maxRetry: 2 }` | exit 1, `custom` at `flows.0.nodes.1.config.retry.maxRetry` | | job `retryPolicy` with `maxRetry: 3` | exit 1, `unrecognized_keys` at `jobs.0.retryPolicy` | | every declared key | exit 0 | | pre-17 `retryDelayMs` alone | exit 0 (`retry-policy-converged` converted it) | `examples/app-showcase` validates clean (exit 0, `valid: true`; 2 `try_catch` with `retry`, 1 job with `retryPolicy`), and so does `examples/app-todo` (exit 0). ## Ablation At HEAD `9f71db481`, `RetryPolicySchema` was mutated back to a plain `z.object` through `scripts/ablation-replace.mjs`: anchor 1 → 0, blob `c06e5bd1a6` → `dbd76790f2`. The spec was rebuilt, and `ablation-dist-preflight` found the marker present in 20 built files. Results: - validate-door pin (`packages/cli/test/retry-policy-key-validate-door.test.ts`): **1 failed, 1 passed**. The refusal went red (exit 0 where exit 1 was expected) and the control held. - spec pins: 6 failed, 49 passed. - registration pins: 2 failed, 14 passed. These include "registerFlow widens nowhere": with the strict policy reverted, the arm sees no unknown key and the walk stands aside, so the mutation is exactly the widening the card names. Restore: the blob is back to `c06e5bd1a6` == HEAD and `git diff HEAD` is empty. After a rebuild, the preflight with `--absent` found the marker in none of 232 built files and the tree clean, and the door pin was green again (2 passed). ## Tests (real readings) At `b7fdd5085` (merged with `origin/main` `6a53564b9`): - `@objectstack/spec`: 626 files, 18751 passed, 1 todo. - `@objectstack/service-automation`: 178 files, 2179 passed. - `@objectstack/lint`: 128 files, 5853 passed. - CLI door pin, `--project integration`: 2 passed. - typecheck for `spec`, `service-automation` and `cli`: exit 0. The test-layer debt is unchanged (spec 52 files / 246 errors held, cli 3 / 28). At `c915191dc` (the in-place fix round, no merge of `main`): `@objectstack/spec` `src/automation` plus `retry-policy.test.ts`, 34 files, 1134 passed, and the spec typecheck exits 0 (debt held at 52 / 246). Ablation: putting back "fewer" with `scripts/ablation-replace.mjs` turned the `flow.test.ts` pin red (1 failed). The blob was restored to `c4c6c4453d` == HEAD. At `a77cb02b1`: `@objectstack/service-job` 11 files, 117 passed. The `@objectstack/cli` unit tier, 270 files, 3968 passed; it includes the tier-partition pin, and the new door pin lands in `integration` by behaviour. It runs per PR because it is not named `.e2e`. Narrowed eslint (`--no-inline-config`) over the 12 changed `.ts` files: 12 files, 0 errors, 0 warnings, none ignored. This config enables no type-aware linting (`eslint.config.mjs:327`), so the diff cannot move any untouched file's verdict. The repo-wide `pnpm lint` is CI's. ## Gates `node scripts/pm/dispatch-gates.mjs --commands` at `b7fdd5085` derived 115 commands, the same list as at `a77cb02b1`, and all 115 ran with exit 0. The `--ran` verdict: `115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED (a DERIVED zero — all 115 recorded an exit code and none of them is 3)`. At `c915191dc`, the round's 4 paths derived 85 commands, every one of them already among the 115. The full derivation is unchanged at 115. All 85 exited 0 (`--ran`: 85 run, 0 NOT-MEASURED). Ten dist-reading gates first exited 3 on a fresh worktree, then exited 0 after a build. Two gates needed a second run: - `check:type-check-debt` hit the per-command 300 s cap. Re-run alone, it exited 0 in 123 s. - At `a77cb02b1`, five spec gates exited 3 (spec `dist` predated a test edit) and `check:dual-build-cjs-loads` exited 3 (8 packages had no `dist`). After building them, all six exited 0. The roster gates whose roster sits under a touched directory also exited 0: `check:meta-url-spelling`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. ## Acceptance notes - `Flow.errorHandling` gets no did-you-mean for `maxRetry`, the same distance gap closed here on the shared policy. Its own alias table has no such row. It is a sibling surface outside this card's scope, and the key is still refused loudly. Carrier: none. - A `script` node carrying a retired dispatch key (`actionType`, …) is refused at error, with exit 1, by `os validate` and `os lint` (rule `expression-invalid`, measured at `6fbc005a6`). The save door answers 200 and stores the body with the key removed: the retired conversion `flow-node-script-branch-keys-removed` replays on the save path and logs `OS_METADATA_CONVERTED`. So nothing reaches the run from either door. `defineStack` and a direct `registerFlow` were not measured. `RETIRED_KEYS_JUDGED_ELSEWHERE` keeps the key arm out of `script` deliberately. Carrier: none (finding 9 of the contract review `6072339018`, measured in round 3 `6073081304`). - Two runtime test comments still say the registration walk judges "only those (and `try_catch`)": `packages/runtime/src/domains/automation-put-post-error-parity.test.ts:50` and `automation-register-error-class.test.ts:79`. After this PR, the walk judges plugin node types only. They are comments in test files that do not ship. Carrier: the next edit of either file. ## Line budget 17 files, +846 / −143 against the merge base (the in-place fix round: 4 files, +13 / −4; round 3: the changeset's `Clause-②:` value, +1 / −1). 0 governed paths. --- _Generated by [Claude Code](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fdfdd7e commit ac8f2c5

17 files changed

Lines changed: 846 additions & 143 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
The shared retry policy refuses a key it does not declare, and the build doors now judge a `try_catch` node's config keys like every other builtin's: a `retry.maxRetry` slip under a `try_catch` node, or on a job's `retryPolicy`, no longer passes `objectstack validate` and `objectstack compile` to be dropped or refused later.
6+
7+
Clause-②: no (narrowing: an undeclared key on the shared retry policy is refused at parse wherever it is written, a job's retryPolicy and a try_catch node's retry, and an undeclared try_catch config key at the build doors and the save door, where each passed)
8+
9+
<!-- adr-0087: registered try-catch-and-retry-policy-undeclared-keys-refused -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
**Why.** `RetryPolicySchema` — the one declaration behind `job.retryPolicy` and a `try_catch` node's `retry` — was a plain object schema, so it dropped a key it did not declare. Its defaults are opt-in (`maxRetries: 0`, `backoffMultiplier: 1`), so a dropped key fell back to "no retry" or to a flat delay: a job with `retryPolicy: { maxRetry: 3 }` parsed, deployed and never retried, and nothing said so. On a `try_catch` node the same strip kept the flow parse from judging the node's keys at all: its node type descriptor closes `retry` to five keys, so only `registerFlow`'s descriptor walk refused such a key, after `objectstack validate` and `objectstack compile` had passed it.
14+
15+
**What is refused.**
16+
17+
- **A key the retry policy does not declare**, on a job's `retryPolicy` or under a `try_catch` node's `retry`. The policy declares `maxRetries`, `backoffMs`, `backoffMultiplier`, `maxRetryDelayMs` and `jitter`. The refusal names the key and, for a near miss or a spelling borrowed from a neighbouring retry vocabulary, the declared key to write (`maxRetry` → `maxRetries`, `maxDelayMs` → `maxRetryDelayMs`, `baseDelayMs` or `initialDelayMs` → `backoffMs`, `retries` → `maxRetries`). A `maxAttempts` is answered with the off-by-one it carries: write `maxRetries` one lower. On a job it is the issue `unrecognized_keys` at `jobs.N.retryPolicy` (`defineStack` → `STACK_SCHEMA_INVALID`, 422; an artifact carrying it is refused whole at load).
18+
- **A key a `try_catch` node's executor contract does not declare** — beside `try`, `catch`, `errorVariable` and `retry`, or under `retry` — at every door that parses a flow: `FlowSchema`, `defineFlow()`, `defineStack`, `objectstack validate`, `objectstack compile`, an artifact's parse, the metadata save door and `registerFlow`. It is the existing closed-set code `node-config-refused-by-contract`, `params: { nodeType: 'try_catch', key }`, anchored at the key (`nodes.N.config.retry.maxRetry`), in the contract's words and closed with the rename-or-remove remedy, from the same judge (`flowNodeConfigRefusals`) that already refused an undeclared key on every other builtin. `builtinNodeConfigKeysJudged('try_catch')` now answers `true`.
19+
- **A `retryDelayMs` the load-path conversion leaves behind** under a `try_catch` node's `retry`: the conversion renames the pre-17 spelling to `backoffMs`, but keeps it when a `backoffMs` beside it holds a different value, and leaves a `null`. That copy is refused at `nodes.N.config.retry.retryDelayMs` in the tombstone's own words (rename it to `backoffMs`). Registration refused both before, as undeclared keys.
20+
21+
**What stays as it was.**
22+
23+
- Registration refuses exactly what it refused before. The descriptor walk (`validateNodeConfigKeys` in `@objectstack/service-automation`) stands aside for `try_catch` and now judges plugin node types only. A `try_catch` flow that carried such a key was already refused there, so a running flow changes nothing; the refusal now arrives as the parse's located issue instead of the walk's `Flow '…' rejected: N undeclared config key(s)` text.
24+
- A key on a `try_catch` region object (`try`, `catch`), or on a region's nodes and edges: the region check's, at registration, as before.
25+
- A pre-17 `retryDelayMs` alone: still renamed to `backoffMs` before the judge at every door that converts first (`defineStack`, `os validate`, `os compile`, `registerFlow`, the save door). Met by a direct `FlowSchema.parse` or `defineFlow()`, it meets its tombstone, like every other retired spelling.
26+
- `Flow.errorHandling`, which carries the same policy keys, was already closed; a `script` node's retired keys keep the scope they had.
27+
28+
**Also corrected:** `Flow.errorHandling`'s refusal of a `maxAttempts` key said a bare rename to `maxRetries` would run one attempt fewer than asked for; it runs one more (`maxAttempts: 3` is 3 runs, `maxRetries: 3` is 4), and the message now says so — the prescription, `maxRetries` one lower, is unchanged.
29+
30+
## FROM → TO
31+
32+
| you wrote | write instead |
33+
|:--|:--|
34+
| `maxRetry`, `retries` or `attempts` | `maxRetries` |
35+
| `maxAttempts: n` (counts the first attempt) | `maxRetries: n - 1` |
36+
| `baseDelayMs` or `initialDelayMs` | `backoffMs` |
37+
| `maxDelayMs` | `maxRetryDelayMs` |
38+
| `retryDelayMs` beside a `backoffMs` | delete it, keeping the `backoffMs` you meant |
39+
| a key nothing reads | delete it |
40+
41+
**The one-line fix: rename or delete the key the refusal names.**
42+
43+
**Who is affected, measured.** Before closing the policy, every writer of either parser was read: in this repository the showcase job and flows, the `app-todo` flows, the docs, the service READMEs, the test fixtures and the load-path conversion's output; in the pinned objectui the job preview sample and the flow designer, whose descriptor form closes `retry` to the same five keys. All of them write only declared keys, and `objectstack validate` over the showcase and `app-todo` examples stays clean. `service-job`'s `runWithPolicy` reads an already-parsed policy and parses nothing. hotcrm, deployed metadata and other repositories were not measured.
44+
45+
### The kit
46+
47+
- **The refusal.** `RetryPolicySchema` (`shared/retry-policy.zod.ts`) is a `strictObject` with a curated near-miss table; the key arm of `flowNodeConfigRefusals` in `automation/flow-node-config-refusals.ts` judges every builtin in the executor contract map, and refuses a tombstoned key the author wrote on every judged type but `script`. No new code joins `FLOW_SLOT_REFUSAL_CODES`.
48+
- **The ledger.** The D3 semantic entry `try-catch-and-retry-policy-undeclared-keys-refused` (protocol 18). No key is removed, so there is no new tombstone, and there is no D2 conversion: the platform cannot know what an undeclared key was meant to be.

‎content/docs/automation/flows.mdx‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ Each node performs a specific action in the flow.
138138
| `id` | `string` | ✅ | Unique node identifier — unique across the **whole flow**: the top-level `nodes[]` and every region body (`loop.body`, `parallel.branches[]`, `try_catch.try` / `.catch`, at any depth) share one id space, and `FlowSchema` refuses a reused id at parse (`Duplicate node id …`, naming both locations) |
139139
| `type` | `string` | ✅ | Node type — a built-in id from the table above **or** a plugin-registered one. Per ADR-0018 the spec does not gate this with a closed enum; it is checked against the live action registry once that registry is complete — plugins contribute node types while they start, so flows registered during boot are checked in one pass when the vocabulary closes (all plugins started), and anything registered after that (Studio publish, dev reload) is checked immediately. Unknown types warn, never reject; executing one fails with `NO_EXECUTOR` |
140140
| `label` | `string` | ✅ | Display label |
141-
| `config` | `object` | optional | Type-specific configuration — its node type owns its shape. On a built-in node type with an executor contract other than `try_catch`, a key that contract does not declare is refused at the flow parse, at the key's path; on `try_catch` and on a plugin node type, a key the published `configSchema` does not declare is rejected at `registerFlow()`. An `approval` node's config is judged whole against its declared contract at the flow parse, and the built-in executors `parse()` the value against their Zod contract before running |
141+
| `config` | `object` | optional | Type-specific configuration — its node type owns its shape. On every built-in node type with an executor contract, a key that contract does not declare is refused at the flow parse, at the key's path; on a plugin node type, a key the published `configSchema` does not declare is rejected at `registerFlow()`. An `approval` node's config is judged whole against its declared contract at the flow parse, and the built-in executors `parse()` the value against their Zod contract before running |
142142
| `connectorConfig` | `object` | ✅ on `connector_action` | `{ connectorId, actionId, input }` — the only input a `connector_action` node's executor reads. `FlowSchema` refuses a `connector_action` node without it, and one whose `connectorId` or `actionId` is blank (empty or whitespace only), at any depth including a region body. `connectorId` is the registered connector's `name`, `actionId` one of the action keys it declares; `input` is optional |
143143
| `position` | `{ x, y }` | optional | Visual position on canvas |
144144
| `timeoutMs` | `number` | optional | Per-node execution timeout |
@@ -151,16 +151,16 @@ The flow, node, edge, and variable **shells are `.strict()`** — a key they do
151151
declare is a parse error naming the likely intended key, not a silent strip. A
152152
node's `config` has no single shape at the flow-parse level (each node type owns
153153
its vocabulary), but it is not unchecked. On every built-in node type with an
154-
executor contract except `try_catch` — `get_record`, `create_record`,
155-
`update_record`, `delete_record`, `notify`, `http`, `screen`, `map`, `loop`,
156-
`parallel`, `script` and `subflow` — the flow parse refuses a config key that
154+
executor contract — `get_record`, `create_record`, `update_record`,
155+
`delete_record`, `notify`, `http`, `screen`, `map`, `loop`, `parallel`,
156+
`try_catch`, `script` and `subflow` — the flow parse refuses a config key that
157157
contract does not declare, at the key's own path and in the contract's words
158158
(its did-you-mean, or its prescription for a known slip such as `fieldValues` →
159159
`fields`), so `objectstack validate`, `objectstack compile`, the metadata save
160160
door and `registerFlow()` all refuse it before anything runs (a key on a `loop`
161-
body or a `parallel` branch, or on a region's nodes and edges, is the region
162-
check's at `registerFlow()`). A `try_catch` node
163-
and a plugin node type are judged at `registerFlow()` instead, against the
161+
body, a `parallel` branch or a `try_catch` region, or on a region's nodes and
162+
edges, is the region check's at `registerFlow()`). A plugin node type is judged
163+
at `registerFlow()` instead, against the
164164
`configSchema` the node type publishes — naming the path, the declared key set,
165165
and a did-you-mean; a plugin node type that publishes no `configSchema` declares
166166
nothing, so nothing can be undeclared. The contract-carrying builtins
@@ -800,6 +800,12 @@ events).
800800
}
801801
```
802802

803+
`retry` is the shared retry policy — the same five keys as a job's
804+
`retryPolicy` (see [Error handling](#error-handling) below) — and it is closed:
805+
a key it does not declare, such as `maxRetry` for `maxRetries`, is refused at
806+
`objectstack validate` with a did-you-mean, rather than dropped to a default
807+
that never retries.
808+
803809
`catch` is optional in the schema, and omitting it is the trap: with no `catch`
804810
the container **fails** when the `try` region fails, so the failure propagates
805811
exactly as if nothing had been wrapped (measured — the no-`catch` run and the

‎content/docs/automation/jobs.mdx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,11 @@ Two defaults worth knowing before you rely on the block:
391391
count still means *no retry*. State a count to opt in.
392392
- **`backoffMultiplier` defaults to `1`** — a flat delay, not exponential.
393393

394+
Those defaults are why the block is closed: a key it does not declare — `maxRetry`
395+
for `maxRetries`, a `maxDelayMs` borrowed from another retry vocabulary — is
396+
refused at `objectstack validate` with the declared key it was near, rather than
397+
dropped and replaced by a default that never retries.
398+
394399
`timeoutMs` is a **per-attempt** limit in milliseconds. An over-limit run is
395400
recorded with status `timeout` and, being a failure, is retried like any other.
396401
JavaScript cannot forcibly cancel a running function, so a `handler` attempt is

0 commit comments

Comments
 (0)