# Field policy `owner`, `shared` and `write_roles` decide which records a member reaches and may write ([Owner and shared entities](entities.md)). **Field policy** decides, record by record, which of a record's fields that member sees, may change, and must fill in. Air-Flex enforces it on every read and write, whatever path the request takes — a page, a function, the API or your coding agent — so your code never filters fields itself. ## Declaring rules Rules are `[[policy]]` tables of the entity's file, `db/schema/.toml`: ```toml [[policy]] field = "internal" effect = "visible" when = { ref = "actor.roles", op = "contains", value = "staff" } [[policy]] field = "seats" effect = "writable" when = { and = [{ ref = "record.status", op = "eq", value = "held" }, { ref = "env.now", op = "lt", value = "2027-01-01T00:00:00Z" }] } [[policy]] field = "slot" effect = "required" when = { ref = "actor.org_path", op = "in", value = ["/crm/companies"] } ``` | key | what it says | |---|---| | `field` | a field the entity declares | | `effect` | `visible`: the field is shown only where `when` holds; `writable`: it may be changed only where `when` holds; `required`: it must be filled in wherever `when` holds | | `when` | a condition: a leaf `{ ref, op, value }`, or `and` / `or` over a list of conditions | A leaf's `ref` reads one of: - `record.` — the record's own value (on a create, the value submitted; for `required`, the value the write would leave); - `actor.roles`, `actor.sub`, `actor.org_path` — the member acting, with **the roles they hold at the record's node**, not at their home ([How authorization works](authorization.md)); - `env.now` — the current time, in steps the platform declares, so a rule's answer does not change every instant; - `surface` — where the request came from: `api`, `mcp` (your coding agent), `import` (your repository's own data — see [below](#import-is-your-repository)), or the `ui.form` and `ui.table` your own pages declare. A `record.` leaf takes the operators a query filter takes on that field's type, and a condition is bounded in depth and size as a query filter is. `aircloud check` refuses a rule outside the grammar — an unknown key, effect, reference, operator or combinator, a field the entity does not declare, an operator its type refuses, a condition past the bounds — with `schema.policy_invalid`; there is no expression escape hatch. ## What a member sees - **Rules only take away.** Several rules on one field all apply: every `visible` and `writable` condition must hold, and any `required` condition that holds requires the field. A node below the root can add rules of its own that tighten yours, never relax them. - **A hidden field is omitted** from that record in every answer: a get, a list page, a query, an aggregate. - **A hidden field matches nothing.** A filter on a field hidden on a record never makes that record match, in either direction; a sort reads it as absent; an aggregate takes no value from it; a full-text or vector search never finds the record through it. Two records that differ only in a hidden value answer alike, so nothing leaks through a query. - **A field hidden for the whole request** — by a rule of yours that reads neither the record nor the member's roles — is refused up front when a query names it (`flex.forbidden`). - **A write that changes a field that is not writable** on that record is refused (`flex.forbidden`, naming the field, the effect and the node). Sending the value it already holds is not a change. ## `import` is your repository Data your repository declares — seed templates applied at a new node, your data migrations, and the test fixtures the release verification pipeline loads — arrives on the `import` surface. It is yours: the repository is the one writer of your schema, so **`visible` and `writable` rules do not apply to `import`**. A fixture or a migration can fill a field you guard for your members — a `stage` only a `manager` may change, say — without a rule that names `surface`. **`required` still applies**: a fixture, seed or migration that leaves a required field empty is refused (`flex.validation`). Everything that is not field policy still applies too: `write_roles`, `owner` and `shared`, the tenant and node a record belongs to, and denies. Only the platform puts a request on `import`. Nothing a page, a function, your coding agent or an API call sends — a header, a query parameter, a body value — makes a request `import`, so a rule that guards a field from your members guards it on every one of those paths. ## `ui.form` and `ui.table` are presentation Your own page declares the `ui.*` surface it renders, so a rule whose only difference between showing and hiding a field is `ui.form` against `ui.table` hides nothing from someone who calls the API and declares the other one. Use them to tidy a screen; protect a confidential field with `actor.*` or `record.*`. ## Asking why `aircloud authz explain … --entity --field ` names the rule that decided whether a member may act on one field at a node ([How authorization works](authorization.md#asking-why)). --- Rendered from aircloud 0.1.12 at commit d84a0ce6dae53629277f656d96d81549df36807e.