ObjectStackObjectStack

17.4.0

Release notes for 17.4.0 of the v17 line, published 2026-09-09. ⚠️ MACHINE DRAFT — mechanically derived from the changesets, not yet curated.

⚠️ This page is a machine draft, not curated release notes. It carries the same section shape as 17.3.0 and its siblings, so a reader lands where they expect — but only the shape is finished. The content was generated by de-duplicating the 396 changesets that make up 17.4.0 across all 61 package CHANGELOG.md files (a changeset that bumps six packages appears once here, not six times) and taking each one's own headline verbatim.

What has not happened. No editorial pass: nothing dropped for being internal, nothing merged into a themed narrative, nothing ranked. Deciding what is user-facing is a judgement read out of these entries, and it is the maintainer's at release time. The two sections that cannot be machine-derived — Highlights, and the upgrade checklist — say so where they stand rather than being filled with a guess.

How to use it. Prune. What survives, grouped and rewritten, is the release section.

Published 2026-09-09, taking over from 17.3.0 as the current version of the v17 line. 396 changesets across 61 packages: 35 carrying a breaking marker, 140 other minor entries, 221 patch entries.

Highlights — 17.4.0

Not written, and deliberately not guessed. Every other release page opens with a short list of what the release means for a reader — which of 396 changesets matter, and why. That is the one section no mechanical pass can produce: it is a judgement about audience, not a transformation of the entries below. Write it from the breaking list first.

What's new in 17.4.0

Breaking changes & migration in 17.4.0

