Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@ content/docs/references/** merge=os-regen
skills/*/references/_index.md merge=os-regen
skills/objectstack-ui/references/react-blocks.md merge=os-regen
scripts/platform-object-tenancy-census.json merge=os-regen
content/docs/ui/components/** merge=os-regen
11 changes: 11 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,17 @@ jobs:
continue-on-error: true
run: node scripts/check-sdui-manifest.mjs --self-test && node scripts/check-sdui-manifest.mjs

# The component reference under content/docs/ui/components/ is generated
# from that same manifest by scripts/gen-sdui-reference.mjs, so it sits
# next to the manifest's own check: a PR that changes the manifest and
# not the reference reds here, and the two land in one PR. Offline and
# unfiltered for the reason the step above gives. The script header is
# the authority on the page layout and on what the drift check compares.
- name: Component reference matches the SDUI manifest
id: component-reference-matches-the-sdui-manifest
continue-on-error: true
run: pnpm check:sdui-reference

# pm-dispatch SKILL.md line ratchet (#7341 / #5925 item 7): the PM skill
# is read in full by every seat session and every Routine fire, so its
# size is a per-round tax on the whole fleet. The #7341 extraction moved
Expand Down
8 changes: 7 additions & 1 deletion apps/docs/redirects.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ export const docsRedirects = [
['/docs/guides/metadata/dashboard', '/docs/ui/dashboards'],
['/docs/guides/public-forms', '/docs/ui/forms'],
['/docs/guides/metadata/doc', '/docs/ui/doc-pages'],
['/docs/concepts/setup-app', '/docs/ui/setup-app'],
['/docs/concepts/setup-app', '/docs/ui/apps'],
// 2026-10-28: the Setup, Console and Studio pages were retired. Their
// author-facing facts moved into apps, views, actions, approvals and
// validating-metadata; the product tour is build-without-code.
['/docs/ui/setup-app', '/docs/ui/apps'],
['/docs/ui/console', '/docs/build-without-code'],
['/docs/ui/studio', '/docs/build-without-code'],
['/docs/guides/solutions/create-vs-edit-form', '/docs/ui/create-vs-edit-form'],
['/docs/guides/solutions/field-grouping-and-order', '/docs/ui/field-grouping-and-order'],
// The page was renamed role-based -> audience-based (the direction
Expand Down
9 changes: 9 additions & 0 deletions content/docs/automation/approvals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,15 @@ reassign user-picker) is collected by the generic action dialog with **no
per-action UI code**. New decision capabilities ship as backend metadata, not
hand-written buttons.

On the record itself, a record with approval requests grows an **Approvals** tab
beside Details and Related, with a request-count badge: the current step, decision
progress, the approvers it is waiting on, the merged decision timeline and a remind
button for the submitter. Every viewer who can read the record sees it, not only
approvers. A request opens on its record page through the approvals routes rather
than the data API, so an approver who is not an administrator sees the whole page,
decision actions included, although the data API stays closed to them on the
approval objects.

Each action's visibility is gated by a **server-computed per-viewer block** the
service attaches to every request it returns:

Expand Down
4 changes: 2 additions & 2 deletions content/docs/deployment/cloud-connection.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,6 @@ to cloud-hosted runtimes; a self-hosted server leaves both unset. A set
connection platform-managed: no Cloud Connection page, and no connect or
disconnect.

The Setup app itself, and how plugins add entries to it, is described in
[Setup app](/docs/ui/setup-app). Running the server is covered in
How plugins add entries to the Setup app is described in
[Apps](/docs/ui/apps#contributing-navigation-to-another-app). Running the server is covered in
[Self-hosting](/docs/deployment/self-hosting).
67 changes: 67 additions & 0 deletions content/docs/deployment/validating-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,73 @@ schema, so a schema error is `os validate`'s verdict to give — but a rule that
can fail the build fails `os lint` too.
</Callout>

## Metadata diagnostics on load

Every metadata item is also validated against its schema when a server loads it, and
the result travels with the item as a `_diagnostics` envelope:

```ts
interface MetadataDiagnostics {
valid: boolean;
errors?: Array<{ path: string; message: string; code?: string }>;
warnings?: Array<{ path: string; message: string; code?: string }>;
}
```

`valid: false` means at least one error, and whatever depends on the item (rendering,
queries, automation) is unsafe to use. Warnings are advisory. `path` is dot-delimited
(`fields.email.type`, `columns.0.bind`). An environment overlay merges first and is
validated after, so the envelope describes the effective item.

`GET /api/v1/meta/diagnostics` sweeps every type and item in one request:

| Query | Default | Effect |
| :--- | :--- | :--- |
| `severity` | `error` | `error` returns invalid items only; `warning` also returns warning-only items |
| `type` | — | One metadata type |
| `package` | — | One package |

The response carries `entries` (each `{ type, name, diagnostics }`), `total`,
`scannedTypes`, `scannedItems` and per-type `stats`. `total === 0` is a usable CI gate
against a running server; add `severity=warning` to hold warnings to the same bar.
Studio shows the same envelope on the metadata directory, each type's list, each
item's edit page and the governance page (`…/metadata/_diagnostics`).

## When a predicate cannot be evaluated at runtime

A predicate that gets past authoring and fails to evaluate (a misspelled root, a type
error, a missing function) never fails silently. The Console warns once per distinct
predicate source, in production builds too, naming the node, the key, the predicate and
the engine's reason:

```text
[ObjectUI] A visibility predicate could not be evaluated - node "record:alert" (id: "a1")
visibleWhen: "nosuchroot.status == 'draft'"
Reason: ... nosuchroot is not defined
```

What the failure does depends on what the predicate guards:

| Predicate | On failure |
| :--- | :--- |
| A field's `visibleWhen`, `readonlyWhen`, `requiredWhen`; a view's `visibleOn`; an option's `visibleWhen`; list conditional formatting | **Fail open**: the rule's safe default applies, so a broken rule never hides a field. A rule that never fires while its field stays visible is the symptom, usually a bare field name where `record.<field>` was meant |
| An object field group's `visibleWhen` | **Fail closed**: the whole group is hidden on the form and the record page |
| A page node's `visibleWhen` / `visible` / `hidden`, and a `page:tabs` item's `visibleWhen` | **Fail closed**: the node or tab is withheld; a withheld related list is not counted in its tab badge |
| A navigation item's or area's `visible` | **Fail open**: the item renders for everyone, reported under `app-shell:visible` |

At **submit** a broken field rule is refused, not passed. A field `visibleWhen` that
cannot be evaluated refuses the submit on the client, with a message naming the field
and the rule, because no server evaluates `visibleWhen`. A broken `requiredWhen` or
`readonlyWhen` is refused by the server, and the form shows that refusal beside the
input. A `visibleWhen` that reads `previous` cannot be evaluated on a create form, so
such a form is always refused.

A **blank** rule (`''`, whitespace, or an envelope whose `source` is blank) is not "no
rule". A blank field rule is refused when it is authored, and one already stored is
refused at submit. A blank gate (`visible`, `hidden`, `disabled`, a page node's
`visibleWhen`) reads as "no gate" at runtime and is reported once with the `[blank]`
reason.

## The workflow

```bash
Expand Down
4 changes: 2 additions & 2 deletions content/docs/permissions/record-view-auditing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ concerns.
## Reading the trail

`sys_audit_log` ships a **Record Views** list view — filtered to `action: read`,
newest first. In the Setup app, Diagnostics › [Audit Log](/docs/ui/setup-app),
the entry the plugin contributes, opens the audit browser, which offers that
newest first. In the Setup app, Diagnostics › Audit Log ([contributed](/docs/ui/apps#contributing-navigation-to-another-app) by the plugin),
opens the audit browser, which offers that
view as a preset; its "Open as list" link opens the object list on it.

The object is append-only and exposes only `get` and `list` on the data API;
Expand Down
2 changes: 1 addition & 1 deletion content/docs/ui/actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ and [Connect an MCP Client](/docs/ai/connect-mcp).

| Symptom | Cause → fix |
|:---|:---|
| Button doesn't appear | `locations` doesn't include the surface; or the `visible` CEL throws (e.g. a bare, unprefixed field name) and fail-closed hides it; or the user fails `requiredPermissions` |
| Button doesn't appear | `locations` doesn't include the surface (empty `locations` is right for a selection-only action that a list view's `bulkActions` places); or the `visible` CEL throws (e.g. a bare, unprefixed field name) and fail-closed hides it; or the user fails `requiredPermissions`, which hides the button at every location with no greyed-out control and no message |
| Click → `Action 'x' … not found` | `target`-style script with no registered handler — add the `registerAction` call in `onEnable` (Path B above) |
| REST → 400 naming the action's `type` | The type has no server dispatch (`url`/`modal`/`form`/`api`) — open its `target` in the client, or call that endpoint directly |
| Appears in UI, missing from `list_actions` (MCP) | `ai.exposed` not `true`, `ai.description` under 40 characters, or the type isn't headless-callable |
Expand Down
56 changes: 56 additions & 0 deletions content/docs/ui/apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,62 @@ in 17.1). Both the app and `branding` are strict: a top-level `logo` or `theme`
refused with a pointer to `branding`, and a misspelled key inside `branding` is
refused rather than dropped.

## Contributing navigation to another app

A package can add menu entries to an app it does not own with
`navigationContributions` (ADR-0029 D7). The Setup app is built this way: it ships
as a shell of empty group anchors, and each capability plugin contributes the
entries for the objects it owns, so the rendered menu always matches the loaded
plugins. A disabled plugin contributes nothing and its slot stays empty.

```typescript
navigationContributions: [
{
app: 'setup',
group: 'group_integrations',
priority: 100,
items: [
{ id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', requiresObject: 'sys_webhook' },
],
},
]
```

| Property | Type | Description |
| :--- | :--- | :--- |
| `app` | `string` | The target app's name, e.g. `setup` |
| `group` | `string` | optional. A group nav item id in the target app; omit it to append at the app's top level |
| `priority` | `number` | Merge order within the group, lower first (default `200`, the object-extender convention) |
| `items` | `NavigationItem[]` | Ordinary [navigation items](#navigation-items), with the usual `requiresObject`, `requiredPermissions` and `visible` gates |

The runtime merges every contribution into the target app's `navigation` by group
id and priority when the app is read, so registration order does not matter.

**A group id the target app does not declare is not refused.** The items are
appended at the app's top level and a `nav_contribution_group_missing` diagnostic
is raised, at `warn` by the runtime and as a warning by `os build` and
`os validate` when both packages are composed into one artifact. Neither fails
the build. The anchor id is the whole contract between the shell and its
contributors, and a typo renders a working menu with the entry one level up, so
read that warning.

The Setup app's anchors:

| Group id | Holds |
| :--- | :--- |
| `group_overview` | System Overview dashboard |
| `group_apps` | Packages and the marketplace |
| `group_people_org` | Users, organizations, business units, teams, invitations |
| `group_access_control` | Positions, permission sets, sharing rules, API keys |
| `group_approvals` | Approvals inbox, requests, history, delegations |
| `group_configuration` | Settings, localization, authentication, email, storage, AI, feature flags, branding |
| `group_diagnostics` | Sessions, notification events, audit log |
| `group_integrations` | Webhooks, cloud connection |
| `group_advanced` | OAuth applications |

Setup is for platform administration and needs `setup.access`. For
project-specific administration pages, declare an app of your own.

## Complete Example

{/* os:check */}
Expand Down
Loading
Loading