| title | v16.0.0 |
|---|---|
| description | 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. |
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: the 16.0.0 train is currently published as
16.0.0-rc.0. This page describes the 16.0.0 content; section headings say 16.0.0 for brevity. (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.)
- 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.
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.
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.
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).
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.
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.
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.
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).
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.
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'.
- 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.
- 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).
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).
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).
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).
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).
- 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.
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).
- 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).
- 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.
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 has since advanced toaf1b0dbonmain— that later Console work is under Landed since 16.0.0-rc.0.
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).
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).
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.
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).
- 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).
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).
- 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).
- 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).
@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).
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 → af1b0db on main. They plus a further wave of Console work are described
under Landed since 16.0.0-rc.0 below.
These changes are on main after the rc.0 cut and roll into the next 16.0
RC — backend from the pending changesets, Console from the objectui pin
advancing 94d4876 → af1b0db (objectui #2706–#2736). Sourced from each repo's
own changesets.
- 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.
- 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.
- 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.- 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.
- 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.
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).