diff --git a/content/docs/protocol/objectui/concept.mdx b/content/docs/protocol/objectui/concept.mdx
index 23ab6f53d2..528dfdfa5e 100644
--- a/content/docs/protocol/objectui/concept.mdx
+++ b/content/docs/protocol/objectui/concept.mdx
@@ -583,22 +583,46 @@ layout:
### 2. Contextual Actions
-Actions appear based on state:
+Actions appear based on state. `visible` and `disabled` are **CEL predicate strings**
+(or a plain boolean, or a `{ dialect, source }` envelope) — never filter objects:
```yaml
actions:
- type: standard_edit
label: Edit
- visible: { status: { $ne: 'locked' } } # Hide if locked
+ visible: "has(record.status) && record.status != 'locked'" # Hide while locked
- type: workflow_approve
label: Approve
- visible: { permissions: { canApprove: true } } # Hide if no permission
+ requiredPermissions: [approve_invoice] # Hidden in the UI AND refused with 403
- type: standard_delete
label: Delete
confirm: true # Require confirmation
- disabled: { has_children: true } # Disable if has child records
+ disabled: "has(record.has_children) && record.has_children == true" # Disable if has child records
```
+
+ **Two rules turn the obvious spelling of these into bugs.**
+
+ **1. The predicate is bare CEL, and the bound record is sparse — guard every read with**
+ **`has()`.** A filter object (`{ status: { $ne: 'locked' } }` — Mongo query syntax) is in
+ none of the three arms `visible`/`disabled` declare, so it is a parse error. And the
+ record an action predicate sees is whatever the client already fetched: on `list_item`
+ that is only what the view's `$select` projected, so an unprojected column is *absent*,
+ not `null`, and CEL aborts the whole expression at key resolution. Here the abort is
+ **fail-closed** — the button is simply not offered, which looks exactly like the
+ predicate having said no, and nothing logs it. (Note the polarity is the opposite of a
+ form field or section predicate, which faults *open*.) See
+ [Actions → Visibility & Disabled Rules](/docs/protocol/objectui/actions#visibility--disabled-rules)
+ for the guard to write per predicate shape.
+
+ **2. Hiding a button is UX, not authorization** — the button is gone, the route is not.
+ That is why the approve action above declares `requiredPermissions` instead of a
+ `visible` predicate over some `permissions` map: under [ADR-0066](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0066-unified-authorization-model.md) D4 that one
+ declaration is dual-surface — the platform action route answers **403** when the caller
+ lacks the capability (the source of truth) and the UI hides the button from the same
+ declaration. A `visible` predicate would only do the second half.
+
+
### 3. Conditional Field Visibility
Fields appear based on other field values. The key is **`visibleWhen`**, and its value
diff --git a/content/docs/protocol/objectui/layout-dsl.mdx b/content/docs/protocol/objectui/layout-dsl.mdx
index 20f618e1e5..d75996606c 100644
--- a/content/docs/protocol/objectui/layout-dsl.mdx
+++ b/content/docs/protocol/objectui/layout-dsl.mdx
@@ -352,25 +352,40 @@ sections:
### Conditional Sections
-Show sections based on field values or permissions:
+Show sections based on field values. The key is **`visibleWhen`** and its value is a
+**CEL predicate string** — never a `{ field, value }` rule object:
```yaml
sections:
- label: Basic Info
fields: [name, email]
-
+
- label: Billing Information
- visible:
- field: account_type
- value: premium # Only show for premium accounts
+ visibleWhen: "record.account_type == 'premium'" # Only show for premium accounts
fields: [payment_method, billing_address]
-
- - label: Admin Settings
- visible:
- permission: admin # Only show to admins
- fields: [api_key, rate_limit]
```
+
+ **There is no section-level `visible` key — and no permission test belongs here.**
+ `FormSectionSchema` is `.strict()` and declares `visibleWhen` (plus the `@deprecated`
+ alias `visibleOn`); `visible` is refused *by name*, before any value shape is examined,
+ and the rejection points you back at `visibleWhen`. Neither a `{ field, value }` rule
+ object nor a `{ permission }` name is a value `visibleWhen` accepts either — it takes a
+ CEL predicate string, normalized to a `{ dialect: 'cel', source }` envelope at parse.
+
+ Earlier revisions of this page also showed an `Admin Settings` section gated on
+ `permission: admin`. It is **removed rather than translated**, because rewriting it as a
+ `visibleWhen` position test would teach the anti-pattern this page warns about further
+ down — twice over. Nothing server-side evaluates a form-view section predicate, so a
+ position test here hides the controls and protects no data: the record still carries
+ every value in the section, and every other read surface still returns them. Worse, on
+ the console's public form route (`/f/:slug`) no host publishes a predicate scope, so the
+ root is unbound, the predicate **faults open**, and the section it was meant to hide is
+ shown to everyone. To withhold a group of fields from some users, declare
+ [field-level security](/docs/permissions/field-level-security) on a permission set, or
+ [row-level security](/docs/permissions/rls) — both enforced on the server.
+
+
### Section Variants
```yaml