v16.0.0
One org identifier for hook and action authors, quorum and per-group sign-off (会签) approvals, time-relative automations that actually fire, filtered roll-ups, strict dashboard widgets, an identity-scoped MCP stdio transport, and a metadata-driven approvals inbox — plus a large enforce-or-remove sweep that makes dead metadata loud. Backend and Console notes for 16.0.0 and 16.1.0.
The v16 line converges the developer surface and makes declared metadata
honest: hook and action bodies read the caller's org under one blessed name
(organizationId), approvals grow real multi-approver governance (M-of-N
quorum and one-from-each-group 会签) with decision actions declared as
metadata instead of hand-written buttons, time-relative business rules
("remind me 60 days before the contract ends") become a first-class trigger,
and a platform-wide enforce-or-remove sweep turns silently-ignored keys —
dashboard widget typos, dead hook events, phantom webhook triggers, unknown
requires capabilities — into loud errors at authoring time.
Release status: 16.0.0 is released. It was published on 2026-07-21, closing a train that ran through
16.0.0-rc.0and16.0.0-rc.1(cut 2026-07-19 and 2026-07-20).changeset pre exitran with that cut, so the@objectstack/*packages no longer publish as16.0.0-rc.N. The v16 line is closed:16.1.0followed on 2026-07-22 and is its final release, and the current series is 17.0.0. This page covers both releases — 16.0.0 first, then What's new in 16.1.0. (15.1.1 was a small patch on the previous line — better-auth family pinning and auth-plugin init isolation — covered by the v15 page.)
Highlights — 16.0.0
- Ask for approval like a real organization. Approval steps now support M-of-N quorum and per-group sign-off (会签) — one approver from each named group, tallied against an open-time snapshot with OOO substitution — a single rejection still vetoes, and thresholds clamp so a misconfiguration can never deadlock a request. Decisions can carry file attachments, progress ("2 of 3 · finance pending") is computed server-side, and notification links deep-link straight into the request drawer.
- The approvals inbox is now metadata, not hand-written buttons.
Approve / reject / reassign / send-back / request-info / remind / recall /
resubmit are declared
type:'api'actions onsys_approval_request; the Console's generic action runtime renders and executes them anywhere the object appears — new decision capabilities ship as backend metadata with zero Console changes. - "Alert 60 days before
end_date" finally just works. A flow start node can declare atimeRelativedescriptor (offsetDays: [60, 30, 7]orwithinDays); a daily sweep launches the flow once per matching record — no more date-equality conditions that only fire if someone happens to edit the record on exactly the right day. - Roll-ups can count only what matters.
summaryOperations.filterlets a parent total aggregate just the matching children (sumofreceivedlines,countofsignupengagements) — and re-aggregates when a child moves in or out of the predicate. - Silent no-ops became loud errors. Strict dashboard widgets name the
offending key and point at the dataset shape; hooks subscribing to
never-fired events, webhooks on triggers with no event source, validation
rules on
delete, unknownrequirescapability tokens, and a batch of dead field/object/agent properties now fail at parse/validate time instead of parsing and doing nothing. - One name for the caller's org.
ctx.user.organizationId/ctx.session.organizationIdacross hooks and action bodies — matching theorganization_idcolumn andcurrent_user.organizationIdin RLS — with the confusingtenantIdalias removed. record.due_date == today()now matches. The formula engine rewrites temporal equality comparisons so the most natural due-today predicate stops silently returningfalse— plus advisory type-soundness warnings when an expression does arithmetic on a text/boolean field.- Ops-grade diagnostics without a tracing stack: opt-in
Server-Timingdecomposed into auth / db (with query count) / hooks / serialize spans, per-request admin-gated viaX-OS-Debug-Timing, with an admin-only slowest-queries breakdown. - A safer, faster sandbox. Hook/action budgets meter script CPU-time
instead of wall clock (a slow host or nested write chain no longer trips
the 250ms budget), the runner honors a body's declared
timeoutMs, and the QuickJS engine drops asyncify for the leaner sync variant. - MCP grows an authoring loop and loses its last unscoped surface: a
validate_expressiontool lets agents lint a formula against the real schema before saving it, the dev banner prints a ready-to-pasteclaude mcp addcommand — and the long-lived stdio transport now requires an API-key principal (fail-closed) instead of reading data unscoped.
Highlights — 16.1.0
- A missing capability provider fails the build, not the boot.
os buildandos validatenow preflight everyrequiresentry: one whose provider has no installable version in the active edition (ai→@objectstack/service-ai, cloud-only) fails fast with an edition-aware message, and an absent-but-installable provider becomes an advisorypnpm addhint. Providers were only resolved atserve/startbefore, so avalidate && build && testscript passed and the app crashed on boot. runAs: 'user'flows run with the user's real grants. A record-change-triggeredrunAs:'user'flow ran its data nodes with a zero-grant principal — the triggering user's permission sets and positions were never resolved — so aprivateobject 403'd the in-flow write and apublic_read_writeobject silently stripped readonly/FLS-gated fields. The engine now resolves that user's actual positions and permission sets at run setup.- Two more dashboard mistakes are build errors. Header and widget actions
pointing at an action or route that does not exist are flagged (a
script/modaltarget errors; an in-appurltarget warns), and every dashboard-level filter must resolve to a real field on each bound widget's dataset object — previously a button that rendered and did nothing, and SQL that crashed the widget at render time. - Import/Export works on the business-unit objects. The Import/Export
buttons Setup shows for
sys_business_unitandsys_business_unit_memberboth returned405 OBJECT_API_METHOD_NOT_ALLOWED; both objects now declareimport/export, so the HRIS org-tree sync imports units and memberships together. - Admin-gated
Server-Timingfinally emits onos serve/os dev. The per-request path — an admin sendsX-OS-Debug-Timing, an ordinary user gets nothing — never emitted on the shipped server; only the global mode, which discloses to every caller, worked. - Console: record hits on the full search page and in ⌘K,
globalActionslabel overlays on record-detail action bars, injectedowner_idkept out of auto-generated list columns, and sort repeater rows rendered for union schemas.
16.0.0 in detail
Breaking changes & migration
Strict-semver edges and behavior changes, with migrations — read before recompiling metadata authored against spec 15.x or upgrading a deployment with custom hooks, actions, MCP stdio, or multi-org tenancy.
Hook & action ctx: the tenantId alias is removed — read organizationId (#3280, #3290)
The caller's active organization is now surfaced to JS authors under one
blessed name, matching the organization_id column and
current_user.organizationId in RLS/sharing. #3280 added
ctx.session.organizationId and ctx.user.organizationId (hooks and
action bodies — the REST and MCP run_action paths both populate them);
#3290 then removed the deprecated tenantId alias from the hook
ctx.session, the action-body ctx.session, and the action-body ctx.user.
- const org = ctx.session.tenantId; // hook or action body
+ const org = ctx.user?.organizationId ?? ctx.session?.organizationId;Migration (in any *.hook.ts / *.action.ts body):
ctx.session.tenantId → ctx.session.organizationId;
ctx.user.tenantId → ctx.user.organizationId. The value is unchanged.
ctx.user is undefined for system/unauthenticated writes, so read
ctx.session?.organizationId when the code must work without a resolved
user. The driver-layer tenancy axis (ExecutionContext.tenantId,
DriverOptions.tenantId, TenancyConfig.tenantField) is deliberately
untouched — that configurable isolation column legitimately carries an
environment id in database-per-tenant kernels and is a different concept.
Community edition never populates an org, so organizationId is undefined
there.
Strict dashboard widgets — undeclared keys are loud (ADR-0021, #3251)
DashboardWidgetSchema is now .strict(): an undeclared top-level key — a
typo, a hallucinated key, or a removed pre-ADR-0021 inline-analytics key
(object / categoryField / valueField / aggregate, pivot rowField /
columnField) — is a parse error naming the key and pointing at the
dataset shape (dataset + dimensions + values) instead of being
silently stripped into a widget that renders nothing. options: z.unknown()
remains the escape hatch for renderer-specific extras. Recorded as protocol-16
migration step16 (dashboard-widget-strict-unknown-keys), the protocol-16
counterpart of 15.0.0's strict form/page flip. Migration: recompile;
rewrite any flagged widget onto the dataset shape (both Studio authoring
surfaces already emit only that shape — objectui#2703).
MCP stdio: its own switch, and a mandatory API-key principal (ADR-0101, #3167, #3246)
Two changes close the platform's last identity-less execution surface:
OS_MCP_SERVER_ENABLEDno longer silently starts stdio. The HTTP surface (/api/v1/mcp) and the long-lived stdio transport used to share one env var. Stdio auto-start is now its own switch,OS_MCP_STDIO_ENABLED(default off);OS_MCP_SERVER_ENABLEDgoverns only HTTP. The legacy=truetrigger still starts stdio for one release with a deprecation warning.- BREAKING (stdio auto-start only): stdio requires
OS_MCP_STDIO_API_KEY=osk_.... The transport previously bridged the metadata service and data engine with no per-request principal — unscoped reads. It now resolves the supplied key through the same verify + authorization chain as HTTP/REST (RLS/FLS/tenant apply exactly as on/data; re-resolved per read so revocation takes effect live) and fails closed: enabling auto-start without a resolvable key refuses to boot. There is no unscoped fallback and deliberately nosystembypass — full authority is a key minted on a platform-admin or service identity. The default-on HTTP surface is unaffected.
Feed contracts retired from the type surface and discovery (#1959, #3180)
The feed runtime was deleted long ago (sys_comment / sys_activity are the
canonical collaboration backend); 16.0.0 removes the dead surfaces that still
pointed at it. @objectstack/spec drops IFeedService, the entire
FeedApiContracts / FeedProtocol request/response family, and the
feed-item data schemas; @objectstack/client drops client.feed.*;
ObjectStackProtocolImplementation's constructor loses its getFeedService
argument; discovery drops capabilities.feed / routes.feed. Every removed
method was already unreachable (the feed route was never mounted). Bug fix
riding along: the comments capability keyed off the deleted feed service
and was permanently false; it now tracks the presence of sys_comment, so
declared === enforced. Migration: use the data API —
client.data.create('sys_comment', { object, record_id, body }), read
sys_activity — and discovery.capabilities.comments.
The enforce-or-remove sweep — dead metadata is now loud (#2377, ADR-0049)
A platform-wide audit removed declared-but-unenforced surfaces. Authoring these was a false affordance — especially dangerous for AI-authored metadata:
- Field props
vectorConfig,fileAttachmentConfig,dependencies,columnName,index,referenceFilters— removed (silently stripped, or tombstoned with guidance). Vector fields keep flatdimensions; file/image keep flatmultiple/accept/maxSize; indexes belong in objectindexes[];lookupFiltersreplacesreferenceFilters;external.columnMapis the only physical-column mapping. - Object props
versioning,softDelete,search,recordName,keyPrefix,tags,active,abstract—ObjectSchema.create()now throws a located error naming the replacement (search→searchableFields;recordName→ anautonumberfield asnameField). - Agent
visibility(never enforced — settingprivatehid nothing) andtenantId, plusplanning.strategy/planning.allowReplan— removed. Restrict agents with the enforcedaccess/permissionskeys. - Hook events collapse 18 → 8 (#3195). The 10 never-dispatched events
(
beforeFindOne/afterFindOne,beforeCount/afterCount,beforeAggregate/afterAggregate,before/afterUpdateMany,before/afterDeleteMany) are removed.findOnefires the samebeforeFind/afterFindasfind; bulk updates/deletes fire the singular events with the row-scoping predicate inctx.input.ast.registerHooknow warns on any event the engine never dispatches. - Validation rules lose the dead
'delete'event (#3184) — the evaluator never ran on delete; guard deletions with abeforeDeletehook. - Webhook triggers
undeleteandapiare removed (#3196) — neither ever had a fire path; the auto-enqueuer now warns when a storedsys_webhookrow carries an unmappable trigger. - The
jsexpression dialect is retired (#3278) — it was a stub with no engine ({cel, cron, template}remain). Procedural JS is unaffected: sandboxedScriptBody { language: 'js' }bodies are a separate surface. systemFields.ownerdropped (#3175 follow-up) — nothing read it; use the now first-classownership: 'user' | 'org' | 'none'(see below).- Schema-only surfaces are labeled (#3197): GraphQL subscriptions,
WebSocket/realtime event enums, connector
webhooks/triggers, and the unimplemented notification channels now say "not yet enforced" in their doc comments and.describe()texts.
Engine-owned system rows are read-only through the generic data API (ADR-0103, #3220)
The overloaded managedBy: 'system' bucket conflated rows a platform service
owns end-to-end with platform-defined schema whose rows are legitimately
admin-writable — and had no engine enforcement, so a wildcard admin could
raw-write service-owned rows through /data. The write policy is now the
resolved affordance: genuinely user-writable objects (RBAC link tables,
sys_user_preference, sys_approval_delegation, messaging config) declare
userActions; engine-owned objects (jobs, notifications, approval
request/approver/token/action, sys_record_share, sys_automation_run,
audit trails, sys_secret, sys_import_job, plus sys_presence /
sys_metadata) are locked to ['get','list'], and a new fail-closed guard
(assertEngineOwnedWriteAllowed) rejects user-context generic writes to
them. reconcileManagedApiMethods now strips contradictory advertised verbs
for every managed bucket, and /me/permissions clamps accordingly.
Potentially breaking: a third-party system object that relied on the
old fail-open behavior gets its write verbs stripped (with a warning) —
declare userActions for the verbs it legitimately takes from users. The
Console badges engine-owned objects distinctly (objectui#2705).
Multi-org: the carried posture rung is authoritative for the tenant wall (ADR-0099 P1, #3211)
The Layer 0 cross-tenant exemption now reads the principal's carried
ctx.posture rung as authoritative; the platform-admin capability probe is
demoted to a fallback for resolver-less contexts. Read and write tenant
checks share one decision so they cannot drift. Security narrowing
(multi-org deployments only): a principal whose rung is not
PLATFORM_ADMIN no longer crosses the tenant wall on private /
platform-global / better-auth-managed objects even if its permission sets
carry a platform-exclusive capability — the two affected shapes are a
scoped admin_full_access grant (non-null organization_id) and a
custom set granting a platform capability piecemeal. Upgrade check: scan
sys_user_permission_set for scoped admin_full_access rows and custom sets
whose systemPermissions intersect {manage_metadata, manage_platform_settings, studio.access, manage_users}; grant the
unscoped admin_full_access where genuine cross-tenant operator access
is intended. A runtime [authz/ADR-0099] warning names any principal hitting
the divergence.
Approver type role → org_membership_level (#3133)
ApproverType.role was the last authoring surface projecting the reserved
word that ADR-0090 D3 retired — and it manufactured a real silent failure: since
every other surface renamed to position, { type: 'role', value: 'sales_manager' } reads like the legacy spelling of a position, resolves
against the membership tier, finds nothing, and waits forever on an approver
that cannot exist. The canonical spelling is now org_membership_level;
role remains a deprecated alias for one window (15.x flows keep loading,
with a warning) and is removed next major. New paired lints:
approval-approver-not-membership-tier and
approval-approver-type-deprecated. If the value names an org position, the
fix is type: 'position'.
Behavior changes (16.0.0)
- Bulk user import defaults to
auto(#3236).POST /api/v1/auth/admin/import-usersgains a fourthpasswordPolicyand makes it the default (wasnone): each row with a deliverable channel (real email + wired email service, or phone + SMS invite path) is invited; only genuinely unreachable rows fall back to a temporary password (returned once,must_change_passwordstamped). Per-row outcomes surface onrows[].deliverywith asummary.deliverybreakdown. Callers that omittedpasswordPolicyand want the old identity-only behavior must now pass'none'explicitly. readonlyfields are stripped on INSERT too (#3043). 15.1 enforced staticreadonlyon UPDATE; creating a record bornapproval_status: 'approved'was the shorter attack. Non-system creates through the external data ingress (REST, GraphQL/MCP dispatch, bulk import) now silently drop caller-suppliedreadonlykeys; the field falls back to its declareddefaultValue. Trusted internal writers (better-auth, metadata repository, seed loader) are unaffected; author-defined business objects only (sys_*objects keep their own reject-style guards).- Validation rules now run on multi-row updates (#3106). The
options.multibulk branch previously skippedevaluateValidationRulesentirely — every object rule,requiredWhen, and per-optionvisibleWhenwas a silent no-op there. The engine now evaluates the payload against each matched row's prior state and rejects the whole batch on any error-severity violation (annotated with the failing record id). - The cross-object transactional batch is gated like single-record writes
(#1604).
POST {basePath}/batchnow validates its body (CrossObjectBatchRequestSchema), enforcesenable.apiEnabled/enable.apiMethodsfor every op before opening the transaction, requires ids for update/delete, rejects unresolvable{ $ref }(no more silent NULL FKs), and rejectsatomic: false(400 BATCH_NOT_ATOMIC). dateField == today()matches now (#3183). AField.datereads back as aYYYY-MM-DDstring, and cel-js equality treats a string and a timestamp as unequal — so the most natural due-today predicate silently never matched. The engine now rewrites temporal==/!=comparisons to coerce the field operand (date(record.due_date) == today()), per-occurrence, type-blind-safe, and memoized. Applies to formulas, defaults, validation rules, hook and flow conditions; RLS/sharing compile viacel-to-filterand were already loud.- Sandbox budgets meter script CPU-time, not wall clock (ADR-0102 D1,
#3295). Idle host-await time and nested hooks' own execution are no
longer charged to the caller, so a loaded host or a deep nested-write chain
can't trip the 250ms hook budget while the script is merely waiting. The
timeout knobs keep their names, defaults, and precedence; a generous
wall-clock ceiling (default 30s,
OS_SANDBOX_WALL_CEILING_MS) backstops never-settling host calls. Error strings changed (exceeded CPU budget of Nms/exceeded wall-clock ceiling of Nms) — update any matching code. Also: a body's declaredtimeoutMsis finally honored instead of being clamped to the 250ms default (#1867), andOS_SANDBOX_HOOK_TIMEOUT_MS/OS_SANDBOX_ACTION_TIMEOUT_MSset deployment-wide defaults (#3259). - better-auth's own writes run as system context (#3164). The identity
adapter's writes carried no context, so the 15.1
readonlyUPDATE strip silently dropped better-auth's writes tosys_usercolumns — change-email and admin ban returned success but never persisted. Its internal writes now carryisSystem(user-context writes to identity tables are still rejected by the ADR-0092 guard).withSystemReadContext→withSystemContext(deprecated alias kept one release). - Platform-global objects are never driver-org-scoped (#3249). An
org-context read of a
tenancy.enabled: falseobject (e.g.sys_license) could return 0 rows while an anonymous read saw data. The engine no longer stampstenantIdfor such objects, drivers keep a sticky record of the opt-out across partial re-registrations, andisTenancyDisabled(schema)is the single shared source of truth.
New capabilities in 16.0.0
Approvals: quorum, per-group sign-off (会签), attachments, and declared actions (#3266 series)
- Decision behaviors (#3268): approval steps gain
quorum(M-of-N viaminApprovals) andper_group(one approver from each labeled group — 会签), tallied against an open-time snapshot with OOO substitution. A single rejection stays a veto; thresholds clamp so misconfiguration can never deadlock.sys_approval_actiongains aField.fileattachments column threaded through decide/comment, the REST routes, and the client SDK. - Server-computed progress + deep links (#3274):
getRequestattachesdecision_progress(got/need for unanimous/quorum; per-group breakdown forper_group), andnotify()rewrites inbox action URLs to/system/approvals?request=<id>deep links. - Decision capabilities as metadata (#3282, #3300): approve / reject /
reassign — then send-back (
/revise), request-info, remind, recall, and resubmit (/resubmit) — are declared astype:'api'actions onsys_approval_requestwith typed params, pending-onlyvisiblegates, and submitter-vs-approver predicates (record.submitter_id == ctx.user.id). The previously-404ing/reviseand/resubmitREST routes are wired. The reassign dialog'stoparam is field-backed onsubmitter_id, so it renders a real user picker. A contract test pins the action set. The Console side retires the inbox's hand-written buttons (see Console below).
Time-relative automation — a daily sweep, not date-equality roulette (#1874)
A flow start node can declare config.timeRelative — swept object,
dateField, and either offsetDays: [60, 30, 7] (T-minus thresholds) or
withinDays: 30 ("expiring soon"; negative = overdue lookback), plus an
optional AND-ed filter and an optional schedule (default daily 08:00
UTC). The new time_relative trigger (TimeRelativeTriggerPlugin in
@objectstack/trigger-schedule) launches the flow once per matching
record with the record on the automation context, so start-node conditions
and {record.<field>} interpolation work exactly as for record-change
flows. The discovery query runs as system, capped (maxRecords, default
1000), with per-record failure isolation. os validate gains readiness
checks; the flow designer gets a first-class panel (objectui#2668).
Flow trigger observability — no more four-layer silence (2026-07-17 eval)
A misauthored auto-launched flow used to produce zero output anywhere. Now:
the os serve/os dev/os start startup banner prints a Flows:
section (flow/bound/draft counts, registered trigger types) with loud ⚠
lines for flows with no automation engine, no registered trigger, or a dead
record-change binding; trigger-fired run failures log at ERROR on
stderr; RecordChangeTrigger probes object existence at bind time; a
kernel:bootstrapped binding audit warns per enabled-but-unbound flow; and
os validate gains flow-wiring advisories (unknown target object, draft
status). Registration-time flow-condition validation also runs schema-aware
checks for dynamically registered flows (#1928).
Filtered roll-ups and honest state machines
summaryOperations.filter(#1868): a roll-upsummaryfield can aggregate only matching child rows (operator and compound forms included), letting one child object feed several distinct parent totals; the engine re-runs the filtered aggregate on every child write, so rows moving in or out of the predicate keep the parent current. The Field metadata form declares the sub-fields so Studio renders a structured editor (#3257), and Studio gains a visual filter editor (objectui#2669).state_machine.initialStates(#3165): transitions only ever governed UPDATE — a record could be bornapproved. Rules can now declare the states a record may be created in; out-of-list inserts are rejected server-side (invalid_initial_state). Omit for legacy behavior.ownershipis first-class (#3175): the record-ownership model ('user' | 'org' | 'none', drivingowner_idauto-provisioning) is now a declaredObjectSchemakey instead of an(schema as any)read that the sanctioned authoring path rejected; a retired'own'/'extend'value fails with guidance (that's the package-contribution kind).
Analytics: exact drill-through scopes (#1752, #3214)
Drilling a "2026-Q2" cell needs a range, not an equality. queryDataset now
emits a drillRanges sidecar of half-open [start, end) bounds for
dateGranularity-bucketed date dimensions (bucketKeyToCalendarRange in
core is the inverse of bucketDateValue); datetime dimensions under a
non-UTC reference timezone use ISO instant bounds at that zone's
midnight (DST-safe, zonedDateStartToUtcMs); and drillRawTotals extends
the raw-value snapshot to totals/subtotal rows, so subtotal drills
exact-match on stored values instead of display labels. Consumed by the
Console's report drill-through (objectui#2672).
Performance diagnostics: Server-Timing spans (#2408)
- Opt-in
Server-Timingnow decomposes requests intoauth(identity resolution),db(total SQL time + query count, correctly attributed under concurrency viaAsyncLocalStorage; SQL text never emitted),hooks(business-hook time + count), andserialize— readable in DevTools → Network → Timing with no tracing backend. - Per-request, admin-gated: send
X-OS-Debug-Timing: 1and the header is emitted only for admin/service principals — no more global-only mode fingerprinting the backend to every caller.X-OS-Debug-Timing: jsonadditionally returns an admin-onlyX-OS-Debug-Timing-Detailheader with the slowest parametrized statements (placeholders only, never bindings). Zero overhead when off.
AI / MCP
validate_expressiontool (#1928): agents can run the same expression checks asobjectstack buildagainst an object's real schema before authoring — errors (bare refs, unknown fields with did-you-mean, unknown functions), warnings (type-soundness, date-equality pitfalls), the in-scope field/function inventory, and the inferred result type. Read-only,data:read-scoped, fail-closed onsys_*.- Discoverable serve-side MCP (#3167): the
os devbanner prints the MCP endpoint, the agent-skill URL, and a ready-to-pasteclaude mcp addcommand, so "an agent operates the app it's building" is wired from the dev loop. - Advisory expression guardrails (#1928 tier 4, #3183): builds warn when
a text/boolean field is used with arithmetic/ordering against a number
(exactly the cases the runtime would fault to
null— never the ones it rescues), threaded through formulas, validation rules, predicates, and flow conditions (fatal only under--strict).
Spec, kernel & platform
- One platform capability vocabulary (#3265):
requirestokens are canonical kebab-case owned by the spec (PLATFORM_CAPABILITY_TOKENS) with deprecated-alias canonicalization;defineStacknow rejects unknown tokens at authoring (a typo no runtime provides was previously silently ignored) while the serve resolver warns-not-throws on raw artifacts so older artifacts never crash-boot a server. - Typed atomic batch SDK (#1604 / ADR-0034):
client.data.batchTransaction(operations)— the all-or-nothing cross-object batch with{ $ref: <opIndex> }intra-batch parent references, deliberately exposing noatomicflag. Non-atomic bulk stays ondata.batch()/createMany/updateMany. ActionParamSchemawidget config (objectui ADR-0059): inline params can declaremultiple,accept,maxSizefor file/image params; field-backed params inherit from the referenced field. Thecreate_userresult dialog surfaces the sign-in phone number, andsys_userlist views/detail highlights finally showphone_number(#3262).- View metadata validates all three runtime shapes (#3095): the
viewtype-schema is now a genuine union over defineView containers (non-empty enforced —defineView()also rejects zero-view containers, with a matchingos validatecheck), standalone ViewItem records, and console personalization overlays — a kanban missinggroupByFieldno longer saves with a false200and a "valid" badge. - Metadata subsystem correctness:
register()/unregister()announce tosubscribe()watchers by default (ObjectQL's schema cache no longer goes stale until restart; bulk ingest opts out via{ notify: false }, #3112); unscopedGET /meta/:typededupes package-aware so two packages shipping the sametype/namestay distinct rows (ADR-0048 #1828); and Studio-saved drafts publish/discard in the draft's own org scope, fixingno_draftafter "Save Draft" (#3115). - Sandbox engine (ADR-0102 D2, #3296): the QuickJS sandbox drops asyncify for the sync variant (smaller, faster, an entire suspended-stack failure class removed; a deferred-disposal leak fixed), and the pump loop stops idle-spinning while awaiting host calls (#3233).
- Bulk-write reliability (#3147–#3152): short driver returns are degraded
per-row instead of padded as phantom successes; the remaining un-retried
seed/import write points get transient retries; import
$refresolution flushes pending creates; retries are idempotent after commit-then-lost responses; summary recompute failures surface asERR_SUMMARY_RECOMPUTE; autonumbers are assigned after validation (no sequence gaps). Seed replays also stop corrupting lookup natural keys and are now idempotent (noupdated_atchurn per boot).
CLI & developer experience
- Scaffold ships the connector executors (ADR-0097):
npm create objectstackwiresConnectorRestPlugin/ConnectorOpenApiPlugin/ConnectorMcpPluginplusrequires: ['automation'], so a declarativeconnectors:entry materializes with no host-code edit (declarative stdio stays deny-by-default). Theopenapiprovider now degrades + retries on an unreachable remote spec URL instead of aborting boot (#3049 follow-up). - Scaffold correctness: projects ship a real
.gitignoreagain (npm strips the file from tarballs; it's now aliased as_gitignoreand restored on copy —.envis finally ignored too), declare pnpm 10/11 build approvals sopnpm installdoesn't exit 1, and no longer inherit repo-internal agent skills. - Dev loop (15.1 third-party eval, P2 batch): hot-added
*.object.tsfiles are queryable without a restart (registry ingest + schema sync + first-seen seeds onmetadata:reloaded); the dev admin credential hint re-arms on every boot while the default password still verifies (and the login page can show it via a dev-gateddevSeedAdminconfig field);os doctorunderstands current metadata shapes. os lintsignal (#3091 class): platform built-in i18n gaps are folded into one summary line (--include-platformto audit them), and an inlinelabel:counts as the default-locale source, so a fresh scaffold lints clean instead of reporting 800+ errors on its own template.- Driver dispatch honesty (#3276):
OS_DATABASE_DRIVER=memoryactually gets the mingo InMemoryDriver (it silently fell through to SQLite:memory:— a different engine), and selectingturso/libSQL in the open-core CLI fails loudly naming the cloud/EE package instead of silently degrading to SQLite. - Everyday fixes:
pnpm dev -- --fresh -p <port>works (the pnpm-injected--separator is dropped pre-parse, #3114);os explain objectdocumentsownershipwith the right values (#3244);createLogger({ file })actually writes under ESM and reports unopenable destinations; the kernel logger honorsNO_COLORand TTY detection; audit update diffs exclude computed fields and treatundefined/nullas equal, so the record History tab stops showing phantom changes (#3293); the seed-replayer reportsskippedso hosts can stamp seed-once on all-skip replays.
New in Console (Studio) — bundled objectui 16.0
16.0.0 bundles the objectui 16.0 Console major (15.0.0 → 16.0.0, the
range 077e45b..94d4876, objectui #2589–#2705). The major is a version-line
alignment with objectstack; the code-level breaking edges are the
@object-ui/types spec-adoption cleanup (#2589 — value-erased …Schema
re-exports from spec/ui are dropped; import them from @objectstack/spec
directly) and the removal of the Gantt mobile QR-share context action
(#2687).
Console vs. pin. This section is sourced from objectui's own changesets, not only the
@objectstack/consolebundle-bump summaries (a bump pins a SHA and can lag objectuimain). Therc.0bundle pins94d4876; the pin advanced to69fa5d163a97before the16.0.0-rc.1cut — that later Console work is under Landed since 16.0.0-rc.0.
Approvals inbox goes metadata-driven (#2678 line)
DeclaredActionsBar(#2692): a reusable bar that renders and executes an object's declared actions for any (object, record, location) through the console action runtime end-to-end — param/confirm/result dialogs,{id}target interpolation, fail-closedvisibleCEL,refreshAfter.- The inbox retires its hand-written buttons (#2697): send-back /
request-info / reassign / remind / recall / resubmit come from the declared
set (framework#3300); approve/reject stay in the richer composer (it stages
file attachments) and are
excluded from the bar. Net ~230 lines of bespoke inbox action code deleted; new decision capabilities now ship as backend metadata. - Decision UX (#2681): attachment-capable decision composer with named
chips in the timeline, server-computed progress as per-group tick badges /
M-of-N counts,
?request=deep links auto-opening the drawer, and the flow-designer approval panel synced with the engine schema (quorum / per_group / minApprovals / group).
Action dialogs render real field widgets (ADR-0059, #2704)
ActionParamDialog retires its bespoke per-type branch chain: every declared
param renders through the same fieldWidgetMap as the object form, so a
param of any form-supported type (file, image, richtext, color, address,
code, date, …) gets its real widget — uploads ride the ambient
UploadProvider with multiple/accept/maxSize honored. Result dialogs
skip declared fields whose path doesn't resolve (#2674).
Dashboards author the dataset shape only (#2703)
Both Studio surfaces (widget config panel and inspector) now emit only the
ADR-0021 semantic-layer shape (dataset + dimensions + values) — the
authoring-side counterpart that let the framework flip
DashboardWidgetSchema.strict(). The widget config panel becomes a dataset
picker (chart and pivot) with a catalog; filter-binding field pickers
source from the bound dataset's dimensions.
Flow designer: nested regions, visible and editable (#2670)
loop / parallel / try_catch containers stop being opaque cards: their
regions render as read-only mini-canvases (#2675), then as inline push-down
expansions on the canvas (#2680), and finally nodes inside a region are
selectable and editable through the same schema-driven inspector with path
write-back (#2699). The Runs panel nests per-iteration / per-region step
logs (#2667) — paired with the backend's region-aware run-history compaction
(#3234), which keeps container steps and early failures when a big loop
overflows the persisted budget. Also: the schedule panel authors the
canonical config.schedule the runtime actually reads (#2671), and the
xExpression marker renders the loop collection as a template editor
instead of false-flagging {tasks} as malformed CEL (framework#3304).
Gantt (plugin-gantt)
- Ownership-aware rescheduling (#2683): an own-dates, unlocked summary shifts like a task and carries its unlocked subtree by the same delta; locked tasks that violate a link are reported, not moved; pushed starts snap to configured shift-band (班次) boundaries; and toolbar auto-schedule confirms first ("shift N task(s)? (M locked skipped)").
- Manual-scheduling summary bars, interaction switches, and a
beforeTaskUpdatehost veto (#2677); adependencyTypesswitch hides the type switcher for id-only dependency stores (#2686); the row 「→」 detail slot is mirrored in the task-list header (#2690). - Removed: the mobile QR-share context action (#2687, the objectui 16.0 breaking edge).
Studio Access pillar (#2600 B-series)
The permission matrix hits the first screen: identity and zero-grant capability sections start collapsed (B1); the Explain panel's object field becomes a package-scoped dropdown instead of free text (B2); the Bulk column stops clipping at narrow widths (B3); wide objects get a field filter plus Read-all / Write-all / Clear bulk shortcuts scoped to the visible rows (B4); and curated capability labels + picker group headers localize (B5).
Lists, grids, data & detail
- Master-detail saves are atomic on capable backends (#2684):
MasterDetailForm / LineItemsPanel persist through
dataSource.batchTransaction($refparent links); the client-side compensation-delete anti-pattern is gone, with emulation only when the endpoint is genuinely absent (404/405/501). - The record History tab renders display values instead of raw audit payloads (#2691), paired with the backend diff fixes (#3293); the detail header gets a Record-#id floor and the meta footer stops showing raw user ids (#2689).
- List toolbars gain a manual refresh button (#2645); injected
owner_idstays out of leading auto-derived columns (#2702); the import wizard explains its disabled Next (live unmapped-required-fields hint) and surfaces the legacy per-row fallback as a "compatibility fallback" notice (#2646); single-file child objects stay inline grids instead of flipping to per-row forms, and the non-specattachmenttype is dropped (#2656). - Report drill-through scopes a date-bucket cell to its exact time range via
the new
drillRangesbounds (#2672); the roll-up summary editor gains a visual child-row filter (#2669).
Auth & login
- Never strand SSO-only users on a password wall (#2629): the
/auth/configfetch gets single-flight caching + retrying backoff, the login form holds a spinner until config resolves (honoringssoEnforcedon first paint), and a 20s watchdog rejects hung sign-ins so the button contract can recover; the duplicate "or" divider is gone (#2633 aligns the signup page). - Dev-seeded admin credentials hint on the login page (#2635, dev-gated).
Platform & fixes
@object-ui/typesadopts@objectstack/spec15.1.1 (#2589, breaking): value-erased…Schemare-exports fromspec/uiare dropped; ListView bridge types derive from the spec (#2636, #2622).- CEL-dialect component/action predicates route to the canonical
@objectstack/formulaengine (#2664); discovery trusts only handlerReady/available services (ADR-0076 D12, #2637); internal html-page links route through the SPA navigation handler (#2642); writable system objects are distinguished from engine-owned in badges + empty states (ADR-0103, #2705); the approver Type dropdown drops the deprecated spelling for a membership-tier picker (#2643); metadata-admin stops surfacing the spec's removed dead fields and the inert object "Enabled" toggle (#2658, #2659); dashboard label fallbacks and skill activation editors are fixed (#2666); AgentPreview drops the removed agentvisibility(#2665).
Console changes after the rc.0 pin
The four frontend changes that were pending when this page was first written
(import wizard auto policy — objectui#2701; schema-driven keyValue /
numberList flow mapping — objectui#2708; ActionParamDialog upload guard +
autonumber — objectui#2707; system-field classifier consolidation —
objectui#2706) are now bundled: the console pin advanced 94d4876 → 69fa5d163a97, released as @objectstack/console 16.0.0-rc.1. They plus a
further wave of Console work are described under
Landed since 16.0.0-rc.0 below.
Landed since 16.0.0-rc.0
These changes are on main after the rc.0 cut and rolled into
16.0.0-rc.1, cut on 2026-07-20 — the train's last RC, so every entry below
shipped in 16.0.0. Backend from those changesets, Console from the objectui
pin advancing 94d4876 → 69fa5d163a97 (objectui #2701–#2743), bundled as
@objectstack/console 16.0.0-rc.1. Sourced from each repo's own changesets.
Breaking / behavior
- Deprecated
aiStudio/aiSeatcapability aliases removed (#3308, breaking-as-minor). The one-cycle deprecation window from #3265 is over: the camelCaserequiresspellings are no longer canonicalized — they are now plain unknown tokensdefineStackrejects like any typo, and the serve resolver stops rewriting them on raw artifacts.canonicalizePlatformCapability/DEPRECATED_PLATFORM_CAPABILITY_ALIASESare dropped from@objectstack/spec. Migration: use the canonical kebab-caseai-studio/ai-seat. - Date arithmetic in a
Field.formulais now a build-time error (#3306).record.end_date - record.start_date + 1,today() + 30,record.date + ntype-check clean (operands aredyn) but always faulted tonullat runtime.objectstack build/validateStackExpressionsnow typesdate/datetimefields asTimestampand rejects arithmetic against a number, with a message pointing atdaysBetween(a, b)/daysFromNow(n)/addDays(d, n)/addMonths(d, n). Only fires for genuinely-broken expressions that already returnednull; ordering, equality (#3183) and string concatenation stay runtime-tolerated. - Per-option
visibleWhenenforced oncheckboxes, and option values matched by string form (objectui#2729, #3350). Server-side per-option gating already coveredselect/multiselect/radio;checkboxeswas omitted fromCHOICE_FIELD_TYPES, so a gated option hidden in the UI was still accepted from a crafted write. It is now re-evaluated element-wise on insert/update/bulk. Separately, option matching now coerces both sides withString(...)so a numeric option value submitted as a string (a normal JSON round-trip) can no longer slip its gate. Fail-open on unboundcurrent_useris preserved.
New capabilities (backend)
- Formulas that compute dates/durations stop silently nulling (#3306).
Two long-standing CEL gaps are closed alongside the build error above: the
null-guard idiom
cond ? <value> : nullnow compiles and evaluates (an AST pre-pass wraps the non-null branch indyn(...), sotrue ? 5 : nullno longer faults the type-unifier), andfloor(x)/ceil(x)are registered (round toward −∞ / +∞) and advertised in the catalog. Shipped template formulas likehr_employee.tenure_yearscompute again. engine-ownedbecomes an explicitmanagedByvalue (ADR-0103 addendum, #3343). The overloadedsystembucket is split: a newengine-ownedenum value carries the same all-locked default affordance and joins the write guard /apiMethodsreconciliation //me/permissionsclamp that already covered it by resolved affordance. 20 engine-owned objects are relabelledsystem → engine-owned(the metadata store, jobs, approval-runtime rows, sharing rows,sys_automation_run, the messaging pipeline,sys_secret, settings); 8 admin-writable objects keepsystem(now reading as "engine-managed schema, writable viauserActions"). Enforcement-, wire- and behavior-identical — a self-documenting relabel; the fullsystemretirement is a v17 rename.- Approvals — precise decision-action gating (#3344).
getRequest/listRequestsattach a per-viewerviewer: { can_act, is_submitter }computed server-side (can_act= the caller is a current pending approver by the same check the decision methods authorize with). The declared decision actions now gate on it — approver actions onrecord.viewer.can_act, submitter levers onrecord.viewer.is_submitter— so a submitter no longer sees approver buttons that 403, and a position-addressed approver is no longer wrongly hidden by a client heuristic. Fails closed whenvieweris absent. - Approvals — file attachments on approve/reject decisions (#3332). The
declared
approval_approve/approval_rejectactions gain an optional multi-fileattachmentsparam, rendered through the shared upload widget and persisted to the existingsys_approval_action.attachmentscolumn — letting the inbox retire its hand-wired attachment composer. Purely additive metadata. - Discovery advertises a
transactionalBatchcapability bit (#3298).WellKnownCapabilitiesSchemagains a requiredtransactionalBatch: booleanso clients negotiate the atomic cross-object batch (POST {basePath}/batch) declaratively at connect time instead of probing404/405/501. Every discovery producer fills it honestly (true⟺ the route is mounted and the engine can honour a transaction);@objectstack/plugin-hono-serverunder-reportsfalse(the safe direction). Exposed asclient.capabilities.transactionalBatch. - i18n — action
resultDialogcopy is translatable (#3347). The one-shot secret-reveal dialogs (temporary passwords, 2FA backup codes, OAuth client secrets) gain anActionResultDialogTranslationSchemaslot (title/description/acknowledge/fieldskeyed by literal result-field path);os i18n extractemits the keys and en/zh-CN/ja-JP/es-ES bundles ship copy for all six shipped dialogs. - i18n — collaboration notifications and storage objects localize; the
notifications REST routes are wired (#3354). Assignment / @mention bell
titles resolve through the i18n service in the recipient's locale (new
assignedToYou/mentionedYoutemplates),sys_file/sys_upload_sessionship their own translation bundle, and — the functional fix —GET /api/v1/notifications+ the tworeadroutes are now explicitly mounted in the standalone dispatcher (they only reached cloud's hono catch-all before), so mark-as-read works onos dev/ standalone instead of leaving unread state that could never clear.
New in Console (objectui, now bundled 94d4876 → 69fa5d163a97)
- Option-widget
visibleWhenparity. Per-option cascading +dependsOngating lands across the widget family so client and server agree on which options are visible:MultiSelectField(objectui#2715/#2717),RadioField(objectui#2728, single-sourcing the option resolver), andCheckboxesField(objectui#2735) — the Console side of the framework enforcement above. Aselect+multiplefield renders as a multi-value chip picker (objectui#2709). - Approvals inbox finishes going metadata-driven. Participant gating aligns
with the server-computed
viewerblock (objectui#2719), and the bespoke approve/reject composer is retired now that declared actions carry file attachments (objectui#2710, pairing framework#3332). - Related lists paginate by default (objectui#2711/#2722): server-side
$top/$skipwindows instead of loading every child row. - Dashboard cleanup: the pre-ADR-0021 inline-analytics renderer branches are retired (objectui#2723, framework#3320), and dashboard bars draw on first paint via a single settle re-mount (objectui#2727).
provider:'api'data sources thread the host's authenticated fetch (objectui#2725/#2732), so external API-backed views carry the session's credentials.- Action result dialogs render the new translation slot (objectui#2736), the client half of framework#3347.
engine-ownedlifecycle bucket in the Console (objectui#2739 + the #2712/#2724userActionsparser consolidation), the UI union that unblocked the framework enum split.- Notifications are marked read through the REST surface, not direct receipt writes (objectui#2743), the client half of framework#3354.
- Fixes: kanban / calendar surface write failures instead of swallowing
them (objectui#2716); the earlier post-
rc.0items (import wizardauto, flowkeyValue/numberList, ActionParamDialog upload guard, system-field classifier) are included in this pin.
Landed at the 16.0.0 GA cut
Two changesets are in the ## 16.0.0 changelog sections but in neither
16.0.0-rc.0 nor 16.0.0-rc.1: they landed between the last RC and the GA
cut on 2026-07-21, so they ship in 16.0.0 without appearing in any RC.
ViewFilterRule.operatorbecomes a closed enum — an accept-set narrowing on a published surface (#3373, changeset8ff9210). The operator was previously an open string, so views could persist operators the runtime cannot evaluate. The Zod schema now constrains it to the supported operator enum and normalizes the known legacy aliases to their canonical form on parse. This is a public spec/api-surface change (packages/spec/api-surface.json); it landed onmainin #3373 without a changeset, and the backfill is what shipped it with the GA instead of leaving it stranded.- Console pin advanced to
9a5f016f7d5c(changesetdb34d54, objectui range69fa5d163a97...9a5f016f7d5c). Nested-array columns in the flow designer's node property form (objectui#2761); the record-list "Add View" flow redone — empty-name 405, invisible drafts, canonical naming (objectui#2768); field-type-aware operators and values for the view filter inSchemaForm(objectui#2766); dashboard chart bars drawn on first paint (objectui#2759); and the non-atomic batch fallback gated on the discoverytransactionalBatchcapability (objectui#2755).
What's new in 16.1.0
16.1 is a small fast-follow minor — 8 changesets (5 minor, 3 patch), no
major — released on 2026-07-22, and the v16 line's final release. Three of
the five minors add a new os build / os validate gate, so its practical
theme is moving configuration failures earlier: from a boot crash or a broken
render to a build error. The bundled Console advances one pin,
9a5f016f7d5c → cf2d56e32a11.
New capabilities in 16.1.0
requirescapabilities are preflighted against installable providers (#3366). A listed capability was only checked atserve/starttime, and a missing provider produced a generic "not installed — add it to your dependencies" error even when the provider has no installable version in the current edition;os validate(token vocabulary only) andos build(never resolved providers) both passed, so avalidate && build && testCI script never caught it and it surfaced as an opaque boot crash — seen upgrading an open-edition app from14.7to16after@objectstack/service-aiwent cloud-only (ADR-0025).@objectstack/spec/kernelnow exportsPLATFORM_CAPABILITY_PROVIDERS(token → provider package + edition) and a pureclassifyRequiredCapability(), one machine-readable source of truth for provider/edition knowledge the serve resolver previously encoded informally.os buildandos validategained the preflight: no installable version in the active edition is a fast, edition-aware failure; absent-but-installable is an advisorypnpm addhint, not a hard error; a satisfiedrequireslist passes unchanged. Theos serveboot error renders the same classification, so preflight and boot read identically.- Dead action and route references in dashboards are flagged (#3367,
ADR-0049 applied to references).
os validate/os buildrun a newvalidateDashboardActionRefsgate over every dashboardheader.actions[]and widgetactionUrl.actionType: 'script' | 'modal'is an error unlessactionUrlresolves to a defined action (stack.actionsor an object'sactions);modalalso resolves via the runtime<verb>_<object>convention (create_/new_/add_/edit_/update_plus a real object) and bare object names. A dangling target ships a button that renders and silently does nothing on click — a false affordance.actionType: 'url'is a warning when a relative in-app path names anobjects/reports/dashboards/pages/viewsroute whose target does not exist in the stack; external URLs, interpolated (${…}) targets and opaque routes are skipped. - Dashboard filter fields are validated at build time (#3365, extending
ADR-0021).
validateWidgetBindingsnow checks that every dashboard-level filter (dateRangeand eachglobalFilters[]) resolves to a real field on each bound widget's dataset object. Since #2501 wired these filters into every widget's analytics query, a filter field absent on a widget's object — adateRangebound toclose_dateinherited by an account or contact widget over a different object — emitted invalid SQL (no such column: close_date) and crashed the widget at render time. The new ruledashboard-filter-field-unknownfails the build with a message naming the dashboard, widget, filter, field and object, unless the widget opts out viafilterBindings: { <name>: false }or re-targets to an existing field. Effective-field resolution matches the runtime, and registry-injected system fields (created_at, thedateRangedefault) and objects outside the validated stack never false-positive.
Behavior changes & fixes in 16.1.0
runAs: 'user'flows execute data ops with the triggering user's real permission sets and positions (#3356, follow-up to #1888). Since #1888 the automation engine honoursflow.runAs, but therunAs:'user'credential propagation was hollow: a record-change-triggered run executed its data nodes (update_record, …) with a zero-grant principal — only themember/everyonebaseline — even when the triggering user was fully authorized. Two faces by object config: aprivateobject 403'd the in-flow write (not permitted for positions [org_member, everyone]), and apublic_read_writeobject let the write through but silently stripped readonly/FLS-gated fields.@objectstack/corenow exportsresolveUserAuthzGrants(ql, userId, opts)— the single place that readssys_member/sys_user_position/sys_*_permission_set— which the HTTP resolver delegates to unchanged, andAutomationEngine.setUserGrantsResolverwires it so arunAs:'user'run whose trigger left the envelope unresolved resolves the user's positions and permission sets once at run setup and threads them into every data node. Contexts that already carrypermissionsare left untouched (a REST trigger, and an ADR-0090 agent ceiling acting on-behalf-of a user), so a deliberately narrowed identity is never re-broadened;runAs:'system'is unchanged, and a resolver error fails safe — it warns and keeps the bare user, never elevates.- Import/Export on
sys_business_unitandsys_business_unit_member(#3025, #3391 P0). The Business Units list (Setup → Business Units) surfaces Import/Export buttons, but both objects declared anenable.apiMethodswhitelist of only the five CRUD verbs, and the REST data plane gates import/export on that whitelist (ADR-0049) — so both buttons returned405 OBJECT_API_METHOD_NOT_ALLOWED. Both objects now declareimportandexport. The pairing matters: the HRIS org-tree sync scenario imports the units and their memberships together, so fixing only the unit object left the membership path still 405'ing. Reconcile-safe —reconcileManagedApiMethodsonly strips generic write verbs and never touchesimport/export. - Admin-gated
Server-Timingemits on the standard server (#3361). The per-request path from #2408 — an admin sendsX-OS-Debug-Timing: 1(orjson) and gets phase timings while an ordinary user gets nothing — never emitted on the shipped Hono server: the disclosure gate is flipped by the runtime dispatcher, but the data and metadata routes onos serve/os devare served by@objectstack/rest'sRestServer, whose identity resolver never opened it. Only global mode (OS_SERVER_TIMING=true), which discloses to every caller, worked. The disclosure predicateisPerfDisclosurePrincipal(ec)now lives in@objectstack/observability(re-exported from@objectstack/runtimefor back-compat) as the single definition of who may pull per-request timings, and bothRestServer.resolveExecCtxand the standalone@objectstack/plugin-hono-serverCRUD surface open the gate for an admin/service principal via the carriedposturerung.
New in Console (Studio) — objectui pin 9a5f016f7d5c → cf2d56e32a11
- Record hits surface on the full search page, with i18n group labels
(objectui#2776), and in the command palette from
/api/v1/search(objectui#2772, pairing framework#3371). globalActionslabel overlays apply on record-detail action bars (objectui#2770).- Injected
owner_idstays out of auto-generated list columns (objectui#2779). SchemaFormrenders sort repeater rows for union schemas (objectui#2771, pairing framework#3379).
Upgrade checklist
16.0.0
- Hooks/actions: rename
ctx.session.tenantId/ctx.user.tenantId→organizationIdin every*.hook.ts/*.action.tsbody. - Dashboards: recompile; rewrite any widget flagged by the strict schema
onto the dataset shape (
dataset+dimensions+values). - MCP stdio: if you enable stdio auto-start, set
OS_MCP_STDIO_ENABLEDand provisionOS_MCP_STDIO_API_KEYon a suitable identity — boot fails closed without it. Stop relying onOS_MCP_SERVER_ENABLEDto start stdio. - Feed: replace
client.feed.*anddiscovery.capabilities.feedwith data-API reads/writes onsys_comment/sys_activityandcapabilities.comments; drop the thirdObjectStackProtocolImplementationconstructor argument. - Metadata: recompile and fix keys flagged by the enforce-or-remove sweep
(object tombstones throw with guidance; field/agent keys strip silently —
remove them). Rewrite
events: ['delete']validation rules asbeforeDeletehooks; move webhooks offundelete/apitriggers; check hooks against the 8 real events. - Multi-org: before upgrading, audit scoped
admin_full_accessgrants and custom permission sets carrying platform-exclusive capabilities; switch intended cross-tenant operators to the unscoped set (ADR-0099). - Third-party
systemobjects: declareuserActionsfor verbs users legitimately need; otherwise generic writes are now rejected (ADR-0103). - Approvals: rewrite
ApproverType 'role'→org_membership_level(orpositionif the value names one) before the alias is removed next major. - Capability tokens: replace any
aiStudio/aiSeatinrequireswith the canonicalai-studio/ai-seat— the aliases no longer canonicalize and are now rejected as unknown tokens (#3308). - Formulas: replace date arithmetic in
Field.formula/ predicates (end - start + 1,today() + 30) withdaysBetween/daysFromNow/addDays/addMonths— it is now a build-time error instead of a silent runtimenull(#3306). - Identity import integrations: pass
passwordPolicy: 'none'explicitly if you relied on the old identity-only default. - Sandbox: update anything matching the old
exceeded timeout of Nmserror string. - Console hosts: import spec schema values from
@objectstack/specinstead of the removed@object-ui/typesspec/uire-exports.
16.1.0
- Re-run
os build/os validateafter upgrading — three new preflights can fail a stack that built clean on 16.0.0: arequirescapability whose provider has no installable version in your edition (hard error; an absent-but-installable provider is only an advisory hint), a dashboardscript/modalaction whoseactionUrlnames no defined action (error, with unresolvable in-appurltargets warning), and a dashboard filter naming a field absent from a bound widget's dataset object (new ruledashboard-filter-field-unknown— opt a widget out withfilterBindings: { <name>: false }or re-target the filter). - Automations:
runAs:'user'flows now execute data nodes with the triggering user's real permission sets and positions. Review any flow that depended on the previous zero-grant behavior — writes that used to 403 on aprivateobject now succeed, and readonly/FLS-gated fields that used to be silently stripped on apublic_read_writeobject are now written when the user is authorized.runAs:'system'is unchanged.
References
ADR-0099 (posture-authoritative tenant wall) · ADR-0101 (MCP stdio
principal) · ADR-0102 (sandbox CPU budget + sync engine) · ADR-0103
(engine-owned vs admin-writable) · ADR-0021 (dashboard dataset shape) ·
ADR-0034 (atomic batch) · ADR-0049 / #2377 (enforce-or-remove) · ADR-0059
(objectui action-param widgets) · ADR-0090 D3 (org_membership_level) ·
#3266/#3268 (approvals quorum/会签) · #2678 (objectui declared-actions
inbox) · #1874 (time-relative trigger) · #1868 (filtered roll-ups) · #1928
(expression guardrails) · #1752 (drill ranges) · #2408 (Server-Timing) ·
#3280/#3290 (organizationId) · #3373 (ViewFilterRule operator enum).
16.1.0 — ADR-0025 (edition boundary) · #3366 (requires provider
preflight) · #3367 (dead dashboard action/route references) · #3365
(dashboard filter field existence) · #3356 / #1888 (runAs:'user' grants) ·
#3361 / #2408 (per-request Server-Timing on the standard server) · #3025 /
#3391 (business-unit import/export).