17.7.0
Release notes and upgrade checklist for 17.7.0 of the v17 line.
Highlights — 17.7.0
- App-authored code reaches stored metadata only through the metadata API.
A sandboxed body is no longer bound as a hook on
sys_metadataorsys_metadata_history, a hook or action body may not write them, and a hook, action or job body may not read them (bd70706, #21563;316be32, #21660); a hook whosebodytargets them, and a flow write node aimed at them, are refused at parse (9e9d693, #21592;a2aadab, #21687). The data door serves a metadata body's content hash in keyed form only and refuses filters that evaluate the body or the hash (713b0fa, #21436;5d0e4e2, #21619). ⚠️ Runos migrate audit-metadata-bodiesonce after upgrading, and expect a version token held from before to be refused once with409 METADATA_CONFLICT. - Credentials stop travelling in copies. A flow's inbound
secretand anhttpnode'ssigningSecretmove into a write-onlysys_flow_credentialchannel (96a9719, #21377); the audit ledger stops recordinginternalfields, and the platform's own credential columns are declaredinternal(50e1c65, #21301); credential fields are masked on every write response, record-change event, webhook body and flow trigger record (a0176ef, #21816;568dc0b, #21866;1f04696, #21928). ⚠️ Rotate every flow secret and the JWT signing keys, re-mint private share links, and resume, cancel or purge paused runs created before the upgrade. - "Hidden" and "gone" are one answer on the write doors. A by-id update or
delete of a row the caller cannot read answers
404 RECORD_NOT_FOUND(53021e3, #21812), and a predicate update or delete matches only readable rows (cab6396, #21900). On a walled posture, a create naming another organization'sorganization_idis refused403instead of being restamped (251a7dd, #21680). - Reads and writes serve declared fields only. A read with no projection,
and every record a write returns, no longer carry a column no metadata
declares (
5c9138b, #21612;5b5e83f, #21631). ⚠️ A conversion that still reads a retired field's leftover column runs before the upgrade, or afterwards through the new operator-onlyos migrate unmapped-columns(759dbe9, #21643). - Metadata write doors agree with the read envelope. The ADR-0010
_lockgate runs on every kernel topology, reads the row the reads serve and takes the strictest lock in scope (c43a8ae, #21715;cf60dbc, #21737;18c2ddc, #21801;18fe681, #21844), and the reads report what the doors refuse (fe10172, #21693). Every write door refuses a body whosenamedisagrees with its row (44defd4, #21536), and a code-defined datasource is read-only at the metadata door and at boot (9cc2c79, #21942;753e7a1, #21965). - Page blocks declare what their renderers read. In nine stages of #21464
and three changes beside it (#21287, #21463, #21869), the
ComponentPropsMapmembers ofobject-grid,object-kanban,object-calendar,object-map,object-gantt,object-tree,object-form,object-master-detail-form,object-metric,object-timeline,action:groupandaction:menutake the shape each block reads instead of any value. These are advisorycomponent-props-*findings atos validate; a stored page still saves. Alongside them,ai:chat_windowis retired (48eb9c1, #21531),element:textdropsheading/subheading(36ad321, #21614), and every block of ajoinedreport must bind adataset(ed15448, #21712). - Agents state what the cloud AI runtime enforces.
memoryrequiresmaxEntriesandreflectionIntervalonce long-term memory is on and loseslongTerm.store(22c2d6f, #21413),structuredOutputis JSON-only (3937ad2, #21367), andlifecycleis retired with theStateMachineSchemafamily (6e33b67, #21461). - Analytics and the engine stop answering what they cannot. Analytics
refuses unselected
orderkeys, fractional or negative windows, the row wildcard outsidecount, and the cube metric typesnumber/string/boolean(1caa603, #21314;6d67ad5, #21399;b793010, #21431;99589f9, #21452). The engine narrows"true"/"1"against a boolean field and refuses other comparands with400(9f13c94, #21372;45efcfa, #21404), and refuses a list under a scalar operator (100c394, #21484). - Public forms open only on an explicit switch. A form is served
anonymously only when its
sharingsetsenabled: truebesideallowAnonymousandpublicLink(6dd99b8, #21566), and a withdrawal at any metadata layer holds, within two known limits (3c7785d, #21864). ⚠️ A form that set onlyallowAnonymousandpublicLinkanswers404 FORM_NOT_FOUNDafter the upgrade. - Jobs carry their own code. A job takes a sandboxed
body(f1e4ae5, #21538), which every artifact door now schedules (6c5697d, #21584), a declarativepullof a mapping's connector source, and theorganizationit runs as (909229e, #21668). - Console: five objectui pin moves —
31971ff1e28f → 89cad75d5570 → ab1879721595 → 2e818d0b51ec → 9dfaca654311 → 0abd4f9f8769(8963dbf,1cbe165,100f68b,1354e7b,8832655) — carrying 229 releasing objectui changesets, 65 of them declared breaking upstream. The first fixes all four known console issues of 17.6.0; see New in Console.
What's new in 17.7.0
17.7.0 was published to the latest tag on 2026-10-06, 4 days after
17.6.0, moving the whole version-locked train and no major; the runtime still
implements protocol 17. The version commit 4e4e8814 (#21352) consumed 323
changesets, and that is the count this page uses. The 69 package
CHANGELOG.md files that carry a 17.7.0 section list them as 444 per-package
entries (194 minor, 250 patch) in 48 of those files, because a changeset that
bumps several packages is listed in each; the entries de-duplicate to the same
323. One of them describes code that 17.6.0 already shipped — see Shipped in
17.6.0 — so 322 are new
in this release. The publish ran from the version commit itself, and the npm
packages carry two commits whose changesets the version commit did not consume
— see Also shipped in 17.7.0.
The bundled Console advances five pins,
31971ff1e28f → 89cad75d5570 → ab1879721595 → 2e818d0b51ec → 9dfaca654311 → 0abd4f9f8769.
⚠️ Read this before treating the version number as a safety guarantee. As
with every minor of this line, entries that landed after the 17.0.0 cut ship as
minor (or patch) under the lockstep launch-window convention while being
explicitly breaking. Several things in this release change behaviour on a
running deployment with nothing to parse-fail on:
- a sandboxed hook bound to
sys_metadataorsys_metadata_historyis refused at registration, a hook or action body's write of either table and a hook, action or job body's read of one answer403 PERMISSION_DENIED, and a flowcreate_record/update_record/delete_recordnode aimed at either table fails its run; - a by-id update or delete of a row the caller cannot read answers
404 RECORD_NOT_FOUNDinstead of a403; a predicate (multi: true) update or delete leaves out rows the caller cannot read, and one whose readable match exceeds 10,000 rows is refused400 INVALID_FILTER; - on a walled posture, a create naming another organization's
organization_idis refused403, where it used to be stored in the caller's active organization; - a read with no
fields, every record a write returns, a hook'sctx.previous,data.record.*events and the webhookafterbodies that carry them, and the audit ledger's create and delete values no longer carry a column no metadata declares; - credential-class fields read as
SECRET_MASK(ornull) on write responses, record-change events, webhook bodies, approval snapshots and a record-change flow'srecord/previous;internalfields are absent there and from new audit rows, and the platform's credential columns leaveGET /api/v1/data/...; - the first boot with a crypto provider moves every stored flow's
secret/signingSecretintosys_flow_credential; with no provider, a save carrying one answers503, and cloning a flow that holds one answers409; - a public form is served anonymously only with
sharing.enabled: true, and a package-shipped form that keeps its link without switchingenabledon counts as withdrawn; - a filter that compares a boolean field with
"true"or"1"now matches thetruerows, and any other string, a number other than1/0, aDateor a list answers400; a row-level policy that compares a numeric or boolean column with a comparand outside the accepted set is dropped, so its read returns no rows and its write is refused unless a sibling policy grants; - the approvals service reads a position address only as
position:NAME,sys_approval_action.actor_idholds the person who acted, and the SLA and dead-run sweeps record no actor; - the
_lockgate refuses on a kernel with no environment id (the showcase's topology) as it does on an environment kernel, and takes the strictest lock among an item's rows and shipping packages; - a view container with no
formno longer serves its firstformViewsentry as the default create and edit form; - a stored datasource row under a code-defined name no longer replaces the code
definition at boot, and
PUT /api/v1/meta/datasource/default, and aDELETEof it with no stored row, answer403; - on
objectstack start, the federation boot gate compares every federated object, so the defaultonMismatch: 'fail'can stop a boot that used to pass; - the environment-membership gate and the organization slug guard answer
503when their own read fails, where they used to admit the request; - an external sign-in no longer links implicitly to a local user whose email is not verified, and a provider the user unlinked does not link again implicitly;
- a flow that the
kernel:readybind refuses is withdrawn instead of stayingactive, and a hook whose stringhandlernames another package's function is not bound; os dev -a,os start --artifactandOS_ARTIFACT_PATHbeside anobjectstack.config.tsserve the named artifact without loading the config, unless the artifact is that config's own compiled output;- a
PUT /api/settings/localizationthat namesdate_format,time_format,number_formatorfirst_day_of_weekis refused400 UNKNOWN_KEY, the live keys beside it included.
Breaking changes & migration in 17.7.0
This section is triaged, not exhaustive. An entry is written up here when
the change can be reached from something an application ships or operates — its
metadata, its data, its own code calling the SDK / REST / CLI, its deployment
config, or a plugin it authors. Everything else is left to the per-package
CHANGELOG.md files. The five Console pin refreshes are described once under
New in Console rather than
enumerated here.
The retirements in this release are registered under protocol major 18, as
in 17.5.0 and 17.6.0. os migrate meta --from 17 lists the source edits, and
os migrate meta --stored --apply rewrites stored rows where a lossless
conversion exists. The new ADR-0087 conversions are
agent-lifecycle-removed, agent-memory-long-term-store-removed,
agent-structured-output-refused-members-removed,
element-text-variant-heading-levels, object-grid-resizable-columns-removed,
object-master-detail-form-detail-sort-field-removed,
page-requires-non-compiled-kind-removed and
translation-widget-sub-caption-removed. The release also adds 41 D3 semantic
entries — the judgements no conversion can make — and the table at the end of
this section maps each one to the entry
below that carries its migration. Most breaking changes have no mechanical
rewrite; each says so. An app keeps engines.protocol: '^17'.
App-authored code reaches stored metadata through the metadata API only
sys_metadata holds each metadata body as stored, credential material
included, and sys_metadata_history holds its versions. For app-authored work,
the metadata protocol is now their only writer. A sandboxed hook, action or job
body also reads them only through it, and a flow's get_record node reads them
only in the projected, keyed form the data door serves.
- Sandboxed bodies. A hook with a sandboxed
bodywhoseobjectnames either table, alone or in a list, is not bound: it is refused at registration withPERMISSION_DENIED/ 403 and recorded in the bind log aterror(bd70706, #21563). A body's write of either table throughctx.apianswers403before it runs, and so does its read —find,findOne,countandaggregate, in a transaction or not, elevated or not (316be32, #21660). An action whose subject row is from either table answers the same403before the body runs. A wildcard ('*') body hook still binds, and its body is not run for those tables' events. - Authoring.
HookSchemarefuses a hook carrying abodywhoseobjectnames either table (9e9d693, #21592), andFlowSchemarefuses acreate_record,update_recordordelete_recordnode whoseconfig.objectNamenames one (a2aadab, #21687), atos validate, atdefineStack(422 STACK_SCHEMA_INVALID) and at the metadata save door (422 INVALID_METADATA). A stored flow carrying such a node is skipped at boot with a warning, and the flows beside it register. The write nodes also refuse the target at run time withPERMISSION_DENIED(f40bb32, #21649); afaultedge does not route that refusal. Aget_recordnode on either table is served the projected body and the keyed hash (a4f0cb0, #21621), and its filter over the body or a hash column is refusedINVALID_FIELD(96b0e31, #21641). - Host code. An action handler a host registers in code still reads both
tables through
ctx.apiandctx.engine.find, served what the generic data door serves — the body as its type's read projection and the hash keyed (abe8f28, #21513) — and is refused the door's filter, sort, group and search shapes over the body or a hash column with400 INVALID_FIELD(2f837a5, #21539). - The data door and the version token. A metadata body's stored content
hash is served, and compared, only as a keyed digest (
713b0fa, #21436). ⚠️ A version token a client held from before the upgrade is refused once with409 METADATA_CONFLICT; take the token from the next read and retry. Filter, sort and group on the two content-hash columns and on the history table'schange_noteanswer400 INVALID_FIELDon the data door, the MCP stdio reader and the analytics door. On the data door, so does any filter that reaches the body or a hash column indirectly (5d0e4e2, #21619).
Migration. Change metadata with PUT /api/v1/meta/:type/:name, and read it
with GET /api/v1/meta/:type/:name and …/history. Delete a body hook bound to
either table, and a flow write node aimed at one; on 17.7.0 neither runs. Filter the
two tables by their scalar columns (the type, the name, the state). Then run
os migrate audit-metadata-bodies, and --apply it, to drop the stored hash
from the audit, activity and decision-audit copies already written. There is
no mechanical rewrite for any of this.
Credentials leave the copies they were made into
- Flow credentials have a write-only channel (
96a9719, #21377). An inbound hook's start-nodesecretand anhttpnode'ssigningSecretare moved by the metadata save door into a new platform object,sys_flow_credential, encrypted through the host crypto provider, masked on every read and read back only when the engine verifies a post or signs a request. Authoring does not change: a save that omits the key keeps the stored secret,''clears it, a new value rotates it. On the first boot with a crypto provider, each stored flow that still carries a credential is moved once and logged ([Automation] flow '…' … was stored in cleartext … ROTATE: …); the run is recorded insys_migrationasflow-credential-channel. ⚠️ Rotate every inbound and outbound flow secret that existed before the upgrade: version history and audit snapshots written before the move keep their copies. With no crypto provider, a save carrying a flow credential answers503 SERVICE_UNAVAILABLE, andPOST /api/v1/automation/:name/clonerefuses a flow that holds one with409 RESOURCE_CONFLICT— a packaged inbound flow can no longer be cloned in one step; author the copy as a new flow with its own secret. Packaged flows are not moved. - The audit ledger omits
internalfields, and the platform's credential columns areinternal(50e1c65, #21301). Neithersys_audit_lognorsys_activityrecords aninternal: truefield any more, and the generic data path stops returning the JWT signing key's private key, bothsys_verificationcredential columns, the two-factor secret and backup codes, the SSO providers' OIDC and SAML blobs, the OAuth token columns, the OAuth client secret digest, the SCIM credential digest, a share link's token and password hash, and the approval action-token digest. ⚠️ Rotate the JWT signing keys, and revoke and re-mint share links that must stay private: ledger rows written before the upgrade are not rewritten. An integration that read one of these columns throughGET /api/v1/data/...reads share links through/api/v1/share-links, and OAuth clients and SSO providers through their auth routes. - Copies are masked like the generic read. Every write response that
returns a record (REST, batch and MCP) carries
SECRET_MASKfor a set credential field andnullfor an unset one (a0176ef, #21816), and so dodata.record.created/data.record.updatedevents, an approval request's stored snapshot, an outbound webhook'sbefore/after/changesand its delivery row, and knowledge-index documents (568dc0b, #21866);internalfields are omitted there. A record-change flow'srecordandpreviousare served on the same terms (1f04696, #21928): a condition that comparesrecord.FIELDwithprevious.FIELDon a credential field sees two equal masks, so read a credential through a privileged binder instead. ⚠️ Resume, cancel or purge paused runs created before the upgrade; their stored variables keep the clear values. - Share-link passwords (
f5b8e29, #21890). The stored hash never leaves the server, new passwords are hashed with scrypt (a legacy hash still verifies and is re-hashed on its first redemption), and the password can travel in thex-share-passwordheader, which the default CORS allow-list now carries. A host that passes its ownallowHeadersadds the header itself. - Settings audit fingerprints for a secret-valued setting use the crypto
provider's keyed digest,
hmac-sha256:…(ba57588, #21809); with no keyed digest the trail records the write with no fingerprint. - Crypto providers (host contract).
ICryptoProvidergains a requiredkeyedDigest(plain)(222ecc2, #21292), andCryptoContexta requiredscopefromCRYPTO_CONTEXT_SCOPES(57cc695, #21453): an implementation withoutkeyedDigest, and a context literal withoutscopein an implementation or a directencrypt/decrypt/rotateKeycaller, stop compiling, andLocalCryptoProviderrefuses a scope-less context at run time withCryptoContextScopeError. New ciphertext carries av2:marker and the older bare form still opens. ⚠️ A secret set or rotated on 17.7.0 cannot be opened by an earlier release, so a rollback past it needs those values set again.os secret rewrap(dry run by default,--applyto write) re-seals the older ciphertext at rest (0557c2f, #21469). Nothing on the upgrade path runs it, and a row it re-seals carriesv2:too, so an applied run has the same rollback cost.
On the write doors, a row the caller cannot read is not there
- By id (
53021e3, #21812). A by-id update or delete of a row the caller cannot read answers404 RECORD_NOT_FOUND, with the body a nonexistent id gets, for every principal class — FROM a403(PERMISSION_DENIED,FORBIDDEN, or a parent-derived gate's code) TO the404. An uploader or a comment author who can no longer read the parent record is now refused the same way. A caller who can read the row but may not write it keeps its403.security/explainanswers the missing-record shape for such a write. - By predicate (
cab6396, #21900). Amulti: trueupdate or delete matches only the rows the caller can read, so a predicate that reaches only hidden rows succeeds with zero rows, and one whose readable match exceeds 10,000 rows is refused400 INVALID_FILTERbefore anything is written. Grant read access before asking a user to change rows, and batch a predicate above the ceiling. - A supplied
organization_idon create (251a7dd, #21680). On a walled posture the insert stamp fills only an absentorganization_id. A create that names another tenant's organization is refused403 PERMISSION_DENIED(it used to answer201and store the row in the active organization), an import row naming one is reported failed, and undergroupa sister organization the caller holds is admitted. Omit the key, or name the active organization. - Guards fail closed (
80f9f7e, #21954). The dispatcher's environment-membership gate and the organization slug guard answer503 SERVICE_UNAVAILABLEwhen their own read faults, where they used to admit the request or the slug change. - Implicit account linking (
41a1135, #21872). An external sign-in links to an existing local user only when that user's email is verified; otherwise it is refused witherror=account_not_linked. The platform identity provider (objectstack-cloud) keeps its exception, and a provider the user unlinked is not linked again implicitly. A deployment that passessecondaryStoragenow also keeps verification values in the database, so a reset link, one-time code or verification link that was in flight in the cache alone at deploy time can no longer be consumed, and its user requests a fresh one. Setaccount.accountLinking.requireLocalEmailVerified: falseto turn the local-verification check off again (an unlinked provider still does not re-link implicitly), after reading the library's account-takeover warning.
Reads and writes serve declared fields, and the engine refuses names it does not know
- Undeclared columns (
5c9138b, #21612;5b5e83f, #21631). A read with nofields— the data door, by-id reads, export, search hits,expanded records andengine.findin process — serves the declared fields, the registry's system columns,id,created_atandupdated_at, and nothing else. The record a write returns, a hook'sctx.resultandctx.previous,data.record.*events, webhookafterbodies and the audit ledger's create and delete values follow the same rule. A field retired in an upgrade leaves its column in the table untilos migrate apply --allow-destructive, and that column's values are no longer returned. ⚠️ Run a conversion that copies such a column into its replacement before upgrading, while the field is still declared, or afterwards read the values withos migrate unmapped-columns --object NAME(759dbe9, #21643), which is operator-only and read-only. No flag re-opens undeclared columns on a runtime door; a reader that needs one declares it as a field. Cloning a record whose table carries such a column now works. - Unregistered object names (
eb9ef79, #21545). The engine's in-process verbs —find,findOne,count,aggregate,insert,update,deleteandvalidate— refuse an object name the registry does not resolve with404 OBJECT_NOT_FOUND, the data door's own envelope, before any hook or driver runs; they used to hand the name to the driver as a table. Register the object first; host code that must reach storage without a registry entry addresses the driver itself. - Readonly values on system writes (
8843505, #21695). A seed, migration orisSystemwrite keeps its exemption from the readonly strip, but the value it keeps is now checked for its type's shape: a seed's'yesterday'on a readonlydatetimeis refusedVALIDATION_FAILEDand counted as a seed error, where it used to be stored as written. Fix the value at its producer.
Metadata write doors: names, locks, hooks and code-defined datasources
- A body's
namemust be its row's name (44defd4, #21536;5555047, #21483). The save, rollback, revert, publish and package-publish doors refuse, with400 VALIDATION_ERROR, a body of any type whose ownnamediffers from the name the row is written under (atranslationsaved withname: ''included). A body with nonamepasses, and a view's missing name is still stamped. Setnameto the save name, or save under the body's name. - View containers have one naming rule at the save door. A container saved
under a name its own expansion produces (
e367002, #21618), under a name another stored container of the same object expands to (7b07749, #21637), or whose save or expanded names are already served elsewhere — another stored container, of any object, or a view item a package ships — is refused400 VALIDATION_ERROR(eea82af, #21648). A package-less container row stored under a packaged view item's name now belongs to no package, so the packaged views it used to replace are served again. A container saved for an object another package ships expands under its own name,OBJECT.CONTAINERandOBJECT.CONTAINER.KEY, with noisDefault(535d1d2, #21430); a navigationviewNameor form-actiontargetthat used an old name now reaches the shipping package's view. And a container'sformis its default form (41b1333, #21535): a container with noformno longer serves its firstformViewsentry as the default create and edit form — in the CRM example that was the anonymous Web-to-Lead form — so move the intended form intoformand re-pointOBJECT.edittoOBJECT.form. - Hooks saved at runtime need a
body(ced217c, #21686;7fd2c34, #21706).PUT /api/v1/meta/hook/:namerefuses, with400 VALIDATION_ERROR, every hook that carries nobody— one whosehandlernames a function, and one with neither. Such a hook used to be stored with a200, and on 17.7.0 it could never bind. Give it a sandboxedbody. - The
_lockgate agrees with the reads. It now runs on a host-config kernel — one with noenvironmentId, as the showcase boots — and answers403 ITEM_LOCKEDthere as on an environment kernel (c43a8ae, #21715).DELETE /api/v1/meta/app/setupis one write it now refuses:setup,studioandaccountdeclareprotection.lock: 'full'. An organization with no row of its own is bound by the env-wide row's lock (cf60dbc, #21737), and an item's lock is the strictest among its stored rows in scope (18c2ddc, #21801) and among the installed packages that ship its name (18fe681, #21844), whatever the row or registration order. The reads report the same answer: a packaged flow, action, object, hook and the other types with no overlay channel readlock: 'full',editable: false,deletable: false(fe10172, #21693), and an artifact's_lock: 'none'no longer masks a stored row's lock (b7a13c7, #21759). A client that gated an edit affordance oneditable/deletablenow hides it where the server refuses. - Code-defined datasources are read-only at the metadata door and at boot.
PUT /api/v1/meta/datasource/:nameon a datasource a package declares in*.datasource.tsanswers403 NOT_OVERRIDABLE(it answered200and stored a row), and so does aDELETEwith no stored row (9cc2c79, #21942). The boot restore no longer lets a stored row displace a code-defined datasource, opens no pool from one, and logs one warning naming it;PUTon/api/v1/meta/datasource/default, and aDELETEthere with no stored row, answer403too, naming the host's database configuration as the remedy (753e7a1, #21965). Change a code-defined datasource in its source, or in the host's database URL fordefault, andDELETEa row the warning names. Until you do, the metadata door's reads in 17.7.0 still serve that row, not the code definition the boot and the admin door use:GET /api/v1/meta/datasource/:name, theGET /api/v1/meta/datasourcelist and theeffectivelayer of/layers. That half of #21922 landed onmainafter 17.7.0 was published (1abfc58, #21985). A runtime datasource saved through the metadata door is also listed and editable through/api/v1/datasources— that fix shipped without a CHANGELOG entry; see Also shipped in 17.7.0.
Page blocks take the shape their renderers read (#21464)
A page block's properties is checked by the component-props gate on
os validate, os build and os lint, which reports a refused value as an
advisory component-props-invalid or component-props-unknown-key finding.
A stored page still saves and loads: a component's properties is not parsed
on the metadata save or load path. These members used to be z.unknown(), so
any value passed and the renderer answered an off-shape one with a silent
default; each now takes the shape its renderer reads, and its TypeScript type
follows. None has a conversion; each row's D3 entry carries the judgement.
| Block · members | Takes | Commit · D3 entry |
|---|---|---|
object-grid exportOptions | the list view's export options object, not a bare format array | 5a9292e (#21287) · ui-object-grid-export-options-closed |
object-grid rowHeight, rowColor, navigation, conditionalFormatting, bulkActionDefs, aggregations, operations | the list view's own shapes; aggregations as [{ field, type }], operations as four booleans | aa46322 (#21463) · ui-object-grid-row-members-typed |
object-map, object-gantt, object-tree navigation | NavigationConfigSchema, { mode, size?, openNewTab?, preventNavigation? } | 529d971 (#21502) · ui-object-map-gantt-tree-navigation-typed |
object-grid fields, selection, selectable, rowActions, bulkActions, batchActions; object-kanban columns; object-calendar calendar | field-name strings, { type }, action names, lanes of one spelling, { startDateField, … } | 7d674df (#21559) · ui-object-grid-kanban-calendar-list-members-typed |
object-form contentLayout, submitBehavior, navigateOnSuccess, mobile | 'simple' / 'tabbed', the form view's submitBehavior, a relative path, the mobile block | 958cfe2 (#21590) · ui-object-form-members-typed |
object-metric aggregate, trend | { field?, function, groupBy? }, { value, label?, direction? } | 3f1bc81 (#21622) · ui-object-metric-aggregate-trend-typed |
object-metric compareTo, drillDown; object-grid columns | { kind }; the drill members without filter / mode; the list view's columns | 72f3c74 (#21673) · ui-object-metric-compare-to-typed, ui-object-metric-drill-down-typed, ui-object-grid-columns-typed |
object-gantt markers; object-timeline mapping; top-level fields of object-form and object-master-detail-form | { date, label?, color? }; { title?, date?, description?, variant? }; field names | 16d241a (#21699) · ui-object-gantt-markers-typed, ui-object-timeline-mapping-typed, ui-object-form-fields-names-typed |
object-kanban conditionalFormatting | the list view's [{ condition, style }] | ced3e1a (#21711) · ui-object-kanban-conditional-formatting-typed |
object-form customFields; sections of object-form and object-master-detail-form | a closed runtime form field; one section shape, canonical spellings only (visibleWhen, a numeric columns) | 1289925 (#21742) · ui-object-form-custom-fields-typed, ui-object-form-sections-typed |
object-metric drillDown.report; object-timeline items; action:group / action:menu members | a ReportSchema report; the entry kind variant selects; a closed inline action (type, not actionType; target, not endpoint; disabled, not enabled) | 4331a6b (#21764) · ui-object-metric-drill-down-report-typed, ui-object-timeline-items-typed, ui-action-group-menu-members-typed |
action:group / action:menu member params | an array, unless the member's type is api; static values go on their own action:button node | 88a39c0 (#21869) · ui-action-group-menu-member-params-array-only |
Two keys are retired with a tombstone and a conversion, and tsc refuses them:
object-gridresizableColumns→resizable, the same boolean (aa46322, #21463). Conversionobject-grid-resizable-columns-removed; D3object-grid-resizable-columns-retiredfor a grid that wrote both with different values.object-master-detail-formdetails[].sortFieldis deleted (6ec54f0, #21632): the grid stamps the child object's first field namedposition,sort_order,sequence,line_no,line_numberorsort. Conversionobject-master-detail-form-detail-sort-field-removed; D3object-master-detail-form-detail-sort-field-retired.
One widening rides with them: an inline object-form field declares the grid
widget's eight camelCase keys (minRows, maxRows, allowAdd, allowDelete,
allowReorder, totalField, addLabel, sortField), and each snake_case
spelling's refusal names its replacement (6fb7115, #21825).
Three more page-level changes. The first and the third are refused at parse,
so they also fail defineStack and the metadata save door; the second is a
properties value, reported like the members above:
ai:chat_windowis retired (48eb9c1, #21531). No renderer for it ever shipped; the floating chat overlay on every page is the AI chat entry point. Delete the node, and set the app'sdefaultAgentwhereagentIdwas meant.AIChatWindowPropsleaves@objectstack/spec/ui. D3ui-ai-chat-window-retired; no conversion, because closing up a region is a layout decision.element:textvariantrefusesheadingandsubheading(36ad321, #21614), the second release of the announced two-release convergence:heading→h2,subheading→h3. Conversionelement-text-variant-heading-levels; D3element-text-variant-heading-subheading-retired. The old spelling is an advisorycomponent-props-invalidfinding and atscerror, and a stored page replays the rewrite when it is read. The rewrite keeps the heading element, buth2/h3draw their own, larger styles.- A page's
requiresis accepted only onhtmlandjsxpages (72af58c, #21547), the kinds whose source is compiled at save; onreact,full,slottedand kind-less pages delete it. Conversionpage-requires-non-compiled-kind-removed; D3page-requires-non-compiled-kind-refused. And on an html page, a literal of the wrong type for a component input is now a compile error (a4fd82a, #21678): writeaggregate={{"function":"count"}}, notaggregate="count".
Reports, dashboards and translations
- Every block of a
joinedreport binds adataset(ed15448, #21712), refused atblocks.N.dataset; such a block never drew anything. A stored report row keeps its bytes, carries the issue in_diagnosticsand is refused on its next save. Studio's report inspector now draws the block'sdatasetas a required dataset picker (9059082, #21819). D3ui-report-joined-block-dataset-required; no conversion. - A
pie,donut,funnel,treemaporsankeywidget with a dimension takes one measure (32d5769, #21425): those types draw one series, so a secondvaluesentry drew nothing. Write atableorbar, or one widget per measure. The check export is renamedcheckDashboardWidgetDimensionlessMeasureArity→checkDashboardWidgetChartMeasureArity, same signature. D3dashboard-widget-single-series-multi-measure-refused. - The widget translation key
subCaptionis retired (99e1912, #21342): deletedashboards.DASHBOARD.widgets.WIDGET.subCaption, and translate card copy under the widget'sdescription. Conversiontranslation-widget-sub-caption-removed; D3translation-widget-sub-caption-retired. An authored widgetoptions.descriptionnow draws theunconsumed-widget-optionwarning. - Action translations.
resultDialog.title,.descriptionand.acknowledgeunder an action that declares noresultDialogare nowtranslation-target-unknownerrors atos validate/os build(1371dc9, #21304). At run time a bound action's copy is read only underobjects.OBJECT._actions.ACTION, never fromglobalActions(3911901, #21344): move such keys, including translations stored at runtime, whichos validatedoes not see. - A field's translated help is served on
description(bab7685, #21956), not on an undeclaredhelpkey, and only while the description still equals the packaged one.ObjectFieldLikedrops itshelpmember.
Agents: the contract is what the cloud AI runtime enforces
The cloud AI runtime refused the memory, structuredOutput and JSON Schema
declarations below before an agent's first turn, and never read lifecycle;
authoring now refuses all four by name. The out-of-repo population was not
measured by any of the changes.
memory(22c2d6f, #21413). WithlongTerm.enabled: true,longTerm.maxEntriesandreflectionIntervalare required (integers of at least 1, no default);reflectionIntervalwithout an enabledlongTermis refused;longTerm.storeis retired whatever it holds. Conversionagent-memory-long-term-store-removeddeletesstore; D3agent-memory-store-retired-and-limits-requiredcarries the two numbers only the author can choose. Retired keyai/Agent:memory.longTerm.store.structuredOutput(3937ad2, #21367) is JSON-only:regex,grammarandxmlleaveformatandfallbackFormat, andcoerce_typesleavestransformPipeline. Usejson_schemawith a JSON Schema, orjson_object. Conversionagent-structured-output-refused-members-removeddeletes a block whoseformatwas retired, a retiredfallbackFormatand thecoerce_typesstep; D3agent-structured-output-refused-members-retiredasks whether that agent should now carry ajson_schemacontract. Studio's agent form now offers the block (ca0dfb6, #21398).lifecycle(6e33b67, #21461) is retired: it was parsed and never read. Delete it; a conversation phase is a skill withtriggerConditions, a multi-step process is a flow, and record transitions are astate_machinevalidation rule.StateMachineSchema,StateNodeSchema,TransitionSchema,ActionRefSchema,GuardRefSchemaand their types leave@objectstack/spec/automation(andStateNodeConfigthe root and/aientries) with no replacement. Conversionagent-lifecycle-removed; D3agent-lifecycle-retired; retired keyai/Agent:lifecycleand the fiveautomation/*defs.- JSON Schema slots (
23365ea, #21353).action.ai.outputSchemaandagent.structuredOutput.schemarefuse a subschema with notypethat carries one of 22 type-scoped keywords (properties,items,pattern, …), at its own path. Declare thetype. D3ai-json-schema-untyped-subschema-refused.
Analytics answers only what it can compute
orderkeys name selected members (1caa603, #21314): a key that is not one of the query'sdimensions,measuresor bucketedtimeDimensionsanswers400 INVALID_FIELDon both strategies and on/analytics/sql. The native face used to answer500, or an arbitrary order on SQLite.limitandoffsetare non-negative integers (6d67ad5, #21399):-1,1.5and the like answer400 VALIDATION_FAILEDat/analytics/query,/analytics/sqland the dataset door; omitlimitfor "no limit". Anoffsetwith nolimitnow runs on SQLite. D3analytics-query-window-non-negative-integer.- The row wildcard
'*'belongs tocountalone (b793010, #21431): a cube measure'ssqlor a dataset measure'sfieldof'*'under any other aggregate, and a cube dimension'ssqlof'*', are refused at parse. A stored dataset carrying one answers400on every query until it is fixed, including queries that select only its other measures. D3analytics-row-wildcard-outside-count-refused; no conversion. - The cube metric types
number,stringandbooleanare retired (99589f9, #21452): give each measure an aggregate (count,sum,avg,min,max,count_distinct), keep a per-row value as a field, and move a ratio to a datasetderivedmeasure. A cube registered in process without the parse is refused by both strategies. D3cube-metric-expression-types-retired; no conversion. - A caller-named measure must name a field (
0b82391, #21474):_sum,*,*_sumand an empty spelling answer400 INVALID_FIELD(they answered500). The console's analytics adapter posts_sumfor a widget whose value field is empty, and now shows that400as an error. - Comparands on the native face follow the engine. A comparand against a
declared boolean field (
8b123c0, #21424) or number field (086ad0a, #21446) is judged by the engine's rule before the statement compiles: an accepted spelling is bound as its value, anything else answers400 INVALID_FILTER. A list under a scalar operator is refused on every face (100c394, #21484, below). - Bounds and temporal values follow the engine (
81e69ca, #21553;1ca1eb0, #21562). The native strategy and the draft preview read a bare-day$lte, a$betweenmaximum or adateRangeend as "through that whole day" only on a declareddatetimecolumn, and the row-level read scope merged into the native statement, like the draft preview, compares a temporal comparand in the column's storage form, so their row sets move — in both directions — onto the engine's answer. A host that buildsAnalyticsServiceby hand passessourceFieldMeta, and a direct caller ofcompileScopedFilterToSqlpasses the driver's coercion pair, to get those answers. - Authoring a cube (
39a912e, #21416;d70353f, #21435).os validate,os buildandos lintrefuse ananalyticsCubesdimension over a JSON-stored or multi-value column, acount_distinctover one, and asum,avg,minormaxmeasure over a column type its aggregate does not take — pairs the analytics door already refused at query time.
Filters at the engine and in row-level security
- Boolean comparands (
9f13c94, #21372;45efcfa, #21404). Against a declaredbooleanortogglefield, atwhere, a per-aggregationfilterandhaving,"true"/"false","1"/"0"and1/0narrow to the boolean they name — so?flag=truenow returns thetruerows — and any other string ("TRUE","yes", a blank), a number other than1/0, aDateor an array answers400 INVALID_FILTER. On PostgreSQL several of these answered500; on SQLite and in memory they matched no row, or every row under$ne. Writetrueorfalse; to match either, use$in. A consumer that switches exhaustively over the verdict'sformgains three cases. - A list under a scalar operator (
100c394, #21484) —$gt,$gte,$lt,$lte, the text operators and the flags — answers400 INVALID_FILTERat every face (400 VALIDATION_FAILEDon the HTTP routes that parse a filter in their body), and the save door refuses a dataset, measure, widget or report filter that carries one. Write one value,$infor "one of" or$betweenfor a range. - Cross-field references in
havingand a per-aggregationfilterfollowwhere: a{ $field }pair across two comparison classes (c2cd651, #21297), or against a column with no class — a file field, a list, a formula (ceb4a93, #21406) — is refused400 INVALID_FILTER, andapplyInMemoryAggregationapplies the same rules when handed a field map. - Bare-day upper bounds (
7aab759, #21336).@objectstack/formula's own whole-day reading is deleted; the sharedlowerFilterConditionapplies it once, on declareddatetimecolumns. The RLS write check now agrees with the read on other columns, so a write the read would hide is refused403. - Row-level policies that cannot be compiled are dropped. A policy that
compares a numeric column (
7aab759, #21336) or a boolean column (8b123c0, #21424) with a comparand outside the accepted set is dropped through the fail-closed route: its read returns no rows, its write is refused403, and a WARN line names the policy. Numeric strings are read as numbers. Acheckthat aims a scalar, ordering or text operator at a JSON-stored or multi-valued field is refused with the read's400 INVALID_FILTER(97239c3, #21317), andsecurity/explainanswers the same (ee75aae, #21371). Test membership withcontains, and compare a numeric column with a number.
Flows, hooks and jobs
- An
approvalnode'sconfigis judged whole at parse (866683f, #21893), againstApprovalNodeConfigSchema: an undeclared key, a refused value (escalation.timeoutHours: 0.5, under its minimum of 1) and a missingapproversare refused atos validate,os compile,defineStack, the metadata save door andregisterFlow.registerFlowalready refused an undeclared key; a refused value used to register there and fail every run that reached the node. A stored flow carrying one is skipped at boot with a warning. D3flow-approval-node-config-contract-refused. - A flow the
kernel:readybind refuses is withdrawn (54fb60a, #21897). It used to stay registered andactivefrom the boot pull, with its trigger bound; nowGET /automation/:nameanswers404and the boot warning carries the located refusal. Correct the config it names. - A hook's string
handlerresolves inside its own package only (98eb3b9, #21653). A name the hook's package does not hold is refused at registration withINVALID_REFERENCE/ 400 and the hook is not bound; it used to fall back to any package's function of that name. Give the hook abody, or declare the function in its own package. os package install(install-local) refuses what it cannot run, with422 VALIDATION_ERRORand nothing installed: an enabled job with nobody(6c5697d, #21584), a hook with nobodyor a jobbodythat does not bind (045b946, #21615), and an enabled job whosepulldoes not bind (83e2fee, #21683). A package installed by an earlier release keeps rehydrating; its handler-only hooks are reported atwarnand not bound. Boot such an artifact withos start --artifact, which loads its runtime module, or give each hook and job abody.- Under
isolatedposture with package-authored scheduled work switched on, a packaged job must declareorganization(909229e, #21668), or it is not scheduled and the error log names it; an unrecognisedOS_TENANCY_POSTUREwithholds every job. - A refused flow resume answers the engine's code (
309224d, #21740) onPOST /api/v1/automation/:name/runs/:runId/resumeand MCPresume_run:INVALID_SCREEN_INPUT,INVALID_SIGNAL,RUN_NOT_FOUND,STORE_UNAVAILABLEandRESUME_IN_PROGRESS, whereerror.codeused to be derived from the status. The statuses do not move.
Approvals
- A position address has one spelling,
position:NAME(c9c555a, #21770, ADR-0090 D3). The pre-rename spelling — the D3-retired word followed by a colon — is no longer read as a position anywhere the service compares a slot with the caller, and a caller that names it asactorIdis refused403 FORBIDDEN; the stock console already sendsposition:NAME. The deprecated approver type with that name, whose membership-tier lookup finds no one, now writes the canonicalorg_membership_level:VALUEslot. Two classes of pending request are now decided only by an admin override: a request a 15.x-era release opened with a slot in the old spelling, and a new one opened from a flow that still authors the deprecated type over a position name. Fix: author{ type: 'position', value: '…' }; an admin approves, rejects or reassigns the stuck requests (via_override: true). sys_approval_action.actor_idholds the person who acted (6f17d1d, #21493): the slot an action was admitted under moves to a newacted_ascolumn, and the action log returns it besideactor_id. A boot-time repair moves slot literals out of storedactor_idvalues, so those rows show the slot and no person. The SLA and dead-run sweeps record no actor where they wrotesystem:sla/system:dead-run, notifications name only a person, andreassign_from/reassign_tobecome text columns of slot addresses (88fb5e8, #21514). A report that readactor_idas the slot readsacted_as; one that tested for thesystem:sentinels reads theescalateandrecallrows.- Approval notifications carry their text in
payload.body(255a777, #21888), the field the messaging service delivers; it waspayload.messageand arrived empty. A tenant-authoredsys_notification_templatefor anapproval.*topic that wrote{{ message }}writes{{ body }}.
Public forms
sharing.enabled: trueis required (6dd99b8, #21566). A form is served onGET /forms/:slugandPOST /forms/:slug/submitonly when itssharingdeclaresenabled: true,allowAnonymous: trueand apublicLinkslug, one rule shared by the endpoints and the organization-scoped save check.enableddefaults tofalse, so a form that set only the other two now answers404 FORM_NOT_FOUND. Addenabled: trueto the form'ssharing, and to any stored overlay of it.- A withdrawal holds at every layer (
3c7785d, #21864). An explicitenabled: falseorallowAnonymous: falseat any layer closes the form, and an organization-scoped save that would re-open a form the env-wide definition withdraws is refused403 NOT_OVERRIDABLE. A package-shipped form that was parsed by the strict stack schema and keeps its link without switchingenabledon is a withdrawal. Between 17.6.0 and this change an organization overlay could re-open such a form; that never shipped in a release. Two limits remain. The form doors may still serve an organization overlay's copy of the form when that overlay was stored before the withdrawal, or restored by a rollback or commit revert, which the save check does not gate: withdraw the form in that overlay too. And a withdrawal closes the view's name in every package that ships a view of that name (#21934). - Walled postures (
a7ab047, #21580;ce53218, #21473). A form whose object is walled by an organization column answers404 FORM_NOT_FOUNDto anonymous visitors (its submit used to answer500); the administrator's read explains why, and declaringtenancy: { enabled: false }on the object is the remedy when its rows belong to no organization. An organization-scoped withdraw or publish there is refused403 NOT_OVERRIDABLE; save it env-wide.
Fields and platform objects
- A
selectorradiofield needsoptionsorpicklist(c52c49d, #21390), refused at parse;Field.select()with an empty list is refused too. A stored row is still served with_diagnostics.valid: falseand its next save is refused;GET /api/v1/meta/diagnosticslists them. Use atextfield if any value is meant to be allowed. sys_accountloses thelink_socialaction (7665c54, #21894), which never completed a link. Link a provider throughPOST /api/v1/auth/link-social(auth.accounts.linkSocialin@objectstack/client).- A member no longer reads a colleague's
Adminfield group onsys_user(1878ef9, #21340):member_defaultandviewer_readonlydeclare it unreadable, so a member's filter or sort on such a field answers403.bannedmoves to theAccountgroup and stays readable. A custom set meant to show members those fields names themreadable: true.
Datasources and the boot store
- The in-memory (mingo) engine is no longer a boot store (
9a4182a, #21598): a boot on it signed a user in and then answered503to every data request. FROM--database-driver memory/OS_DATABASE_DRIVER=memoryTOos dev --fresh; FROM amemory://URL TO:memory:; FROM a default datasource{ driver: 'memory' }TO{ driver: 'sqlite', config: { filename: ':memory:' } }.DATABASE_DRIVER_SELECTION_ALIASESand…_IDSdrop the spellings, andProjectDatabaseUrlSourceloses'memory-driver'. objectstack startvalidates federated objects at boot (bc7747c, #21887). The federation service now reads the metadata service when it uses it, so onstartthe boot gate compares every federated object, and under the defaultonMismatch: 'fail'real drift stops the boot withExternalSchemaMismatchError. Fix the drift, or setexternal.validation.onMismatch: 'warn'on that datasource; runPOST /api/v1/datasources/:name/external/validateonobjectstack devfirst to see it.- "Import as Object" saves through the metadata door (
07e933b, #21837): the imported object is durable and reads from its remote table. A re-import that would drop or retype a field is refused400 EXTERNAL_IMPORT_ERROR(it answered201with an in-memory overwrite); import under a newname, or save the definition throughPUT /api/v1/meta/object/:name?force=true(e864db5, #21874). An import over a datasource whose package declares a namespace must carry that prefix (faf8dce, #21906). - Install-local runs the protocol handshake (
75ddcd1, #21805). A manifest whose declared range excludes this runtime's protocol major is refused with422 OS_PROTOCOL_INCOMPATIBLE(it used to install with a200), and on a restart such a ledger entry is not loaded.POST /api/v1/packagesanswers the same422where it answered500(e83c9f6, #21760).
The CLI
os verifyruns the author-time rules first (f397608, #21364): a stackos validateandos buildrefuse now exits 1 there too, and--jsoncarries the findings undererrors.os verify --jsonwrites one JSON document to stdout; the boot logs move to stderr (5155093, #21381).objectstack generatebinds what you name (11905a4, #21369):flow,actionandapptake--object, andactiontakes--flow, or bind the stack's only object or flow; anything ambiguous is refused with nothing written. Aviewis still named after a declared object.- One-shot commands write nothing they do not report (
3b4efa7, #21389;b206403, #21432).os migrate *,os meta resync,os secret orphansandos storage orphansno longer run the app's seed loader, and every no-write mode boots read-only, so a preview no longer creates a missing table or file. Pointed at a database that was never booted, these commands now answer empty work, exit 0, and name the tables they did not read (aa0d4b9, #21550;1777a9b, #21570); a table that exists but cannot be read still exits 1. Point--database-urlat the deployment's database.createStandaloneStackgainsarmLifecycleSweep. --artifactandOS_ARTIFACT_PATHbeside a config (e909aa0, #21549).os dev -a PATH,os start --artifact PATHandOS_ARTIFACT_PATHserve the named artifact alone, without loading theobjectstack.config.tsbeside it, unless the artifact is that config's own compiled output. Drop the override, or point it at./dist/objectstack.json.- Refusals print once, on stdout (
bf36edd, #21560):os initandos compile(and soos build) no longer repeat a refusal as oclif'sError:block on stderr; read the✗line on stdout. - Plugin signatures are Ed25519 only (
1ac7308, #21534):signPayload,verifyPayloadand the publisher and platform verifiers refuse any other key type, andos plugin signexits 1 with no sidecar. Re-sign an artifact signed with an RSA, EC or Ed448 key.
Settings and host contracts
- Four Localization settings are retired (
0d8ea5e, #21970):date_format,time_format,number_formatandfirst_day_of_week, which nothing read; dates, times, numbers and the week start followlocale. A stored value is kept but not served, aPUT /api/settings/localizationnaming one is refused400 UNKNOWN_KEYfor the whole batch,settings.getrejects withSETTINGS_UNKNOWN_KEY, andOS_LOCALIZATION_*_FORMAT/OS_LOCALIZATION_FIRST_DAY_OF_WEEKare no longer read. - Membership is settled at user creation (
149153c, #21813). Under theautopolicy a user is bound to the default organization when created, the pre-existing-user backfill runs once per deployment (recorded asadr-0093-membership-backfillinsys_migration), and the default organization's owner is bound once (adr-0093-default-org-owner-bind). Asys_userrow inserted straight through the engine is not bound once the backfill is recorded: code that writes users that way writes their membership too.backfillMemberships'limitis now a page size, and the ungatedensureDefaultOrganizationis deprecated forcreateEnsureDefaultOrganizationOnce. - Turso's remote transport takes a resolver that answers a column's
JsonColumnFieldClassorundefinedinsetJsonColumnResolver, in place of a boolean (30af17e, #21282).
Smaller breaking changes
action-name-undefinednow reads arecord:related_listblock'sproperties.actions: each id must name an action of the related object (defined on it, or astack.actionsentry bound to it byobjectName) that declares alist_toolbar,list_itemorrecord_relatedlocation, so a stack that built clean can failos validate,os lintandos build(0fc8087, #21626). Such an id never drew a button; define the action on the related object, or remove the id.- A file field's
accept/maxSizerefusal answers400 ERR_FILE_CONSTRAINTnaming the field (it was500), andFileConstraintErroris constructed as(field, constraint, message)(33f9791, #21751). - Phone OTP with no deliverable SMS service answers
400 SMS_SERVICE_REQUIRED(it was a500with an empty body), and the code joinsErrorCode(a43d90a, #21858). PermissionDeniedErrordeclaresstatus: 403, so a door that readstatusalone answers403where it answered500(520f66f, #21429).- On the
/ai/*routes, a declared path under an undeclared method answers405with anAllowheader (it answered404), andPATCHto a declared route is served (088428f, #21823). - Under an organization wall, install-local's reseed and purge answer
403to a session with no active organization (reseed answered400 RESEED_SKIPPED), and an install recordsseeded: { mode: 'refused' }(e09f1ac, #21780). - A driver error that leaves the engine — including a raw statement's fault and
a lifecycle sweep's — carries a
[statement and bound values redacted]marker in place of the statement and its values (04f0cc4, #21335;d956910, #21384); branch on the error's class andcode. - Text that operators and log filters match changed: the audit failure line now
opens
Audit write FAILED on TABLEand names the lost row (69a12a0, #21383), and many refusals, warnings and field help texts stopped citing a tracker number and state their decision in words. A filter keyed on the old text needs the new spelling.
Every ADR-0087 entry added in 17.7.0
Each conversion and D3 semantic entry registered under protocol major 18 since
17.6.0, and the entry above that carries its migration. One D3 id spells the
ADR-0090 D3 word this site does not print; it is named by its subject, and
os migrate meta --from 17 prints it.
| Kind | Id | Migration above |
|---|---|---|
| D2 | agent-lifecycle-removed | Agents |
| D2 | agent-memory-long-term-store-removed | Agents |
| D2 | agent-structured-output-refused-members-removed | Agents |
| D2 | element-text-variant-heading-levels | Page blocks |
| D2 | object-grid-resizable-columns-removed | Page blocks |
| D2 | object-master-detail-form-detail-sort-field-removed | Page blocks |
| D2 | page-requires-non-compiled-kind-removed | Page blocks |
| D2 | translation-widget-sub-caption-removed | Reports, dashboards and translations |
| D3 | agent-lifecycle-retired, agent-memory-store-retired-and-limits-required, agent-structured-output-refused-members-retired, ai-json-schema-untyped-subschema-refused | Agents |
| D3 | analytics-query-window-non-negative-integer, analytics-row-wildcard-outside-count-refused, cube-metric-expression-types-retired | Analytics |
| D3 | the approvals position-address entry | Approvals |
| D3 | by-id-write-unreadable-row-not-found, predicate-write-unreadable-row-not-matched | Write doors |
| D3 | dashboard-widget-single-series-multi-measure-refused, translation-widget-sub-caption-retired, ui-report-joined-block-dataset-required | Reports, dashboards and translations |
| D3 | flow-approval-node-config-contract-refused | Flows, hooks and jobs |
| D3 | flow-trigger-record-credential-masked | Credentials |
| D3 | flow-write-node-stored-metadata-target-refused, hook-body-stored-metadata-target-refused | Stored metadata |
| D3 | element-text-variant-heading-subheading-retired, object-grid-resizable-columns-retired, object-master-detail-form-detail-sort-field-retired, page-requires-non-compiled-kind-refused, ui-ai-chat-window-retired | Page blocks |
| D3 | the seventeen ui-object-* and two ui-action-group-menu-* entries in the page-block table | Page blocks |
The release also registers the retired keys ai/Agent:lifecycle,
ai/Agent:memory.longTerm.store, ui/ObjectGridProps:resizableColumns and
ui/ObjectMasterDetailFormProps:details.sortField, and the retired defs
automation/StateMachine, automation/StateNode, automation/Transition,
automation/ActionRef, automation/GuardRef and ui/AIChatWindowProps.
New capabilities in 17.7.0
Jobs carry their own code, and can pull a connector. JobSchema.body is the
sandboxed JavaScript body hooks and script actions carry — ctx.api under its
declared capabilities, ctx.log, the job's own timeoutMs as its one limit
(f1e4ae5, #21538) — and handler is deprecated beside it. Job bodies are
scheduled on every door that brings an artifact in: the boot and install-local,
on install and on every rehydrate (6c5697d, #21584); a package's jobs stop
with it through the runtime.package-jobs uninstall cleanup, and two packages
may declare a job of the same name (6946f2f, #21633). A third run form,
pull: { mapping }, pulls a mapping's connectorSource through the import
runner with no code, and organization names the organization a job runs as
(909229e, #21668); IAutomationService.pullConnectorSource is the new
contract behind it.
Install-local runs what it installs. os package install ARTIFACT binds the
app's script action bodies and body hooks (1d0600b, #21401) — closing 17.6.0's
known issue — announces metadata:reloaded so the package's record-change flows
fire and its permission sets are projected without a restart (ab52182,
#21488), runs the registered uninstall cleanups so its permission sets and
grants go with it (74281a8, #21512), withdraws the package from the running
kernel on uninstall (901e7cf, #21581), and purges sample data through the
engine — under an organization wall, only the caller's organization's rows
(d7fff21, #21773). The listing
reports sample data per organization and marks a package the runtime refused to
load (c4d5713, #21820; 48297ad, #21833). bindAppArtifactHandlers is the new
runtime export behind the first.
Share links and attachments. A record's owner, or an explicit Modify-All
holder, may mint a share link on a record the data door refuses them, outside
the walled postures (4c8363f, #21447), and a plain member's own share-link list
answers instead of an error (db3fee3, #21403). A user who can edit a record
may delete another user's attachment on it, as the attachment gate declares
(3eb38ae, #21753), through a new contributeOwnershipFloorAlternates seam on
the security service, which ISecurityService now declares together with
discardPermissionSetOverlay (045f764, #21781).
Auditing and access. A new capability, view_all_audit_log, exempts its
holder from the compliance ledger's parent-record read gate; platform
administrators hold it by default, so the deletion and sign-out trail is
readable again by them (7ebb543, #21296). An action can declare
requiresMembershipReach, lowered into its visible predicate from the new
MEMBERSHIP_REACH table, and the organization's member, invitation and team
actions now appear only for the grades the server admits (607463d, #21883).
ApprovalActionRow declares acted_as (72217cd, #21479).
Operator commands. os secret rewrap re-wraps older sys_secret ciphertext
under the current AAD derivation (0557c2f, #21469). os migrate unmapped-columns --object NAME reads a retired field's leftover columns by record id
(759dbe9, #21643). os migrate resume can resume an interrupted
os migrate recorded-by run, and every os serve boot reports interrupted
migration runs (550f4cc, #21527; 10454b3, #21554). A plain os dev now
self-heals safe schema drift on restart and provisions the telemetry sibling
database (025008a, #21766). objectstack generate picklist NAME scaffolds a
shared option list, and init and the blank starter wire src/picklists
(bcd68a2, #21167). os environments runs on the os cloud login session
(4b20c84, #21400).
Authoring. A flow screen field's help text is translatable as
inlineHelpText (ecb6ca0, #21386), and the flows translation group is live:
the screen-flow runner names the flow by flows.FLOW.label (aead296,
#21859). Studio's forms offer an agent's structuredOutput (ca0dfb6,
#21398), an object's imageField, which the record header now draws
(2df3d13, #21854; 07bf21f, #21824), and an action's onSuccess and
outcomeMessages (8e35895, #21901). deriveInlineRowFormFields and
isInlineRowFormOffered (@objectstack/spec/data) state what an inline
master-detail grid's row form draws (dcc5ef4, #21256), and the MCP server's
serverInfo.version is the package version (6cf1154, #21548).
Notable fixes in 17.7.0
These are the fixes an upgrading deployment is most likely to notice.
Everything else is in the per-package CHANGELOG.md files.
Security.
- Driver errors no longer carry the failing statement or the caller's values
past the engine, in thrown errors, the driver's own refusal log lines or
operator-facing records (
04f0cc4, #21335;d956910, #21384;6d728b8, #21414;85e29b8, #21482). Any in-process logger of a caught error printed them before. - Field-level reads are narrowed on more surfaces: the object-schema mask
removes a denied field's references from the whole served document
(
a6a7547, #21743); an activity row whose every changed field the reader is withheld is no longer served (3bddd4a, #21427); and a field-narrowed search no longer matches through a field outside the set (0728cbf, #21930). The mask also judges anobjectOverrideparam against the object it names, so a delegated admin is now served the invite action (e6dc7a2, #21904), and global search skips the objects and fields the caller cannot read instead of answering403(87712ab, #21879). - A write refusal on an attachment or a comment no longer names a parent record
the caller cannot read (
50b5e03, #21769), and a by-id write of a hidden row answers as a missing one (above). - A withdrawn public form is refused on both anonymous form routes and creates
no record, and both routes refuse the request, instead of serving the form,
when a service they need to resolve it is registered but cannot be reached
(
49524f6, #21420). - The stored-metadata family's credential material stays behind the door: keyed content hashes, refused evaluate shapes, projected reads for host code and flows, and no access for app-authored bodies (above).
- Credentials leave the copies they were made into: the audit ledger, write
responses, events, webhooks, approval snapshots, flow trigger records and the
share-link password hash
(above); the datasource
read redaction identifies a driver the way the write door does (
fb69825, #21963). - A hook's
handlername can no longer bind to another package's function (98eb3b9, #21653), an in-process verb can no longer address an unregistered table by name (eb9ef79, #21545), and a plugin signature labelleded25519is one (1ac7308, #21534). - Discard Overlay no longer deletes the only stored row of a permission set
saved into a writable runtime package (
5e0b489, #21873).
17.6.0's known issues. Each one listed on the 17.6.0 page is resolved or closed:
- the
--storedandaudit-metadata-bodiespreviews no longer write to the database (3b4efa7, #21389), nor does any one-shot command's boot (b206403, #21432); - a locally installed package runs its script actions and body hooks
(
1d0600b, #21401), and a hot install fires its flows and projects its permission sets (ab52182, #21488); os verifyrefuses whatos validaterefuses (f397608, #21364), and--jsonwrites clean JSON (5155093, #21381);- "My Pending" lists a request routed to a position, its holder sees
can_actand decides it from the console (6d487d2, #21378;5e58193, #21410); - a cloned packaged flow reaches Studio through objectui#11553, carried in the
ab1879721595pin (1cbe165, #21625); - anonymous endpoints: #21158 was closed as not planned on 2026-10-04, on the
maintainer's ruling that there is no demand, so an app-declared
authRequired: falseendpoint still cannot read or write objects; - the four console issues are fixed in the first pin,
89cad75d5570(8963dbf, #21380).
Automation and approvals.
- A flow saved through
PUT /api/v1/meta/flow/:nameis armed on the running engine at once (73b2246, #21746); it used to wait for a restart. - A pure reorder of an object's
fieldsis a change: the content hash keeps the field order, so publishing a drag-to-reorder now saves it (e1790fd, #21814;0fe0a59, #21852). - A metadata publish promotes only the draft its gate judged; a draft saved in
between is refused
409 METADATA_CONFLICT(c9761cd, #21962). - An email template edited through the metadata door keeps the admin's wording
across a restart (
08adfea, #21818).
Data, drivers and seeds.
- On MySQL,
sys_packagesis created and written, so installed and edited packages survive a restart, and a failed write answers the failure (0e10be6, #21273); an uninstall whosesys_packagesdelete is refused removes nothing (1fd5664, #21438). - Deleting an organization, a business unit or a user no longer fails on a
deployment with a federated object bound (
f243a29, #21917;13a22d0, #21937), and a runtime schema sync sends no DDL to one (26d710e, #21796). - A
Field.dategrouped by day, week, month, quarter or year buckets as its own calendar day on PostgreSQL and MySQL (440cd32, #21611); SQLite groupsweekin SQL (5d095a0, #21629). - A per-organization seed replay gives each organization its own row ids, so on
a walled deployment the organizations created after the first get the app's
fixed-id seed rows
(
ff16740, #21688); a seed's authoredcreated_atis kept on first insert (be55fd2, #21661); replayed seed rows are handed to the platform admin on every boot (f9a8eb8, #21503). - A SQLite connect no longer rewrites a file that is already
auto_vacuum=INCREMENTAL(da40a5f, #21744).
Analytics. The ObjectQL strategy applies order, offset and limit, and the
dataset door no longer applies offset twice (fbe2deb, #21363). The SQL echo
prints a date bucket only in the expression the driver groups by, and answers
501 NOT_IMPLEMENTED where none stands for it (35dfb81, #21587; 1968d5e,
#21645; 31e3e00, #21664).
Auth and Setup. A TOTP enrollment names the deployment, not the auth
library, as its issuer (1c3a4d9, #21752). Setup → Users opens on "All Users"
(f76c622, #21971). A cloned permission set no longer logs a false
permission_set_declaration_unowned warning (234d1d8, #21692), an org-owned set,
a clone and a runtime-package set edit again (c9be1f1, #21857), and the
packaged-set lock tells the admin to clone (833d57c, #21902). A create no
longer reports a middleware-filled organization_id in droppedFields
(5259a35, #21701).
The CLI. os migrate recorded-by, resume and account-issuer print one
--json document and exit 0 on success (2ee8383, #21495); a refusal prints
one error line (5895119, #21522; 24dc7c1, #21541); a project whose database
does not exist yet gets empty work and exit 0 (aa0d4b9, #21550; 1777a9b,
#21570); no one-shot command mints a data key file (25797a1, #21497;
2df621a, #21507); os migrate plan boots a config whose connectors need a
requires provider (6afb1b5, #21739); a narrowed --object run records no
deployment-wide ADR-0104 flag, and an unknown --object is an error (417443e,
#21662); os verify samples a multi-valued select as a list (bee8d1c,
#21526).
New in Console (Studio) — objectui pins in 17.7.0
Five pin moves carry the console half of this release:
31971ff1e28f → 89cad75d5570 (8963dbf, #21380),
89cad75d5570 → ab1879721595 (1cbe165, #21625),
ab1879721595 → 2e818d0b51ec (100f68b, #21710),
2e818d0b51ec → 9dfaca654311 (1354e7b, #21800) and
9dfaca654311 → 0abd4f9f8769 (8832655, #21827). Together they carry 229
releasing objectui changesets (74, 111, 17, 24 and 3) of the 254 added across
173 objectui commits; 25 changesets release nothing, and 13 commits carry no
changeset. The per-commit lists are in packages/console/CHANGELOG.md under
## 17.7.0; the second pin's list stops at 100 of its 111 releasing
changesets.
- 17.6.0's console issues are fixed in the first pin: the dataset designer
no longer writes
field: ''(objectui0858267e4), an External or Validate-only datasource saves without a credential (objectui8001068b9), a published html page that gains a plugin component publishes again (objectui3ae919307), and a region-tagged language code such as zh-CN reaches its base language's catalogue (objectuid0fba91aa). - Studio reaches what it could not. The organization's own flows that belong
to no package, a cloned packaged flow among them (objectui#11553); a joined
report block's dataset through a
ref:datasetpicker (objectui#11601); and a read-only flow canvas opens its inspector read-only again (objectui#11546). - Saves say when they are refused. A refused save, pin, reorder, view setting, report save, publish or discard is shown to the user, and the Create View dialog no longer closes as if the view were saved; "Save as view" saves a Kanban view the platform accepts, and Create View makes chart views it accepts (objectui#11578, objectui#11581, objectui#11576). A refused metadata save shows the server's message and field path on every transport.
- Pages and forms. The record header draws the record's picture from the
object's
imageField(objectui#11383); create and edit no longer open a container's first named form when it declares no default form; the defaultsimpleobject-formdraws a self-describing inline section entry (objectui#11615);record:detailsedits atextareaand amarkdownfield in a multi-line editor; the record dialog draws aform.sections[].groupsection; and anobject-gridhonourskeyboardNavigation,descriptionandemptyState. - Data entry and lists. The import wizard's "Download template" downloads
the server's
.xlsxtemplate (objectui#9600); "Is empty" / "Is not empty" are written as the spec's$emptyoperator in the filter builders, and sent asisempty/isnotemptyby the list view's live query (objectui#10813); the action success toast is composed from the action'soutcomeMessages, thensuccessMessage; a percentage is scaled at the storage its field declares; and the screen-flow runner names the flow by its translated label (objectui#11092). - Approvals. The console names a position approver in the
position:NAMEspelling the server stores (objectui#11455), the spelling 17.7.0 now requires (Approvals).
⚠️ Console hosts and authors: 65 of those entries are declared breaking
upstream (16, 43, 2, 4 and 0), and one more commit carries ! with no
annotation in its changeset (objectui b403bb36f, objectui#8347). They are
objectui's own surfaces — they matter to a host that builds on @object-ui/*
packages, runs the objectui CLI, or authors objectui page JSON directly. None
registers an ADR-0087 migration on this side, and none of the node type keys
they retire is a member of @objectstack/spec's PageComponentType or a
ComponentPropsMap row; where an entry mirrors an ObjectStack key, the
ObjectStack retirement carries the ledger entry. How this repository answers
each class:
| objectui change (commit) | How ObjectStack answers |
|---|---|
Node type keys retired: the bare tree and view, 28 bare field-widget fallbacks (990a2d616); pie-chart, donut-chart, radar-chart, page-header (ad1785c1d); scatter-chart, dashboard-grid, the bare metric / metric-card, four builder-chrome keys, form-analytics, import-wizard, related-list, shared-view-link (37140f4f5); spec-report (9d9ed5495); ten sidebar-* keys (1c8403692); navigation-renderer, responsive-grid (9d1c0bff9) | None is an ObjectStack component type; a stored page reaches one only through PageComponentSchema.type's open string arm. Where a retirement names a replacement it is object-tree, object-view, field:TYPE, chart with chartType, page:header, record:related_list or grid. |
Authored props go in the properties bag, and the flat spelling is refused, for flex and object-grid (138ad4554, 6aa029b63) | The shape ComponentPropsMap already declares. |
conditionalFormatting on object-grid, list-view and object-kanban takes only { condition, style } (6f5719e1c, c73cdb569) | The list view's own rule; this release types the kanban member the same way (ced3e1a, #21711). |
A dataset-less provider: 'object' metric widget, and an object-metric in a widget's legacy component envelope, draw the retired-format prompt (160c6c6ea, 83e3f8377) | ObjectStack's dashboard widget schema has required dataset since 9.0.0, and refuses a widget's component key by name; no stored ObjectStack dashboard carries either form. |
A dataset-bound widget stops reading chartConfig.series / xAxis / yAxis, and chartConfig.type and those three are TypeScript errors (1a88ce22f) | Mirrors keys ObjectStack retired and tombstoned on the dashboard widget before 17.7.0. |
Drill-down reports: the { name } arm and the pre-9.0 object-bound report are retired; the drawer scopes a dataset-bound report by runtimeFilter (9ed8d0f1c, 8366accd1) | object-metric drillDown.report is now ReportSchema (4331a6b, #21764): every report it admits is one the drawer draws. |
Layout value sets: grid breakpoint columns and counts, stack / flex / grid gap, container padding (2d576e46e, aea682a31, 4abc0aafa, 3f6efd640); a page refuses maxWidth / padding (a1a44d621) | objectui's own layout nodes; no ObjectStack page shape declares these keys. |
The grid form field's eight keys are camelCase, the snake_case spellings refused (2abec3a96, objectui#11614) | The runtime form field already refused the snake_case spellings; this release declares the camelCase ones (6fb7115, #21825). |
A form view's subforms[].columns entry is judged by InlineGridColumnSchema; ObjectFormSection.fields gains the form view's { field } arm (9db9ff3f9, 9dfaca654) | The shape ObjectStack has enforced on the form view; its accept set does not move. |
A percentage is scaled at the storage its field declares, through the spec's percentScaleOf (f560ded15) | The spec's own rule; nothing to author. |
@object-ui/cli retires create, lint, test, studio and add, analyze's two flags, and generate's --from and --output (37268aae9, ea3914139, 1fe05ff37, 9de0b3483) | objectui's own CLI; os is unaffected. |
TypeScript surfaces: designer node members and props (063832222, 5988b6b53, 0e9058b95, c4ab6d09a), SidebarSchema (ca3de7272), ObjectGridSchema's zod mirror (0d723a33f), app-schema-renderer (fcdc8ec91), mergeAuthoredPresentation / axisPresentation (f9c8c4e45), DeclaredNode (83e3f8377), PartialSchema (8b14aecbd), and BaseSchema's index signature (b403bb36f) | No code in this repository imports @object-ui/types or compiles against these node types. A designer relationship's onDelete is respelled deleteBehavior, the spelling ObjectStack's lookup fields already use. |
A declared gate that cannot be evaluated is a fault, not "no gate" (063119f2b); objectui's app document refuses mobileNavMode (e100589f3) | objectui's own gate evaluator; ObjectStack's app shape does not declare mobileNavMode. |
Shipped in 17.6.0 — listed again in 17.7.0's CHANGELOG
One of the changesets in 17.7.0's CHANGELOG.md files describes code that was
already published in 17.6.0: 748b240 (#21270) is an ancestor of the 17.6.0
version commit 617f25f8, which did not consume its changeset. The 17.6.0 notes
already describe it under Also shipped in
17.6.0. A
deployment on 17.6.0 already runs it, and nothing changes when it moves to
17.7.0.
748b240(#21270) —ScheduledWorkPolicy.hostDisabledReasonandscheduledWorkDisabledReason(policy)(@objectstack/types), so a kernel a host turns off reports the host's own reason.
Also shipped in 17.7.0 — not in its CHANGELOG
The publish ran from the version commit 4e4e8814 itself (Release run
37458970237), so no commit after it shipped. Two commits landed on main after
the Version Packages PR's last refresh and before it merged, between 10:34 and
10:41 UTC on 2026-10-06. Both are ancestors of the version commit, so the
17.7.0 npm packages carry them, but the version commit did not consume their
changesets, and no 17.7.0 CHANGELOG.md entry names them. Their changesets are
still in .changeset/, so the next release's CHANGELOG.md will list them
again. This is the same window that produced the stragglers of 17.5.0 and
17.6.0 (#21361). The release-integrity audit that card added (7b21af8,
#21373) named both changesets in a warning on the publish run, which it never
holds.
8a399b2(#21977, for #21923) — the datasource admin door and the metadata door agree on a runtime datasource (@objectstack/service-datasource). The admin door servesorigin: 'code'only for a name the host registers from code, andruntimefor every other name, whatever the stored record says, so a datasource saved through/api/v1/meta/datasource/:nameis listed and editable through/api/v1/datasourcesafter a restart and gets its live pool. A metadata-door write now reaches the admin door's registry in the same boot. And the admin door stamps the checksum the metadata door's optimistic lock compares, so an admin-created datasource can be edited and removed through/api/v1/meta/datasource/:nameinstead of answering409 METADATA_CONFLICT; a row stored before this release becomes editable there after one edit through the admin door.04e776b(#21976, for #21968) —App.defaultAgent's docblock in@objectstack/specnames the agent route,POST /api/v1/ai/agents/:agentName/chat, as the one chat door, and says the console's chat dock is what reads the key. No schema, type or accept-set change.
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.
17.7.0
⛔ 17.6.0 → 17.7.0 has not been exercised. No upgrade of an application was run for this page. Every line below is derived from a Migration note in Breaking changes & migration in 17.7.0 or from a changeset of this release, and is marked not exercised: accurate about what changed, unproven about what it costs to cross. A step nobody has run, presented beside steps that were, is how a reader finishes a checklist and believes they are done — so this list claims nothing it has not been given.
Before you upgrade
- Run any conversion that reads a retired field's leftover column — a hook,
flow, webhook receiver or script that copies an old column into its
replacement — while that field is still declared. After the upgrade no runtime
door returns the column;
os migrate unmapped-columns --object NAMEreads it. Not exercised. - Find app-authored bodies and flows that touch
sys_metadataorsys_metadata_history— hook bodies bound to them, body reads and writes throughctx.api, and flow write nodes aimed at them — and move each to the metadata API. Not exercised. - Add
sharing.enabled: trueto every public form that must stay public, in source and in stored overlays; without it the form answers404 FORM_NOT_FOUND. Not exercised. - List flows whose
approvalnode config breaksApprovalNodeConfigSchema(os validatenames each), and approval steps authored with the deprecated approver type over a position name; fix them first, because the stored flow is skipped at boot and the new requests need an admin override. Not exercised. - Note every pending approval request whose slot is stored in the pre-rename position spelling; after the upgrade only an admin override, or a reassignment to the position's holder, decides it. Not exercised.
- Find code that addresses an object by a name the registry does not hold through the engine, and register the object. Not exercised.
- If you may need to roll back past 17.7.0, keep a way to set again every
secret you set, rotate or re-wrap on it: an earlier release cannot open the
new
v2:ciphertext. Not exercised.
Getting onto the release
- Move all the
@objectstack/*pins as one set and regenerate the lockfile — Moving the dependency pins. Not exercised. - Leave the protocol declarations on 17:
engines.protocol: '^17'and a^17.0.0specVersion. 17.7.0 still implements protocol 17. Not exercised. - Run
os migrate meta --from 17, thenos migrate meta --stored --apply, for the new conversions — agentlifecycle,memory.longTerm.storeand the retiredstructuredOutputmembers,element:textheading/subheading,object-gridresizableColumns, master-detaildetails[].sortField, pagerequireson non-compiled kinds and the widget translationsubCaption— and read the D3 entries it lists as manual changes. The previews no longer write to the database. Not exercised. - Run
os migrate audit-metadata-bodies, thenos migrate audit-metadata-bodies --apply, to drop the stored content hash from the audit, activity and decision-audit copies. Not exercised. - Replace an in-memory boot store —
--database-driver memory,OS_DATABASE_DRIVER=memory, amemory://URL or a default datasource{ driver: 'memory' }— withos dev --fresh,:memory:or a SQLite datasource. Not exercised.
After the first boot
- Rotate every inbound and outbound flow secret once the boot log has moved
it into
sys_flow_credential, and hand the new value to whoever signs posts to the hook or verifies its deliveries. Not exercised. - Rotate the JWT signing keys, and revoke and re-mint share links that must stay private; their earlier values may have copies in the audit ledger. Not exercised.
- Resume, cancel or purge paused flow runs created before the upgrade; they still hold clear credential values. Not exercised.
- Optional, and only once you will not roll back past 17.7.0: run
os secret rewrap, thenos secret rewrap --apply, to re-seal oldersys_secretciphertext. Nothing on the upgrade path runs it. Every row it re-seals carriesv2:, which an earlier release cannot open, so--applyremoves the rollback path for those secrets: a rollback past 17.7.0 needs each of them set again. Not exercised. - On
objectstack startwith federated objects, expect the boot gate to compare them; fix any drift it names or setonMismatch: 'warn'. Not exercised. - Check the boot log for a stored datasource row skipped under a code-defined
name, and
DELETE /api/v1/meta/datasource/:nameit. Not exercised.
Metadata and build — run os validate before you ship
- Fix the new parse refusals:
select/radiowith neitheroptionsnorpicklist; agentmemorywithoutmaxEntries/reflectionInterval, a non-JSONstructuredOutputandlifecycle; untyped JSON subschemas with a type-scoped keyword;ai:chat_window;requireson a non-html page;joinedreport blocks with nodataset;pie/donut/funnel/treemap/sankeywidgets with a dimension and two or morevalues; the widgetsubCaptiontranslation key; cube metric typesnumber/string/boolean;'*'outside acount; hooks with abodyand flow write nodes on the stored-metadata tables; andapprovalnode configs. Not exercised. - Fix the new author-time errors:
resultDialogtranslation keys under an action with noresultDialog, cube members over JSON-stored or incompatible columns,record:related_listaction ids that name no drawable action of the related object, and html-page literals of the wrong type. Not exercised. - Read the new
component-props-*advisories on page blocks and rewrite each member in the shape the page-block table names; renameresizableColumnstoresizable, deletedetails[].sortField, and writeelement:textvariantash2/h3where it readheading/subheading. Not exercised. - Move a bound action's translation from
globalActions.ACTIONtoobjects.OBJECT._actions.ACTION, including translations stored at runtime. Not exercised. - Give a view container its default
formwhere it relied on the firstformViewsentry, and re-point references fromOBJECT.edittoOBJECT.form. Not exercised.
Data, API clients and integrations
- Read
404 RECORD_NOT_FOUNDfrom a by-id write as "no row you can see", and expect predicate writes to skip hidden rows and to refuse a readable match above 10,000 rows. Not exercised. - Stop sending an
organization_idof another organization on create under a walled posture. Not exercised. - Write boolean filters as
true/false(or1/0), a list only under$in/$nin/$between, and analyticslimit/offsetas non-negative integers withorderkeys the query selects; review row-level policies that compare a numeric or boolean column with a value outside the accepted spellings ('9999-12-31'on a number,'yes'on a boolean). Not exercised. - Stop reading credential columns from
GET /api/v1/data/..., and expectSECRET_MASKin write responses, events and webhook bodies. Not exercised. - Read
sys_approval_action.acted_asfor the slot, and stop testingactor_idforsystem:sla/system:dead-run; write{{ body }}inapproval.*notification templates. Not exercised. - Drop
date_format,time_format,number_formatandfirst_day_of_weekfrom any client that writes the Localization settings. Not exercised. - Take the next version token from a read when a held one answers
409 METADATA_CONFLICTonce after the upgrade. Not exercised.
Deployment, auth and the CLI
- Expect external sign-ins to an unverified local email to be refused
(
error=account_not_linked), or setaccount.accountLinking.requireLocalEmailVerified: falseknowingly. Not exercised. - Add
X-Share-Passwordto a custom CORSallowHeadersif a cross-origin client sends share-link passwords. Not exercised. - Under
isolatedposture with package scheduled work on, declareorganizationon each packaged job. Not exercised. - Re-sign plugin artifacts signed with a non-Ed25519 key. Not exercised.
- Drop
os dev -a/os start --artifact/OS_ARTIFACT_PATHoverrides that relied on the config beside them loading, and pass--object/--flowtoobjectstack generatein a stack with several objects or flows. Not exercised. - Give packages you install with
os package installabodyon every hook, and abodyor a bindingpullon every enabled job, or boot them withos start --artifact. Not exercised.
Application code and custom hosts
- Implement
ICryptoProvider.keyedDigestand passCryptoContext.scopein a custom crypto provider or a directencrypt/decrypt/rotateKeycaller. Not exercised. - Rename
checkDashboardWidgetDimensionlessMeasureAritytocheckDashboardWidgetChartMeasureArity; drop imports ofAIChatWindowPropsand theStateMachineSchemafamily; return aJsonColumnFieldClassfrom a custom TursosetJsonColumnResolver. Not exercised. - Write membership yourself for users inserted straight through the engine,
and call
createEnsureDefaultOrganizationOnceinstead ofensureDefaultOrganization. Not exercised. - Pass
sourceFieldMetato a hand-builtAnalyticsServiceto get the engine's answer for bare-day bounds on the native face, and the driver's temporal coercion pair to a directcompileScopedFilterToSqlcall. Not exercised.