⚠️ Mechanically detected, by a ! in the entry's own conventional-commit prefix or the literal BREAKING in its opening paragraph. ⛔ Neither the presence nor the absence of that marker was verified against the diff — an unmarked breaking change would not appear here, and this list is therefore a floor, never a clearance.

  • be75493BREAKING os lint --generator now refuses to run without --eval, instead of accepting the flag and ignoring it.
    cli
  • c8e5ac6 — retire os create example — it was a weaker os init plus a README, and the refusal now names os init (#16483, #15531)
    cli
  • cee3961BREAKING os create <type> <name> now refuses a project name that npm refuses, and refuses it before it writes anything.
    cli
  • e9fcd6b — a duration-shaped z.number() key carries its unit in the key name — hook.timeout / job.timeout / DriverOptions.timeouttimeoutMs, MetadataManagerConfig.cache.databaseLoader.ttlttlMs (the outer cache.ttl leaves outright under #15624 — nothing read it), tenant idleTimeout / sessionTimeout*Seconds; new gate check:duration-unit-keys (#14478, #14519)
    cli, core, driver-memory, driver-turso, metadata, objectql
  • ffe058aBREAKING os lint --eval --generator "" now refuses instead of quietly running the offline eval, matching the rule the same flag already follows without --eval.
    cli
  • 7092d63 — the organizations.* family declares the wire shapes better-auth actually sends — nineteen published Promise< any > returns narrowed, twenty ledger entries closed (#14314)
    client
  • 7beaaa3oauth.applications.delete resolves on the zero-byte 200 its route answers, instead of rejecting on every successful delete (#15451)
    client
  • b1b978c — the auth.* family declares the wire shapes better-auth actually sends — thirteen published Promise< any > returns narrowed (#14313)
    client
  • e944fdb — the oauth.* family declares the wire shapes better-auth actually sends — four published Promise< any > returns narrowed (#14312)
    client
  • 10d05bb — an array comparand is refused instead of compared by reference, and a Date comparand is compared by time value
    driver-memory, spec
  • 78bc4adfindWithWindowFunctions() presents its rows like every other read door — a declared boolean answers true, not 1 (#16609)
    driver-sql
  • 0145680timeout beside an UPPERCASE WSS:// / WS:// url in forced remote mode is refused at construction, closing the last corner of the same gap (ADR-0049 enforce-or-remove)
    driver-turso
  • 5071310timeout beside a pre-configured client in remote mode is refused at construction instead of being accepted and never delivered (ADR-0049 enforce-or-remove)
    driver-turso
  • bc0ac1dtimeout beside a wss:// / ws:// url is refused at construction instead of being accepted and never delivered (ADR-0049 enforce-or-remove)
    driver-turso
  • ed5d557timeout bounds remote operations; localPath and wasm leave the published config schema (#16024, ADR-0049 enforce-or-remove)
    driver-turso, spec
  • b398ad2BREAKING (behaviour): a static readonly field is now stripped from a non-system caller's INSERT payload inside engine.insert, exactly as it already was on engine.update. A non-system create that used to write a read-only column now has that column dropped, reported through onFieldsDropped / droppedFields, logged at warn, and refused outright under strictReadonlyWrites. Seeding a read-only column at create time is a system act — use context.isSystem, a flow's runAs: 'system', a system hook or a seed.
    lint, metadata-protocol, objectql, service-automation, service-settings, spec
  • e0af1a8<ListView objectName> / <ListView viewType> are retired from the react-tier component contract — data={{ provider: 'object', object }} / type are the only spellings (#14791)
    lint, spec
  • 7629f4d — retire the three inert outer keys of MetadataManagerConfig.cacheenabled, ttlSeconds (formerly ttl) and maxSize — read by nothing; cache.databaseLoader is the only live half (#15624, ADR-0049)
    metadata, spec
  • 65846bc — a batch ROW reports a unique-constraint refusal as UNIQUE_VIOLATION — the same wire spelling as the whole-request failure on the same route (#14723)
    metadata-protocol, objectql, rest
  • 8e0b297positions[] on the session payload is the SECURITY axis, not the better-auth […] scalar (one word elided: ADR-0090 D3 reserved word) (#15136)
    plugin-auth, spec
  • afa3a26 — a non-member schedule timezone no longer discards the cron expression, and a schedule already holding one stops instead of firing on a cadence nobody asked for (#16291)
    plugin-reports
  • 233222etimeDimensions[].dateRange's string arm closes to the date-range preset vocabulary; any other string is refused with 400 ANALYTICS_DATE_RANGE_UNRECOGNIZED (#16041)
    runtime, spec
  • 21aabbcFlowSchema refuses a region node whose id is already declared elsewhere in the flow — one node-id space across the top-level nodes[] and every region body (#16134)
    spec
  • 414c1fcComponentPropsMap['element:record_picker'].filter converges onto the ViewFilterRule array form — the last record-form filter in the map (#14406, objectui#6206 Option B)
    spec
  • 52804cdFlowSchema refuses a flow whose edges[] declares the same id twice (#14964)
    spec
  • 53cf263FlowSchema refuses a flow whose top-level nodes[] declares the same id twice (#15713)
    spec
  • 64bd6a3composeStacks objectConflict: 'merge' refuses object pairs whose object-level collections cannot be merged (#14848)
    spec
  • 6f1ce7d — a text operator over a field whose DECLARED type can never store a string is refused at the engine's field-aware door — the contract rows (#15661)
    spec
  • 8f404a5plugins / devPlugins are artifact envelope keys — excluded from the assembled package body and refused inside packages[] (#15219)
    spec
  • 9c270bbGanttConfigSchema / TreeConfigSchema refuse undeclared keys — both .passthrough() windows are closed and the ten gantt members plugin-gantt read through the window are declared (#15469)
    spec
  • c463d03 — retire the incident-response, training and change-management families whole — nineteen defs and every name they exported — and the ESignatureConfig deadline pair (#15513, #14477, ADR-0049)
    spec
  • ef3a138 — an evaluated expression slot requires a non-blank sourceEvaluatedExpressionSchema, composed by the assignment value envelope (#15430)
    spec
  • f81afe3 — a typed expression slot fixes its dialect on the envelope arm too, and refuses a blank string (#15028, #15035)
    spec
  • fb77aa5 — a tree field's reference, when present, must name the declaring object — any other target is refused at parse (#14892)
    spec
  • 4f85e4d — the record handed to a record-change flow no longer aliases the write's payload (#14744)
    trigger-record-change

New capabilities in 17.4.0

Every remaining minor-bumping changeset, verbatim.

  • 000fd05os generate migration now emits the character column driver-sql actually creates, in both the TypeScript and the SQL format.
    cli
  • 0cde37dobjectstack lint now judges hooks authored as inline handler functions with the same write-set rules it already applied to explicit body hooks.
    cli, lint
  • 25011b0 — point @objectstack/cli/console at a public barrel with a name-and-shape pin
    cli
  • 2da2901os lint --strict makes warning-severity findings fail the run, so an app can rely on the platform's warning-level rules as its gate instead of re-implementing them locally (#15935)
    cli
  • 4a1a3b0os lint no longer crashes on a localized label.
    cli
  • 6b7d709objectstack validate now lowers hooks authored as inline handler functions to a metadata body before it parses, so the hook write-set rules judge them there exactly as objectstack build and objectstack lint already do.
    cli, lint
  • 95d5cbb — Ratify ./hook-body as a public subpath export — extractHookBody, HookBodyExtractionError, HookBodyRefusalKind and ExtractedBody were reachable as a deep dist/utils/extract-hook-body.js import until #13123 sealed the surface, and an app's hook-body fidelity harness (hotcrm's test/helpers/action-sandbox.ts) consumes them to run the SAME body-only lowering os build ships through the real QuickJS runner, so a test executes what production executes rather than a lookalike. The #13123 body names exactly this remedy for an out-of-repo consumer — ratify the subpath as public surface rather than read dist/ paths — and 17.3.0 applied it to ./console for cloud's objectos-runtime; this applies it to the second consumer (#15325). @objectstack/cli/hook-body is a dedicated entry that re-exports those four names and nothing else; the deep dist/ path stays sealed. Also admits ./package.json, so the ordinary tooling idiom of reading a dependency's own manifest resolves again.
    cli
  • 984f1dascoreMetadata no longer scores a stack whose linter crashed as a perfect one.
    cli
  • 9c3fda5os lint --eval --json now carries the ADR-0112 error carriers on its generator-load failure, instead of a bare {error}.
    cli
  • acab609os serve's runtime state file is keyed by the PROJECT, not by the environment id alone — so two projects on one machine stop overwriting each other's supervision record.
    cli
  • cf6b671os create now emits a project that installs outside this monorepo.
    cli
  • ec0a6e7backfillSummaryNulls accepts recomputeUndefinedOnEmpty — a caller who KNOWS a min/max/avg roll-up column was just declared can have it filled; os migrate summary-nulls --recompute-undefined-on-empty object.field surfaces it (#15064)
    cli, objectql
  • 61b4eb3client.meta.getHistory answers the published HistoryMetaItemResponse on both of its exits, and the route ledger names the schema.
    client, rest
  • 68437d4 — The automation resume route's 400 FLOW_FAILED now says whether the run is stranded.
    client, runtime, spec
  • e08892dcloneData reports droppedFields like every other create face: CloneDataResponseSchema (@objectstack/spec/api) gains an optional droppedFields member of the same shape as CreateDataResponseSchema's, and the POST /data/:object/:id/clone 201 body carries it whenever the engine stripped a static readonly column from the clone.
    client, metadata-protocol, spec
  • 236f2df — Console (objectui) refreshed to a472b07167a3. Frontend changes in this range:
    console
  • 30b0990 — Console (objectui) refreshed to 53ded82bf7a4. Frontend changes in this range:
    console
  • 2025b1fkernel.use() now enforces the declared plugin contract. A plugin object that PluginSchema (@objectstack/spec, kernel/plugin.zod.ts) refuses is refused at load instead of being stored and mounted.
    core
  • 2a3deccPluginSchema now REQUIRES staticPath and slug when type is 'ui', and core's Plugin interface inherits every PluginSchema key from PluginDefinition instead of restating two of them.
    core, spec
  • 2ed6be6 — Advisory validation rules no longer flood the startup log, and no longer count a row twice on a clean first boot.
    core, metadata-protocol, objectql
  • 51ae731LiteKernel.use() now enforces the declared plugin contract — the same check, the same refusal, as ObjectKernel.use(). A plugin object that PluginSchema (@objectstack/spec, kernel/plugin.zod.ts) refuses is refused at registration on both published kernels instead of on one.
    core
  • 66dc6ab — Plugin startup elapsed time is now reported as durationMs — the unit-bearing name the spec contract for the same result declares. startTime, which never held a start time, is deprecated and still populated.
    core
  • b0529e1ResolvedAuthzContext.authRefusal is removed — a published member nothing ever read (#14273)
    core
  • cc00df2 — ADR-0087 semantic-migration ledger: register the retirement of @objectstack/core's PluginSecurityScanner (#14919)
    core, spec
  • cf9bda4 — The kernel's in-memory i18n fallback learns the declared i18n.fallbackLocale, so one declaration stops answering two ways (#15694)
    core, runtime, spec
  • 4998efa — Scaffolded projects now ship a CI workflow, and a lint script for it to run.
    create-objectstack
  • 813d6c5npx create-objectstack now declares the same TypeScript range as os init and
    create-objectstack
  • fd75728 — Scaffolded projects now install the AI skills bundle for one agent runtime
    create-objectstack
  • 1cf7392driver-memory analytics honours AnalyticsQuery.timezone when it resolves a string dateRange, instead of accepting the field and answering on UTC (#16042)
    driver-memory
  • 2003259find(), findOne() and create() publish their declared types (#14435)
    driver-memory
  • 5f4f1f6driver-memory analytics resolves dateRange on the UTC calendar, so 'today' and last N ... stop being offset by the process timezone (#15825)
    driver-memory
  • a646120 — The three SQL compilers in this package — the RLS read-scope lowering (compileScopedFilterToSql), NativeSQLStrategy's own where and the ObjectQLStrategy SQL echo — compile a text operator over a column whose declared type stores no text to the contract's declared answer.
    driver-memory, driver-sql, driver-turso, service-analytics, spec
  • 2200f8eupdate() publishes its honest type — the contract's Record<string, unknown> | null, not any (#14438)
    driver-sql, driver-turso
  • 33e939f — Schema drift now reports a SINGLE-VALUE JSON-class column that a stale varchar/text column is holding — the population the detector could never see.
    driver-sql
  • 45cfa1b — The record read doors present the builtin audit stamps (created_at, updated_at) and every declared Field.datetime column as the canonical instant text YYYY-MM-DDTHH:MM:SS.sssZ on EVERY dialect — Postgres and MySQL now included, exactly as SQLite always has (ADR-0053 addendum D-F1..D-F3, #13973).
    driver-sql
  • 54bb2f1 — The analytics SQL compilers compile the case-sensitive text family per dialect, so a $contains policy on SQLite stops admitting rows it excludes (#15684)
    driver-sql, service-analytics
  • 6d4d5d3SqlDriver.aggregate answers 0 — not null — for a sum over a group whose aggregand is NULL in every row, matching the engine's in-memory aggregate tier and the identity emptyGroupValueFor already declares (#15546; maintainer ruling 2026-09-07, option A: a non-empty group whose aggregand is absent and an empty group are the SAME case for sum, and the SQL face is the one that moves).
    driver-sql, driver-turso, spec
  • 7862fb7SqlDriver's object-definition parameters now DECLARE every key they read. initObjects accepts lifecycle, and the whole rotation chain — rotateShards, ensureRotation, ensureShardTable — accepts tenancy and indexes, spelled as a fresh object literal rather than only as a value bound to a variable first.
    driver-sql, driver-sqlite-wasm, driver-turso
  • b72226fSqlDriver.initObjects() and SqlDriver.registerObjectMetadata() now declare the indexes key they have always read.
    driver-sql, driver-sqlite-wasm
  • 098cbb7validateExpression now refuses a non-string expression source through errors[], instead of throwing a raw TypeError that wiped out the caller's located reporting.
    formula
  • cfb64a6createHonoApp mounts the auth surface where the auth service actually serves, and refuses a prefix it cannot serve it under.
    hono, plugin-auth
  • 36a16d0 — Two new widget-binding rule ids for a chart widget with an empty selection
    lint
  • 56fe8c2 — A flow predicate authored as a CEL envelope is now refused at build time, instead of running unread by either validator.
    lint, service-automation, spec
  • 615fac3 — A publish now refuses an object whose highlightFields names a field that does not exist on it — the same gate that refuses a code-authored stack.
    lint, metadata-protocol
  • 720bf47flow-update-readonly-field and hook-api-update-readonly-field now report a non-system create of a static-readonly field — a new error-severity finding that fails os lint / os validate / os build on a shape they used to accept.
    lint
  • 9408b7f — A flow condition that is neither CEL text nor an expression is now refused at build time, instead of being read as an empty condition and answering a silent false.
    lint, service-automation, spec
  • 954cb0b — an assignment value may be a CEL envelope — evaluated at run time, validated at registerFlow, objectstack validate and the runtime publish gate (#15137, the executor half of #14149)
    lint, service-automation
  • 9f890d3filter-preset-comparand gains a FIELD-TYPED arm (#16106, maintainer-ruled 1′): on a declared date / datetime field, a dashboard date-range preset name (last_30_days, this_quarter, …) is now refused in EVERY comparand position — bare (implicit equality), $eq / $ne, $in / $nin, and their view-rule (equals / not_equals / in / not_in) and triple (= / != / in / nin) spellings — with the same located message and prescription the ordering positions already carry ({ $gte: '{30_days_ago}' } for last_30_days, and so on). The field type is read from the stack's own object graph: a dashboard widget or report through its dataset to that dataset's object, a view through data.object, a flow CRUD node through config.objectName, a page component through dataSource / properties, an object's own list views and relatedListFilter, a summary field's child object. A position the graph cannot bind, a registry-injected column, a time field, or a select / text column stays unjudged — equality against a picklist value that collides with a preset name is a working filter. The field-agnostic schema door in @objectstack/spec keeps its ordering-only boundary unchanged; this closes the authoring-time gap where objectstack lint and the runtime publish gate accepted a filter the engine then refused with INVALID_FILTER / 400 on first render.
    lint, spec
  • a87163c — New advisory rule field-no-consumers (validateFieldConsumers): a field declared on an object that nothing in the stack reads or displays is reported as a warning by os validate, os build and os lint.
    lint
  • b4b37e5 — The object publish door now refuses an object whose searchableFields entry, or whose built-in list view's columns (and every other field-naming position on that list view), names a field the object does not have.
    lint, metadata-protocol
  • c01b3a6chart-field-unknown drops to warning on the three chartConfig binding keys the pinned renderer refuses, and says what actually happens
    lint
  • cd55558widget-measures-missing — the empty-measure selection is reported on every widget family, not just charts
    lint
  • d61bad0 — New gating rule flow-filter-token-unknown: a {…} filter token in a flow node's config.filter that NEITHER {…} dialect can resolve is now an authoring-time error.
    lint
  • 3bd9b34deriveViewContainerObject gets a leaf /view-container subpath, so objectql's lean ADR-0076 entry stops loading the manager, chokidar, glob and js-yaml for a six-line pure function
    metadata, objectql
  • 8647c87 — A completed run of the ADR-0030 notification cut-over now records itself in the sys_migration deployment ledger, per the ruled claim matrix.
    metadata
  • a56baa2 — a keyed plural read on MetadataManager, listNames fault parity, and an action audit that answers from the same identity and sources as the router
    metadata, objectql
  • c1d274d — two files sharing one stem are refused with both paths named, instead of one being listed twice and served by extension precedence (#14921)
    metadata
  • 6491463/discovery stops advertising a realtime service that has no mounted surface, and "what counts as a subscribable channel" becomes one explicit definition.
    metadata-protocol, runtime, spec
  • c1d8f98 — Seed datasets gain a locale filter axis, composed with env by the loader.
    metadata-protocol, spec
  • d2b6fa0 — Stop persisting the caller's _packageId / _packageVersion / _provenance, and restate tenant authorship at hydration for every metadata type.
    metadata-protocol
  • 11f848eReadonlyFieldRejectedError's error code is now an importable constant.
    objectql
  • 33388f9 — Engine refusals now declare their HTTP status under both spellings: httpStatus beside the existing status, same number, at every producer in the package.
    objectql
  • 48b0fcf@objectstack/objectql now publishes a recognizer for the org-less system-write refusal, so a consumer no longer has to choose between an unsound check and a re-spelled string.
    objectql
  • 6d7d740ValidationError publishes its error code as an importable constant — the last row of #16159's census.
    objectql
  • 7778115ObjectQL.find() now guarantees the array it declares: an afterFind hook that replaces the result container is refused with FIND_HOOK_RESULT_NOT_ARRAY.
    objectql, spec
  • d0ee598 — the boot loop refuses a view container whose name disagrees with the object it binds to, instead of silently rewriting the author's field (#14666)
    objectql
  • ddfbf04 — Three more engine refusals publish their error code as an importable constant.
    objectql
  • e6279dc — The registry's three conflict refusals now publish their error code as an importable constant.
    objectql
  • eddd612 — Both transaction-seam refusals publish their error code as an importable constant.
    objectql
  • f7ffbd6accept-language: zh now reads a Chinese refusal on the response whose labels are already Chinese.
    objectql
  • f9a3c32 — the Layer 0 tenant wall records its verdict on the operation, and the bulk data-event producer reads it instead of re-deriving the wall
    objectql, plugin-security, spec
  • fa125f3Field.valueDomain binds at the write seam — a non-member is refused with value_domain (maintainer ruling 2026-09-02 on #14168, engine half)
    objectql, spec
  • c677cda — Ship the multi-organization runtime as open source: @objectstack/organizations is now an
    organizations, plugin-security, service-cluster, spec
  • 0a038ccsys_job.timezone and sys_report_schedule.timezone are validated against the IANA domain (#15872)
    platform-objects
  • 4ca358dsys_session.revoke_reason accepts organization_membership_ended — "Remove member" now actually signs the person out
    platform-objects, plugin-auth
  • 6acb37esys_business_unit.timezone and sys_organization.timezone — the organization hierarchy carries the IANA zone a date boundary is computed in (#14238)
    platform-objects, plugin-auth
  • 7797102sys_organization admits generic update on the data door, column-gated by the ADR-0092 D2 identity write guard (#15873 — maintainer ruling 2026-09-07, decision batch #64, option (a), verbatim 「同意」).
    platform-objects
  • a83482c — The three platform record pages carry a translated label in every shipped locale.
    platform-objects
  • 3d3f60e — An approval decision that lands while its flow run strands now says so in fields, not only in prose.
    plugin-approvals, rest, types
  • 4c31f02 — The stranded-request inspection tells a repairable strand from a cascade-failed run — through a dedicated read-only engine member, not through the wire (#15358, ruling B′).
    plugin-approvals, service-automation
  • 6530e04 — A restored approval suspension can now be decided again, not only cancelled.
    plugin-approvals
  • 142c01c — MCP OAuth can complete again: the MCP resource is registered as an RFC 8707 resource and DCR-registered clients are linked to it, so authorize?resource=<mcp url> no longer answers invalid_target.
    plugin-auth
  • aedbaefPOST /sign-up/email for an address that already has a sys_user row is refused explicitly, instead of answering 200 for a row that is never written (#15587)
    plugin-auth, spec
  • 5f7fa1dGET /auth/me/localizationlocale is now the signed-in user's language — sys_user.locale when set, then the request's Accept-Language, then the deployment default (#14788)
    plugin-hono-server, spec
  • 6615a02 — the current-user faces assemble their ExecutionContext through the shared assembler (#15747)
    plugin-hono-server
  • 4db3c61publicSharing.enabled now has one canonical predicate, exported from the package that declares the key.
    plugin-sharing, runtime, spec
  • 9fa5775 — the field sharing recipient is enforced — expanded once per matched record
    plugin-sharing
  • 46803fa — the authored label is the default locale's text — ResolveOptions.defaultLocale skips the fallback chain for a default-locale request, and a chain-less caller no longer falls to a literal en (#15711)
    rest, spec
  • 7370989GET /api/v1/packages, GET /api/v1/packages/:id and DELETE /api/v1/packages/:id have one implementation: the runtime dispatcher's /packages domain. @objectstack/rest's registerPackageRoutes no longer mounts its own copies of those three routes; it mounts POST /api/v1/packages/publish and nothing else.
    rest
  • 784cb92 — every metadata.endpoints.* switch gates exactly the face its name states, and the whole-store operations get their own key maintenance (#15542, #15854)
    rest, spec
  • a84e1ceII18nService.getFallbackLocale() — the declared fallback locale is readable, so the metadata-document translators can be handed the chain the deployment declared (#14882)
    rest, service-i18n, spec
  • f5cc78b — the generic declared-status passthrough names its object on both error doors (#14725)
    rest
  • 1ecee3e — every defineStack refusal carries an ADR-0112 envelope — six new STACK_* codes beside STACK_CROSS_REFERENCE_INVALID (#15963)
    runtime, spec
  • 2c753fe — a flow action's run context now carries recordLoadDenied (#15168)
    runtime, spec
  • 3e560daGET /api/v1/health answers 200 whenever the process can serve HTTP, even while a configuration fault is making every other route 503.
    runtime
  • 44c849c — The two operator run-lifecycle verbs get a door: POST /automation/:name/runs/:runId/cancel and POST /automation/:name/runs/:runId/restore-suspension, both gated on the platform operator.
    runtime
  • 8a12067 — the platform action route executes the declarative row-level operation: 'update' action (#14092)
    runtime
  • 92b5d7fPOST /api/v1/packages now answers an install-time namespace collision with error.code: "NAMESPACE_CONFLICT". NAMESPACE_CONFLICT is registered in ERROR_CODE_LEDGER under @objectstack/objectql, so the closed ADR-0112 vocabulary (StandardErrorCode ∪ ERROR_CODE_LEDGER) gains one member and a caller can branch on the refusal directly.
    runtime, spec
  • b31ebfe — A screen flow can now be completed by a headless caller, and list_actions publishes its input names.
    runtime, service-automation
  • bca21f7POST /packages/:id/duplicate now refuses a source that is not a writable base, instead of answering 200 with an empty copy.
    runtime, spec
  • ce8bfc9 — Dispatcher domain routes stop claiming their lexical neighbours: DomainRoute.match now defaults to 'segment', and the project-membership skip list gained the same boundary.
    runtime
  • de75e40 — The /keys mint gate and the install-wide activation-write gate classify a tenancy resolution failure instead of reading it as "no wall"
    runtime
  • f2f6684 — Package lifecycle state is keyed by the PROJECT as well as the environment id, so two projects on one machine stop sharing which packages an operator has disabled.
    runtime
  • 07f40e5 — A dataset measure's fields[].type stops contradicting the value beside it: a min/max over a temporal field is described as time, not number (#15768)
    service-analytics, spec
  • 6136293 — A min/max over a string-valued field is described as string, not number (#16098)
    service-analytics
  • 6573af9 — A draft-preview min/max answers the operand's own type instead of 0, and a preview dimension column is described by its own type
    service-analytics
  • 5964124 — a resume that consumed the pause and then failed downstream answers status: 'stranded' (#13937)
    service-automation
  • 5a91387evaluateCondition now refuses a malformed condition shape with the same STRUCTURAL_CONDITION_SHAPE_REFUSAL registration already raises — evaluation and registration share one refusal, so a shape that slipped past registration can never surface as a raw TypeError or as a silent false.
    service-automation
  • 65ec530 — The run step log tells a parallel branch apart from a loop row: iteration is the enclosing loop's iteration, always, and the branch index moves to branch.
    service-automation
  • 8341ed2 — A scheduled (cron) flow is now delivered once per tick window, and replaying a window that was already delivered is refused instead of silently sent again.
    service-automation, service-job, trigger-schedule
  • d30ccb9 — A contained per-iteration failure is now visible at run level, attributed to its iteration, and bound to its row.
    service-automation
  • 6b8c677 — the settings door answers from the ONE shared value-domain predicate, and refuses a non-member with value_domain (#15162)
    service-settings
  • dd2184amountStorageRoutes — mount the storage routes on a host-owned HTTP surface, composed from a kernel that has no http-server service (#15169)
    service-storage
  • 13c48c2 — retire connector.errorMapping — eleven authorable keys nothing ever read, one of them spelled like the live userMessage channel (#14676, ADR-0049)
    spec
  • 222dc0fIJobService.replay gains an optional third argument, options?: JobReplayOptions, carrying force: true (#14766 — the contract half of the #14501 A+a2 ruling)
    spec
  • 3e3ecb0 — The model-facing solution-blueprint mirror can no longer generate an identifier the applier rejects.
    spec
  • 3f89967 — A flow can now REFUSE with per-record text: the end node gains outcome and an interpolated message, and the run vocabulary gains refused.
    spec
  • 4cfc93bobjectNavTargetExclusivity — the object-level check on an object navigation item that refuses filters combined with recordId / viewName, and runAction combined with recordId — is now EXPORTED from @objectstack/spec/ui, one function per refinement in the same posture as the check* exports. A hand-written mirror of the object nav item chains this very function in its own superRefine instead of restating the rule from prose; a restatement is what drifts: objectui's hand-written mirror re-implements neither rule — its superRefine checks only id / label, and the file names no filters rule beyond the field's declaration (measured at the pinned .objectui-sha and at objectui origin/main).
    spec
  • 554a160translatePage now reads a region-level page:header by page name only. The id route
    spec
  • 581d8f8TryCatchErrorValueSchema declares the code key the try_catch engine binds (#14954)
    spec
  • 613bfbd — register the fourteen remaining door: 'none' error codes that ship in dist — the rest of the #16404 class after #16449 enters ERROR_CODE_LEDGER (#16649)
    spec
  • 625b0c3PLATFORM_TOOLS_BY_PACKAGE['service-ai-studio'] lists the five tools the cloud AI runtime registers that it had been omitting: get_authoring_rules, load_tools, open_record, test_flow and toggle_flow. Added in the list's existing alphabetical order; nothing else in the registry moves.
    spec
  • 68d5dfdExecutionStepMetrics gains an optional failures slot, and FlowRunSummary.failed is declared as the fold INCLUDING what a delegating node rolled up from its child (maintainer ruling 2026-09-06 on #15617, spec half)
    spec
  • 69602e5 — export COMPOSE_KEY_DISPOSITIONS and STACK_DEFINITION_KEYS — the artifact envelope's top-level key set and each key's composition rule, derivable from one source instead of hand-copied per consumer (#14877)
    spec
  • 6ba0db4 — declare the aggregate × field-type compatibility matrix a dataset measure is judged against — AGGREGATE_FIELD_TYPE_COMPATIBILITY and isAggregateCompatibleWithFieldType (#16353, spec half of #16099)
    spec
  • 7936b29 — export the object-level refinement checks the mirrored UI schemas run — checkListViewPageMount, checkListViewCalendarVisualization, checkPageSourceCompleteness, checkGlobalFilterDateDefaultValue (#16489, the spec half of objectui#7715)
    spec
  • 8333a6c — A blueprint nav entry can say WHICH view it opens: viewName is added to BlueprintNavItemSchema and, in lockstep, to the strict mirror's StrictNavItem (required-but-nullable, per the strict convention).
    spec
  • 859ded3FieldSchema refuses a WHITESPACE-ONLY reference on lookup / master_detail
    spec
  • 87f0cccSharingRuleEvaluationResult declares grantsRefused?: number — the optional seventh key the sharing-rule evaluate route already answers (#14969)
    spec
  • a7da4deadr-0030-notification-event joins CREATION_ATTESTED_MIGRATION_IDS, and its docblock states what a run may claim in the sys_migration ledger (maintainer ruling 2026-09-05 on #15710)
    spec
  • abae16a — register every error code that ships in distOBJECT_OWNERSHIP_CONFLICT, the seven STACK_* defineStack refusals and PLUGIN_UI_REQUIRED_KEY_MISSING enter ERROR_CODE_LEDGER (#16449)
    spec
  • abb140c — declare the two operator run-lifecycle verbs on IAutomationServicecancelRun and restoreConsumedSuspension (#16495, the contract half of #13953)
    spec
  • bf1054a — retire the fourteen inert deadline keys of the incident-response, training and change-management schemas (#14477, ADR-0049)
    spec
  • c2a336c@objectstack/spec/system now names the ADR-0030 notification cut-over, so "has this deployment run it?" has a place to be answered.
    spec
  • c7aca0dResumeFailureReport — the machine-readable half of a resume failure, declared once in contracts/approval-service.ts and carried as the optional, additive resumeFailure member of ApprovalRecallResult and ApprovalDecisionResult (the contract half of the #16472 family ruling, decision batch #76).
    spec
  • de0bcddComponentPropsMap['object-kanban'] declares limit, the row cap four objectui faces already implement (#16503, the spec half of objectui#8172)
    spec
  • e89fa92IDataDriver now declares aggregate? — the one engine-reached driver verb that had no signature to match against.
    spec
  • ed7243dAGGREGATE_FIELD_TYPE_COMPATIBILITY accepts boolean / toggle for sum / avg / min / max — ruling #11152 (booleans aggregate as numbers on every backend) stands over batch #59's blanket default (#16685)
    spec
  • f502898 — list-view grouping is server-side — the group header query and the per-group row page compile from the view (#14556)
    spec
  • fe0d9a4action.ai.requiresConfirmation gets a real contract — a DECLARATION, not yet a live gate: the contract states that an AI-facing call on an action declaring the flag must carry an explicit confirmation and is refused without it, with a refusal that tells the caller how to retry. No door performs that refusal yet; setting the flag still stops nothing until the runtime half lands.
    spec

Fixes in 17.4.0

221 patch changesets, ⛔ not enumerated — a patch entry is by construction not a new capability, and listing all of them is what the per-package CHANGELOG.md files are for. Distribution across the packages they touched:

  • spec — 50
  • cli — 38
  • lint — 23
  • runtime — 15
  • rest — 13
  • platform-objects — 12
  • service-automation — 12
  • metadata-protocol — 10
  • objectql — 9
  • core — 8
  • metadata — 7
  • plugin-auth — 7
  • driver-sql — 6
  • plugin-approvals — 5
  • service-analytics — 5
  • client — 4
  • plugin-sharing — 4
  • plugin-security — 4
  • plugin-webhooks — 3
  • plugin-email — 3

…and 35 further packages with fewer entries.

New in Console (Studio) — objectui pin 00d3f09c500c → 53ded82bf7a4

⚠️ Endpoints measured, the chain between them is not. 17.3.0 shipped with 00d3f09c500c (its own Console section names it as the last of that release's four moves); .objectui-sha on this tree reads 53ded82bf7a4, bumped by 30b09907"the shipped console lowers array analytics filters" (#16788). ⛔ The intermediate bumps are not enumerated here: this checkout's history for .objectui-sha does not carry all of them, and guessing the chain would be worse than naming the gap. Whoever curates this page should walk the pin's real history and list the moves the way 17.3.0 does.

Upgrade checklist

⚠️ One checklist per release, for the release you are landing on and every release you cross to get there — and see how far each list has actually been walked.

Not written. An upgrade checklist is a walked route, not a restatement of the breaking list above; the v17 line is explicit that a step nobody has run, presented beside steps that were, is how a reader finishes a checklist and believes they are done. Nobody has walked 17.3.0 → 17.4.0. Until someone does, read Breaking changes & migration in 17.4.0 directly and treat every line in it as unproven about what it costs to cross.

On this page