# grid

Rows a person searches, filters, sorts, selects and acts on, such as every open ticket and who is working on it.

`source` names a source of the screen; `columns` lists 1 to 30 columns, each a `key` (a column of the rows), a
`label` and a `kind`: `text`, `strong`, `muted`, `mono`, `number`, `status`, `badge`, `time` or `bar` (numeric
kinds align themselves). `priority` says when a column hides as the page narrows (`primary` never, `secondary`
below 1060 px, `tertiary` below 720 px). `tones` maps a status or badge value to `ok`, `warn`, `bad`, `info` or
`neutral`. `search` (true by default) searches every visible column; `filters` lists the columns a person can
filter by. `open` adds the actions column last, an `Open` link on every row (the key column stays plain): a
screen name opens `/a/<app>/<open>?id=<row id>`, `$id` the screen the row's id names. `actions` act on the
selected rows: `- set: { column, label, value }` (or `options` to choose from), `- delete: {}` (always
confirmed), `- export: csv`. `plain: true` drops the toolbar, selection and pager for a small read-only table. The
address holds the grid's state, so a filtered view is a link. Put the column a person knows a row by first, as
`strong`; give every column past three `priority: tertiary` so a phone keeps three. An empty cell shows a dash.

Category: data. Tier 1. Contract version 1.

## When to use

- Many rows a person works through, such as the open tickets of a queue.
- Rows a person changes in bulk, such as assigning tickets to an agent or closing them.
- A small table of facts, such as the policies in force, with `plain: true`.

## When to use another block

- A handful of lines with a title and a link each. Use [`list`](/catalog/list.md).
- Things that happened, in time order. Use [`timeline`](/catalog/timeline.md).
- One row a person approves or rejects. Use [`approval`](/catalog/approval.md).

## Props

- `id` (name: a-z, 0-9 and -): The block's id: its Markdown anchor and its actions' route (`/act/<id>/<action>`). Default `<type>-<n>`, n counting blocks of this type depth-first from 1.
- `source` (a source of the screen, required): The source whose rows it shows.
- `columns` (list of 1–30: mapping, required): The columns, in order.
  - `key` (column, required): The source column the cell shows.
  - `label` (text ≤ 60, required): The column's heading.
  - `kind` (one of text, strong, muted, mono, number, status, badge, time, bar, default `text`): How the cell draws its value; numeric kinds align themselves.
  - `priority` (one of primary, secondary, tertiary, default `primary`): primary never hides; secondary hides below 1060 px, tertiary below 720 px.
  - `tones` (mapping of value → one of neutral, ok, warn, bad, info): Cell value → tone, for status and badge cells.
  - `max` (number): The value of a full bar (bar cells only).
- `search` (true or false, default `true`): A search box over every visible column.
- `filters` (list of 0–30: column, default `[]`): The columns a person can filter by (keys of `columns`).
- `open` (screen name, or $id): The screen a row opens: the last column is an `Open` link to `/a/<app>/<open>?id=<row id>`; `$id` opens the screen the row's id names, `/a/<app>/<row id>`.
- `actions` (list of `- set: {…}`, `- delete: {…}`, `- export: csv`): Bulk actions on the selected rows: `- set: {…}`, `- delete: {…}`, `- export: csv`.
- `plain` (true or false, default `false`): No toolbar, no selection, no pager (a small read-only table).
- `page_size` (one of 25, 50, 100, default `25`): Rows per page.

## Data

Reads the rows of `source` (up to 1,000), searched, filtered, sorted and paged by the address's query.

Shape: one row per line with an `id` (integer, or text hashed to the row id) and every column `columns` names.

## Actions

- `set`: sets the column to the value on those rows. Needs a selection of rows, or the row; reversible; confirm page when declared; cost write.
  - `column` (column, required): The column it sets.
  - `label` (text ≤ 40, required): The button's words.
  - `value` (text, number, true or false): The value it sets; or `options`.
  - `options` (list of 1–30: text, number, true or false): The values the person picks from; or `value`.
  - `approval` (true or false, default `false`): Through the confirm page first.
  - `id` (name: a-z, 0-9 and -): The action's id; default `set-<column>`, plus `-<value>` when a value is given.
- `delete`: deletes those rows. Needs a selection of rows; irreversible; confirm page always; cost write.
  - `label` (text ≤ 40, default `Delete`): The button's words.
  - `id` (name: a-z, 0-9 and -): The action's id; default `delete`.
