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.
be75493— BREAKINGos lint --generatornow refuses to run without--eval, instead of accepting the flag and ignoring it.
clic8e5ac6— retireos create example— it was a weakeros initplus a README, and the refusal now namesos init(#16483, #15531)
clicee3961— BREAKINGos create <type> <name>now refuses a project name that npm refuses, and refuses it before it writes anything.
clie9fcd6b— a duration-shapedz.number()key carries its unit in the key name —hook.timeout/job.timeout/DriverOptions.timeout→timeoutMs,MetadataManagerConfig.cache.databaseLoader.ttl→ttlMs(the outercache.ttlleaves outright under #15624 — nothing read it), tenantidleTimeout/sessionTimeout→*Seconds; new gatecheck:duration-unit-keys(#14478, #14519)
cli,core,driver-memory,driver-turso,metadata,objectql…ffe058a— BREAKINGos lint --eval --generator ""now refuses instead of quietly running the offline eval, matching the rule the same flag already follows without--eval.
cli7092d63— theorganizations.*family declares the wire shapes better-auth actually sends — nineteen publishedPromise< any >returns narrowed, twenty ledger entries closed (#14314)
client7beaaa3—oauth.applications.deleteresolves on the zero-byte 200 its route answers, instead of rejecting on every successful delete (#15451)
clientb1b978c— theauth.*family declares the wire shapes better-auth actually sends — thirteen publishedPromise< any >returns narrowed (#14313)
cliente944fdb— theoauth.*family declares the wire shapes better-auth actually sends — four publishedPromise< any >returns narrowed (#14312)
client10d05bb— an array comparand is refused instead of compared by reference, and aDatecomparand is compared by time value
driver-memory,spec78bc4ad—findWithWindowFunctions()presents its rows like every other read door — a declared boolean answerstrue, not1(#16609)
driver-sql0145680—timeoutbeside an UPPERCASEWSS:///WS://url in forced remote mode is refused at construction, closing the last corner of the same gap (ADR-0049 enforce-or-remove)
driver-turso5071310—timeoutbeside a pre-configuredclientin remote mode is refused at construction instead of being accepted and never delivered (ADR-0049 enforce-or-remove)
driver-tursobc0ac1d—timeoutbeside awss:///ws://url is refused at construction instead of being accepted and never delivered (ADR-0049 enforce-or-remove)
driver-tursoed5d557—timeoutbounds remote operations;localPathandwasmleave the published config schema (#16024, ADR-0049 enforce-or-remove)
driver-turso,specb398ad2— BREAKING (behaviour): a staticreadonlyfield is now stripped from a non-system caller's INSERT payload insideengine.insert, exactly as it already was onengine.update. A non-system create that used to write a read-only column now has that column dropped, reported throughonFieldsDropped/droppedFields, logged atwarn, and refused outright understrictReadonlyWrites. Seeding a read-only column at create time is a system act — usecontext.isSystem, a flow'srunAs: 'system', a system hook or a seed.
lint,metadata-protocol,objectql,service-automation,service-settings,spece0af1a8—<ListView objectName>/<ListView viewType>are retired from the react-tier component contract —data={{ provider: 'object', object }}/typeare the only spellings (#14791)
lint,spec7629f4d— retire the three inert outer keys ofMetadataManagerConfig.cache—enabled,ttlSeconds(formerlyttl) andmaxSize— read by nothing;cache.databaseLoaderis the only live half (#15624, ADR-0049)
metadata,spec65846bc— a batch ROW reports a unique-constraint refusal asUNIQUE_VIOLATION— the same wire spelling as the whole-request failure on the same route (#14723)
metadata-protocol,objectql,rest8e0b297—positions[]on the session payload is the SECURITY axis, not the better-auth […] scalar (one word elided: ADR-0090 D3 reserved word) (#15136)
plugin-auth,specafa3a26— a non-member scheduletimezoneno longer discards the cron expression, and a schedule already holding one stops instead of firing on a cadence nobody asked for (#16291)
plugin-reports233222e—timeDimensions[].dateRange's string arm closes to the date-range preset vocabulary; any other string is refused with400 ANALYTICS_DATE_RANGE_UNRECOGNIZED(#16041)
runtime,spec21aabbc—FlowSchemarefuses a region node whose id is already declared elsewhere in the flow — one node-id space across the top-levelnodes[]and every region body (#16134)
spec414c1fc—ComponentPropsMap['element:record_picker'].filterconverges onto theViewFilterRulearray form — the last record-formfilterin the map (#14406, objectui#6206 Option B)
spec52804cd—FlowSchemarefuses a flow whoseedges[]declares the same id twice (#14964)
spec53cf263—FlowSchemarefuses a flow whose top-levelnodes[]declares the same id twice (#15713)
spec64bd6a3—composeStacksobjectConflict: 'merge'refuses object pairs whose object-level collections cannot be merged (#14848)
spec6f1ce7d— 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)
spec8f404a5—plugins/devPluginsare artifact envelope keys — excluded from the assembled package body and refused insidepackages[](#15219)
spec9c270bb—GanttConfigSchema/TreeConfigSchemarefuse undeclared keys — both.passthrough()windows are closed and the ten gantt members plugin-gantt read through the window are declared (#15469)
specc463d03— retire the incident-response, training and change-management families whole — nineteen defs and every name they exported — and theESignatureConfigdeadline pair (#15513, #14477, ADR-0049)
specef3a138— an evaluated expression slot requires a non-blanksource—EvaluatedExpressionSchema, composed by theassignmentvalue envelope (#15430)
specf81afe3— a typed expression slot fixes its dialect on the envelope arm too, and refuses a blank string (#15028, #15035)
specfb77aa5— atreefield'sreference, when present, must name the declaring object — any other target is refused at parse (#14892)
spec4f85e4d— 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.
000fd05—os generate migrationnow emits the character columndriver-sqlactually creates, in both the TypeScript and the SQL format.
cli0cde37d—objectstack lintnow judges hooks authored as inlinehandlerfunctions with the same write-set rules it already applied to explicitbodyhooks.
cli,lint25011b0— point@objectstack/cli/consoleat a public barrel with a name-and-shape pin
cli2da2901—os lint --strictmakes 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)
cli4a1a3b0—os lintno longer crashes on a localized label.
cli6b7d709—objectstack validatenow lowers hooks authored as inlinehandlerfunctions to a metadata body before it parses, so the hook write-set rules judge them there exactly asobjectstack buildandobjectstack lintalready do.
cli,lint95d5cbb— Ratify./hook-bodyas a public subpath export —extractHookBody,HookBodyExtractionError,HookBodyRefusalKindandExtractedBodywere reachable as a deepdist/utils/extract-hook-body.jsimport until #13123 sealed the surface, and an app's hook-body fidelity harness (hotcrm'stest/helpers/action-sandbox.ts) consumes them to run the SAME body-only loweringos buildships 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 readdist/paths — and 17.3.0 applied it to./consolefor cloud'sobjectos-runtime; this applies it to the second consumer (#15325).@objectstack/cli/hook-bodyis a dedicated entry that re-exports those four names and nothing else; the deepdist/path stays sealed. Also admits./package.json, so the ordinary tooling idiom of reading a dependency's own manifest resolves again.
cli984f1da—scoreMetadatano longer scores a stack whose linter crashed as a perfect one.
cli9c3fda5—os lint --eval --jsonnow carries the ADR-0112 error carriers on its generator-load failure, instead of a bare{error}.
cliacab609—os 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.
clicf6b671—os createnow emits a project that installs outside this monorepo.
cliec0a6e7—backfillSummaryNullsacceptsrecomputeUndefinedOnEmpty— a caller who KNOWS amin/max/avgroll-up column was just declared can have it filled;os migrate summary-nulls --recompute-undefined-on-empty object.fieldsurfaces it (#15064)
cli,objectql61b4eb3—client.meta.getHistoryanswers the publishedHistoryMetaItemResponseon both of its exits, and the route ledger names the schema.
client,rest68437d4— The automation resume route's400 FLOW_FAILEDnow says whether the run is stranded.
client,runtime,spece08892d—cloneDatareportsdroppedFieldslike every other create face:CloneDataResponseSchema(@objectstack/spec/api) gains an optionaldroppedFieldsmember of the same shape asCreateDataResponseSchema's, and thePOST /data/:object/:id/clone201 body carries it whenever the engine stripped a staticreadonlycolumn from the clone.
client,metadata-protocol,spec236f2df— Console (objectui) refreshed toa472b07167a3. Frontend changes in this range:
console30b0990— Console (objectui) refreshed to53ded82bf7a4. Frontend changes in this range:
console2025b1f—kernel.use()now enforces the declared plugin contract. A plugin object thatPluginSchema(@objectstack/spec,kernel/plugin.zod.ts) refuses is refused at load instead of being stored and mounted.
core2a3decc—PluginSchemanow REQUIRESstaticPathandslugwhentypeis'ui', and core'sPlugininterface inherits everyPluginSchemakey fromPluginDefinitioninstead of restating two of them.
core,spec2ed6be6— Advisory validation rules no longer flood the startup log, and no longer count a row twice on a clean first boot.
core,metadata-protocol,objectql51ae731—LiteKernel.use()now enforces the declared plugin contract — the same check, the same refusal, asObjectKernel.use(). A plugin object thatPluginSchema(@objectstack/spec,kernel/plugin.zod.ts) refuses is refused at registration on both published kernels instead of on one.
core66dc6ab— Plugin startup elapsed time is now reported asdurationMs— the unit-bearing name the spec contract for the same result declares.startTime, which never held a start time, is deprecated and still populated.
coreb0529e1—ResolvedAuthzContext.authRefusalis removed — a published member nothing ever read (#14273)
corecc00df2— ADR-0087 semantic-migration ledger: register the retirement of@objectstack/core'sPluginSecurityScanner(#14919)
core,speccf9bda4— The kernel's in-memory i18n fallback learns the declaredi18n.fallbackLocale, so one declaration stops answering two ways (#15694)
core,runtime,spec4998efa— Scaffolded projects now ship a CI workflow, and alintscript for it to run.
create-objectstack813d6c5—npx create-objectstacknow declares the same TypeScript range asos initand
create-objectstackfd75728— Scaffolded projects now install the AI skills bundle for one agent runtime
create-objectstack1cf7392—driver-memoryanalytics honoursAnalyticsQuery.timezonewhen it resolves a stringdateRange, instead of accepting the field and answering on UTC (#16042)
driver-memory2003259—find(),findOne()andcreate()publish their declared types (#14435)
driver-memory5f4f1f6—driver-memoryanalytics resolvesdateRangeon the UTC calendar, so'today'andlast N ...stop being offset by the process timezone (#15825)
driver-memorya646120— The three SQL compilers in this package — the RLS read-scope lowering (compileScopedFilterToSql),NativeSQLStrategy's ownwhereand theObjectQLStrategySQL 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,spec2200f8e—update()publishes its honest type — the contract'sRecord<string, unknown> | null, notany(#14438)
driver-sql,driver-turso33e939f— Schema drift now reports a SINGLE-VALUE JSON-class column that a stalevarchar/textcolumn is holding — the population the detector could never see.
driver-sql45cfa1b— The record read doors present the builtin audit stamps (created_at,updated_at) and every declaredField.datetimecolumn as the canonical instant textYYYY-MM-DDTHH:MM:SS.sssZon EVERY dialect — Postgres and MySQL now included, exactly as SQLite always has (ADR-0053 addendum D-F1..D-F3, #13973).
driver-sql54bb2f1— The analytics SQL compilers compile the case-sensitive text family per dialect, so a$containspolicy on SQLite stops admitting rows it excludes (#15684)
driver-sql,service-analytics6d4d5d3—SqlDriver.aggregateanswers0— notnull— for asumover a group whose aggregand is NULL in every row, matching the engine's in-memory aggregate tier and the identityemptyGroupValueForalready 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 forsum, and the SQL face is the one that moves).
driver-sql,driver-turso,spec7862fb7—SqlDriver's object-definition parameters now DECLARE every key they read.initObjectsacceptslifecycle, and the whole rotation chain —rotateShards,ensureRotation,ensureShardTable— acceptstenancyandindexes, spelled as a fresh object literal rather than only as a value bound to a variable first.
driver-sql,driver-sqlite-wasm,driver-tursob72226f—SqlDriver.initObjects()andSqlDriver.registerObjectMetadata()now declare theindexeskey they have always read.
driver-sql,driver-sqlite-wasm098cbb7—validateExpressionnow refuses a non-string expressionsourcethrougherrors[], instead of throwing a rawTypeErrorthat wiped out the caller's located reporting.
formulacfb64a6—createHonoAppmounts the auth surface where the auth service actually serves, and refuses a prefix it cannot serve it under.
hono,plugin-auth36a16d0— Two new widget-binding rule ids for a chart widget with an empty selection
lint56fe8c2— A flow predicate authored as a CEL envelope is now refused at build time, instead of running unread by either validator.
lint,service-automation,spec615fac3— A publish now refuses an object whosehighlightFieldsnames a field that does not exist on it — the same gate that refuses a code-authored stack.
lint,metadata-protocol720bf47—flow-update-readonly-fieldandhook-api-update-readonly-fieldnow report a non-system create of a static-readonlyfield — a new error-severity finding that failsos lint/os validate/os buildon a shape they used to accept.
lint9408b7f— 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 silentfalse.
lint,service-automation,spec954cb0b— anassignmentvalue may be a CEL envelope — evaluated at run time, validated atregisterFlow,objectstack validateand the runtime publish gate (#15137, the executor half of #14149)
lint,service-automation9f890d3—filter-preset-comparandgains a FIELD-TYPED arm (#16106, maintainer-ruled 1′): on a declareddate/datetimefield, 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}' }forlast_30_days, and so on). The field type is read from the stack's own object graph: a dashboard widget or report through itsdatasetto that dataset'sobject, a view throughdata.object, a flow CRUD node throughconfig.objectName, a page component throughdataSource/properties, an object's own list views andrelatedListFilter, a summary field's child object. A position the graph cannot bind, a registry-injected column, atimefield, 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/speckeeps its ordering-only boundary unchanged; this closes the authoring-time gap whereobjectstack lintand the runtime publish gate accepted a filter the engine then refused withINVALID_FILTER/ 400 on first render.
lint,speca87163c— New advisory rulefield-no-consumers(validateFieldConsumers): a field declared on an object that nothing in the stack reads or displays is reported as awarningbyos validate,os buildandos lint.
lintb4b37e5— The object publish door now refuses an object whosesearchableFieldsentry, or whose built-in list view'scolumns(and every other field-naming position on that list view), names a field the object does not have.
lint,metadata-protocolc01b3a6—chart-field-unknowndrops towarningon the threechartConfigbinding keys the pinned renderer refuses, and says what actually happens
lintcd55558—widget-measures-missing— the empty-measure selection is reported on every widget family, not just charts
lintd61bad0— New gating ruleflow-filter-token-unknown: a{…}filter token in a flow node'sconfig.filterthat NEITHER{…}dialect can resolve is now an authoring-timeerror.
lint3bd9b34—deriveViewContainerObjectgets a leaf/view-containersubpath, so objectql's lean ADR-0076 entry stops loading the manager, chokidar, glob and js-yaml for a six-line pure function
metadata,objectql8647c87— A completed run of the ADR-0030 notification cut-over now records itself in thesys_migrationdeployment ledger, per the ruled claim matrix.
metadataa56baa2— a keyed plural read onMetadataManager,listNamesfault parity, and an action audit that answers from the same identity and sources as the router
metadata,objectqlc1d274d— two files sharing one stem are refused with both paths named, instead of one being listed twice and served by extension precedence (#14921)
metadata6491463—/discoverystops advertising a realtime service that has no mounted surface, and "what counts as a subscribable channel" becomes one explicit definition.
metadata-protocol,runtime,specc1d8f98— Seed datasets gain alocalefilter axis, composed withenvby the loader.
metadata-protocol,specd2b6fa0— Stop persisting the caller's_packageId/_packageVersion/_provenance, and restate tenant authorship at hydration for every metadata type.
metadata-protocol11f848e—ReadonlyFieldRejectedError's errorcodeis now an importable constant.
objectql33388f9— Engine refusals now declare their HTTP status under both spellings:httpStatusbeside the existingstatus, same number, at every producer in the package.
objectql48b0fcf—@objectstack/objectqlnow 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.
objectql6d7d740—ValidationErrorpublishes its errorcodeas an importable constant — the last row of #16159's census.
objectql7778115—ObjectQL.find()now guarantees the array it declares: anafterFindhook that replaces the result container is refused withFIND_HOOK_RESULT_NOT_ARRAY.
objectql,specd0ee598— the boot loop refuses a view container whosenamedisagrees with the object it binds to, instead of silently rewriting the author's field (#14666)
objectqlddfbf04— Three more engine refusals publish their errorcodeas an importable constant.
objectqle6279dc— The registry's three conflict refusals now publish their errorcodeas an importable constant.
objectqleddd612— Both transaction-seam refusals publish their errorcodeas an importable constant.
objectqlf7ffbd6—accept-language: zhnow reads a Chinese refusal on the response whose labels are already Chinese.
objectqlf9a3c32— 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,specfa125f3—Field.valueDomainbinds at the write seam — a non-member is refused withvalue_domain(maintainer ruling 2026-09-02 on #14168, engine half)
objectql,specc677cda— Ship the multi-organization runtime as open source:@objectstack/organizationsis now an
organizations,plugin-security,service-cluster,spec0a038cc—sys_job.timezoneandsys_report_schedule.timezoneare validated against the IANA domain (#15872)
platform-objects4ca358d—sys_session.revoke_reasonacceptsorganization_membership_ended— "Remove member" now actually signs the person out
platform-objects,plugin-auth6acb37e—sys_business_unit.timezoneandsys_organization.timezone— the organization hierarchy carries the IANA zone a date boundary is computed in (#14238)
platform-objects,plugin-auth7797102—sys_organizationadmits genericupdateon 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-objectsa83482c— The three platform record pages carry a translated label in every shipped locale.
platform-objects3d3f60e— An approval decision that lands while its flow run strands now says so in fields, not only in prose.
plugin-approvals,rest,types4c31f02— 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-automation6530e04— A restored approval suspension can now be decided again, not only cancelled.
plugin-approvals142c01c— MCP OAuth can complete again: the MCP resource is registered as an RFC 8707 resource and DCR-registered clients are linked to it, soauthorize?resource=<mcp url>no longer answersinvalid_target.
plugin-authaedbaef—POST /sign-up/emailfor an address that already has asys_userrow is refused explicitly, instead of answering 200 for a row that is never written (#15587)
plugin-auth,spec5f7fa1d—GET /auth/me/localization→localeis now the signed-in user's language —sys_user.localewhen set, then the request'sAccept-Language, then the deployment default (#14788)
plugin-hono-server,spec6615a02— the current-user faces assemble theirExecutionContextthrough the shared assembler (#15747)
plugin-hono-server4db3c61—publicSharing.enablednow has one canonical predicate, exported from the package that declares the key.
plugin-sharing,runtime,spec9fa5775— thefieldsharing recipient is enforced — expanded once per matched record
plugin-sharing46803fa— the authored label is the default locale's text —ResolveOptions.defaultLocaleskips the fallback chain for a default-locale request, and a chain-less caller no longer falls to a literalen(#15711)
rest,spec7370989—GET /api/v1/packages,GET /api/v1/packages/:idandDELETE /api/v1/packages/:idhave one implementation: the runtime dispatcher's/packagesdomain.@objectstack/rest'sregisterPackageRoutesno longer mounts its own copies of those three routes; it mountsPOST /api/v1/packages/publishand nothing else.
rest784cb92— everymetadata.endpoints.*switch gates exactly the face its name states, and the whole-store operations get their own keymaintenance(#15542, #15854)
rest,speca84e1ce—II18nService.getFallbackLocale()— the declared fallback locale is readable, so the metadata-document translators can be handed the chain the deployment declared (#14882)
rest,service-i18n,specf5cc78b— the generic declared-status passthrough names its object on both error doors (#14725)
rest1ecee3e— everydefineStackrefusal carries an ADR-0112 envelope — six newSTACK_*codes besideSTACK_CROSS_REFERENCE_INVALID(#15963)
runtime,spec2c753fe— a flow action's run context now carriesrecordLoadDenied(#15168)
runtime,spec3e560da—GET /api/v1/healthanswers 200 whenever the process can serve HTTP, even while a configuration fault is making every other route 503.
runtime44c849c— The two operator run-lifecycle verbs get a door:POST /automation/:name/runs/:runId/cancelandPOST /automation/:name/runs/:runId/restore-suspension, both gated on the platform operator.
runtime8a12067— the platform action route executes the declarative row-leveloperation: 'update'action (#14092)
runtime92b5d7f—POST /api/v1/packagesnow answers an install-time namespace collision witherror.code: "NAMESPACE_CONFLICT".NAMESPACE_CONFLICTis registered inERROR_CODE_LEDGERunder@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,specb31ebfe— A screen flow can now be completed by a headless caller, andlist_actionspublishes its input names.
runtime,service-automationbca21f7—POST /packages/:id/duplicatenow refuses a source that is not a writable base, instead of answering200with an empty copy.
runtime,specce8bfc9— Dispatcher domain routes stop claiming their lexical neighbours:DomainRoute.matchnow defaults to'segment', and the project-membership skip list gained the same boundary.
runtimede75e40— The/keysmint gate and the install-wide activation-write gate classify a tenancy resolution failure instead of reading it as "no wall"
runtimef2f6684— 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.
runtime07f40e5— A dataset measure'sfields[].typestops contradicting the value beside it: amin/maxover a temporal field is described astime, notnumber(#15768)
service-analytics,spec6136293— Amin/maxover a string-valued field is described asstring, notnumber(#16098)
service-analytics6573af9— A draft-previewmin/maxanswers the operand's own type instead of0, and a preview dimension column is described by its own type
service-analytics5964124— a resume that consumed the pause and then failed downstream answersstatus: 'stranded'(#13937)
service-automation5a91387—evaluateConditionnow refuses a malformed condition shape with the sameSTRUCTURAL_CONDITION_SHAPE_REFUSALregistration already raises — evaluation and registration share one refusal, so a shape that slipped past registration can never surface as a rawTypeErroror as a silentfalse.
service-automation65ec530— The run step log tells aparallelbranch apart from alooprow:iterationis the enclosing loop's iteration, always, and the branch index moves tobranch.
service-automation8341ed2— 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-scheduled30ccb9— A contained per-iteration failure is now visible at run level, attributed to its iteration, and bound to its row.
service-automation6b8c677— the settings door answers from the ONE shared value-domain predicate, and refuses a non-member withvalue_domain(#15162)
service-settingsdd2184a—mountStorageRoutes— mount the storage routes on a host-owned HTTP surface, composed from a kernel that has nohttp-serverservice (#15169)
service-storage13c48c2— retireconnector.errorMapping— eleven authorable keys nothing ever read, one of them spelled like the liveuserMessagechannel (#14676, ADR-0049)
spec222dc0f—IJobService.replaygains an optional third argument,options?: JobReplayOptions, carryingforce: true(#14766 — the contract half of the #14501 A+a2 ruling)
spec3e3ecb0— The model-facing solution-blueprint mirror can no longer generate an identifier the applier rejects.
spec3f89967— A flow can now REFUSE with per-record text: theendnode gainsoutcomeand an interpolatedmessage, and the run vocabulary gainsrefused.
spec4cfc93b—objectNavTargetExclusivity— the object-level check on an object navigation item that refusesfilterscombined withrecordId/viewName, andrunActioncombined withrecordId— is now EXPORTED from@objectstack/spec/ui, one function per refinement in the same posture as thecheck*exports. A hand-written mirror of the object nav item chains this very function in its ownsuperRefineinstead of restating the rule from prose; a restatement is what drifts: objectui's hand-written mirror re-implements neither rule — itssuperRefinechecks onlyid/label, and the file names nofiltersrule beyond the field's declaration (measured at the pinned.objectui-shaand at objectuiorigin/main).
spec554a160—translatePagenow reads a region-levelpage:headerby page name only. The id route
spec581d8f8—TryCatchErrorValueSchemadeclares thecodekey thetry_catchengine binds (#14954)
spec613bfbd— register the fourteen remainingdoor: 'none'error codes that ship indist— the rest of the #16404 class after #16449 entersERROR_CODE_LEDGER(#16649)
spec625b0c3—PLATFORM_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_flowandtoggle_flow. Added in the list's existing alphabetical order; nothing else in the registry moves.
spec68d5dfd—ExecutionStepMetricsgains an optionalfailuresslot, andFlowRunSummary.failedis declared as the fold INCLUDING what a delegating node rolled up from its child (maintainer ruling 2026-09-06 on #15617, spec half)
spec69602e5— exportCOMPOSE_KEY_DISPOSITIONSandSTACK_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)
spec6ba0db4— declare the aggregate × field-type compatibility matrix a dataset measure is judged against —AGGREGATE_FIELD_TYPE_COMPATIBILITYandisAggregateCompatibleWithFieldType(#16353, spec half of #16099)
spec7936b29— export the object-level refinement checks the mirrored UI schemas run —checkListViewPageMount,checkListViewCalendarVisualization,checkPageSourceCompleteness,checkGlobalFilterDateDefaultValue(#16489, the spec half of objectui#7715)
spec8333a6c— A blueprint nav entry can say WHICH view it opens:viewNameis added toBlueprintNavItemSchemaand, in lockstep, to the strict mirror'sStrictNavItem(required-but-nullable, per the strict convention).
spec859ded3—FieldSchemarefuses a WHITESPACE-ONLYreferenceonlookup/master_detail
spec87f0ccc—SharingRuleEvaluationResultdeclaresgrantsRefused?: number— the optional seventh key the sharing-rule evaluate route already answers (#14969)
speca7da4de—adr-0030-notification-eventjoinsCREATION_ATTESTED_MIGRATION_IDS, and its docblock states what a run may claim in thesys_migrationledger (maintainer ruling 2026-09-05 on #15710)
specabae16a— register every error code that ships indist—OBJECT_OWNERSHIP_CONFLICT, the sevenSTACK_*defineStackrefusals andPLUGIN_UI_REQUIRED_KEY_MISSINGenterERROR_CODE_LEDGER(#16449)
specabb140c— declare the two operator run-lifecycle verbs onIAutomationService—cancelRunandrestoreConsumedSuspension(#16495, the contract half of #13953)
specbf1054a— retire the fourteen inert deadline keys of the incident-response, training and change-management schemas (#14477, ADR-0049)
specc2a336c—@objectstack/spec/systemnow names the ADR-0030 notification cut-over, so "has this deployment run it?" has a place to be answered.
specc7aca0d—ResumeFailureReport— the machine-readable half of a resume failure, declared once incontracts/approval-service.tsand carried as the optional, additiveresumeFailuremember ofApprovalRecallResultandApprovalDecisionResult(the contract half of the #16472 family ruling, decision batch #76).
specde0bcdd—ComponentPropsMap['object-kanban']declareslimit, the row cap four objectui faces already implement (#16503, the spec half of objectui#8172)
spece89fa92—IDataDrivernow declaresaggregate?— the one engine-reached driver verb that had no signature to match against.
speced7243d—AGGREGATE_FIELD_TYPE_COMPATIBILITYacceptsboolean/toggleforsum/avg/min/max— ruling #11152 (booleans aggregate as numbers on every backend) stands over batch #59's blanket default (#16685)
specf502898— list-view grouping is server-side — the group header query and the per-group row page compile from the view (#14556)
specfe0d9a4—action.ai.requiresConfirmationgets 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— 50cli— 38lint— 23runtime— 15rest— 13platform-objects— 12service-automation— 12metadata-protocol— 10objectql— 9core— 8metadata— 7plugin-auth— 7driver-sql— 6plugin-approvals— 5service-analytics— 5client— 4plugin-sharing— 4plugin-security— 4plugin-webhooks— 3plugin-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.