
# Button Fields

> A column that is a control rather than a value — it opens a URL or runs an automation, and can be shown on some records only.

A `button` field renders an interactive control on a record. It stores nothing.

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | number |  |  | Unique identifier for a field within a table. Examples: 1, 2, 3, 100 |
| `name` | string |  |  | Internal identifier for the field: the database column name, and the key used in API payloads and formulas. Use `label` for the name end users read. |
| `description` | string |  |  | Author-written guidance rendered beside the field (under the control on a form, beside the value in a drawer) and associated with the control via aria-describedby. Unlike a placeholder it persists once the user starts typing. |
| `required` | boolean |  |  | Rejects a record whose value for this field is missing or empty, both through the API and in any generated form. |
| `unique` | boolean |  |  | Rejects a record whose value for this field is already used by another record in the same table. |
| `indexed` | boolean |  |  | Adds a database index on this field, so filtering and sorting on it stay fast as the table grows, at the cost of slightly slower writes. |
| `type` | enum | `button` |  | Constant value 'button' for type discrimination in discriminated unions |
| `label` | string |  |  | Button text label |
| `action` | enum | `url`, `automation` |  | What the button does: 'url' opens a link client-side, 'automation' runs a named automation against the record |
| `url` | string |  |  | URL to open (when action is 'url') |
| `automation` | string |  |  | Automation name to trigger (when action is 'automation') |

### `visibleWhen`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `visibleWhen` | object |  |  | Render the button only on records whose named field satisfies the condition. Omit to show it on every record. |
| `visibleWhen.field` | string |  |  | Record field whose value the visibility predicate is matched against |
| `visibleWhen.eq` | string \| number \| boolean |  |  | Matches when the value is equal to this one. |
| `visibleWhen.neq` | string \| number \| boolean |  |  | Matches when the value is different from this one. |
| `visibleWhen.in` | array |  |  | Matches when the value is one of the listed values. |
| `visibleWhen.in[]` | string \| number \| boolean |  |  | A value the field is compared against — a string, a number or a boolean. |
| `visibleWhen.notIn` | array |  |  | Matches when the value is none of the listed values. |
| `visibleWhen.notIn[]` | string \| number \| boolean |  |  | A value the field is compared against — a string, a number or a boolean. |
| `visibleWhen.contains` | string \| number \| boolean |  |  | Matches when the value contains this text, or this entry for a list value. |
| `visibleWhen.gt` | string \| number \| boolean |  |  | Matches when the value is greater than this one. |
| `visibleWhen.lt` | string \| number \| boolean |  |  | Matches when the value is less than this one. |
| `visibleWhen.gte` | string \| number \| boolean |  |  | Matches when the value is greater than or equal to this one. |
| `visibleWhen.lte` | string \| number \| boolean |  |  | Matches when the value is less than or equal to this one. |

```yaml
- {
    id: 4,
    name: approve,
    type: button,
    label: Approve,
    action: automation,
    automation: approve_request,
  }
```

## `label` is the button's text, not the field's display name

Every other field type inherits a `label` from the base — the human-readable name a column header or a form row shows in place of the raw `name`. A `button` does not, and the table above is one row shorter than its siblings for that reason: the type spends the top-level `label` key on the **text printed inside the button**, and a struct cannot declare the same key twice.

So `label: Approve` puts the word _Approve_ on the control. It is required, because a button with no text is a button nobody presses. A button field has no separate display name; where a surface needs one, name the column at the surface — `columns[].label` on a table, `recordFields[].label` on a record view — which wins over the field-level `label` for every other type anyway.

## `action` is a closed two-value vocabulary

`action: url` demands `url`; `action: automation` demands `automation`. Any other value — including an empty string — is refused at startup.

That strictness is not tidiness. `action` is the dispatch key both the renderer and the invoke endpoint switch on, so a value neither of them recognises would render a button that silently does nothing when pressed. Refusing the configuration is the only outcome that tells anybody.

The automation a button names must carry a `manual` trigger to be invocable this way. An automation triggered only by a record event cannot be started by a person.

## Showing a button on some records only

`visibleWhen` names a record field and applies the shared condition vocabulary — `eq`, `neq`, `in`, `notIn`, `contains`, `gt`, `lt`, `gte`, `lte` — to its value. Supplying several operators requires all of them to hold.

```yaml
- {
    id: 5,
    name: ship,
    type: button,
    label: Ship,
    action: automation,
    automation: ship_order,
    visibleWhen: { field: status, eq: pending },
  }
```

This is the same grammar a data table's row actions use, so "show this control on some records" reads the same wherever it is written.

Omitting `visibleWhen` shows the button on every record.

## Visibility is not permission

`visibleWhen` decides what is drawn. It does not decide what may run: the automation behind the button is subject to the same permissions it would be subject to anywhere else, and a caller who reaches the invoke endpoint directly is checked there. Use `visibleWhen` to keep an irrelevant control off a record, and the automation's own gating to keep an unauthorised caller out of it.

## Behaviour

### Advanced field-types render consistently against v1

- Array field display card renders v1-styled name, type badge, tagline, and tag-pill list affordance in light + dark
- Autonumber field display card renders v1-styled name, type badge, tagline, and prefix+padded-number affordance in light + dark
- Button field display card renders v1-styled name, type badge, tagline, and primary-action button affordance in light + dark
- Code field display card renders v1-styled name, type badge, tagline, and monospace code block affordance in light + dark
- Color field display card renders v1-styled name, type badge, tagline, and color swatch + hex affordance in light + dark
- Count field display card renders v1-styled name, type badge, tagline, and large-numeral + linked-relationship affordance in light + dark
- Formula field display card renders v1-styled name, type badge, tagline, and monospace formula expression + result-type affordance in light + dark
- Geolocation field display card renders v1-styled name, type badge, tagline, and pin + lat/lng + datum affordance in light + dark
- JSON field display card renders v1-styled name, type badge, tagline, and pretty-printed JSON preview affordance in light + dark
- User can navigate to each `/field-types/advanced/{type}` page and see the canonical v1 rendering in both light and dark modes

### Button Fields

- Creates no database column for a button field (UI-only)
- Rejects an action outside the url/automation vocabulary
- Runs the named automation when the button is invoked
- Renders the button only on records satisfying visibleWhen
- Logs button action execution in audit trail
- Rejects a button with `action: url` when `url` is missing
- Rejects a button with `action: automation` when `automation` is missing
- Returns 404 when the caller may not write the record
- Returns 404 for a field that is not an automation button
- Refreshes the listing after a run that changed the record
- Reports a refused run as unavailable rather than as a failure
- User can complete full button workflow (regression)