- `export`: downloads the grid's current view as CSV (RFC 4180: CRLF after each record); written `- export: csv`. Needs none; reversible; confirm page never; cost free.

## States

- **ready**: the source's rows, drawn
- **empty**: the source has no rows: an Empty part saying why and what comes next
- **error**: the source failed: "We couldn't load …" naming the block's rows, the source's sentence and Try again; the page still answers 200

## Examples

### The open tickets, assigned and closed in bulk

```yaml
grid:
  id: tickets
  source: tickets
  columns:
    - { key: id, label: Ticket, kind: strong }
    - { key: subject, label: Subject }
    - { key: agent, label: Agent, kind: badge }
    - { key: status, label: Status, kind: status, tones: { Closed by agent: ok, Needs a person: warn } }
    - { key: opened_at, label: Opened, kind: time, priority: secondary }
  filters: [agent, status]
  open: ticket
  actions:
    - set: { column: agent, label: Assign to…, options: [Identity, Network, Apps] }
    - set: { column: status, value: Closed by agent, label: Close, approval: true }
    - export: csv
```

As an agent reads it:

```markdown
<a id="tickets"></a>

| Ticket | Subject | Agent | Status | Opened | Open |
|---|---|---|---|---:|---|
| 48213 | VPN drops every 20 minutes | Network | Closed by agent | 2026-10-31 09:12 | [Open](/a/catalog/ticket?id=48213) |
| 48214 | Reset MFA on new phone | Identity | Waiting for requester | 2026-10-31 09:07 | [Open](/a/catalog/ticket?id=48214) |
| 48215 | Expense tool shows last quarter | Apps | Needs a person | 2026-10-31 08:58 | [Open](/a/catalog/ticket?id=48215) |

Showing 1–3 of 3

- #tickets.set-agent "Assign to…": POST /a/catalog/{screen}/act/tickets/set-agent · sel=row id, repeated per row, value=one of Identity, Network, Apps; sets agent · approval: no · cost: write
- #tickets.set-status-closed-by-agent "Close": POST /a/catalog/{screen}/act/tickets/set-status-closed-by-agent · sel=row id, repeated per row; sets status = Closed by agent · approval: yes · cost: write
- #tickets.export "Export CSV": GET /a/catalog/{screen}/act/tickets/export · CSV of the grid's current view · approval: no · cost: free

```

### The agents, filtered by queue

```yaml
grid:
  source: agents
  columns:
    - { key: name, label: Agent, kind: strong }
    - { key: queue, label: Queue }
    - { key: closed, label: Closed this month, kind: number }
    - { key: needs_person_pct, label: Needs a person (%), kind: number, priority: secondary }
  filters: [queue]
  page_size: 50
  actions:
    - export: csv
```

As an agent reads it:

```markdown
<a id="grid-1"></a>

| Agent | Queue | Closed this month | Needs a person (%) |
|---|---|---:|---:|
| Identity | Accounts and access | 1120 | 4 |
| Apps | Business applications | 640 | 22 |

Showing 1–2 of 2

- #grid-1.export "Export CSV": GET /a/catalog/{screen}/act/grid-1/export · CSV of the grid's current view · approval: no · cost: free

```

### The policies in force, as a small table

```yaml
grid:
  source: policies
  plain: true
  columns:
    - { key: policy, label: Policy, kind: strong }
    - { key: applies_to, label: Applies to }
    - { key: state, label: State, kind: status, tones: { enforced: ok } }
```

As an agent reads it:

```markdown
<a id="grid-1"></a>

| Policy | Applies to | State |
|---|---|---|
| Data stays in the EU | Every agent | enforced |
| Orders over $500 need a manager | Procurement agent | enforced |

Showing 1–2 of 2

```

## Invalid

```yaml
grid:
  source: ticket
  columns:
    - { key: subject, label: Subject }
```

Answers: `example.blocks[0].grid.source: no source named ticket; allowed: agents, policies, tickets; did you mean tickets?`

## Composition

- Sits in: screen, card
- Pairs with: kpis, chart, timeline
- Five to seven visible columns; give the rest a lower priority.
- One working grid per screen; others are `plain`.
- Apple HIG: Let people find, sort and act on items in place, and confirm actions that cannot be undone.

## Telemetry

grid.view, grid.search, grid.filter, grid.sort, grid.page, grid.select, grid.action

JSON Schema: [/catalog/grid.schema.json](/catalog/grid.schema.json)
