
# Count & Autonumber Fields

> Two columns the engine fills in — a count of related records, and a sequence the database allocates.

Neither of these takes a value from a user. `count` is derived from a relationship; `autonumber` is allocated by the database on insert.

## `count`

Counts the records linked through a relationship field **in the same table** — a `rollup` narrowed to the one aggregation everybody wants.

| 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. |
| `label` | string |  |  | External display name shown to end users, in place of the internal `name`. Resolution order on every surface: surface-level override, then this label, then the raw `name` verbatim. |
| `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 | `count` |  | Constant value 'count' for type discrimination in discriminated unions |
| `relationshipField` | string |  |  | Name of the relationship field in the same table to count linked records from |

### `filters`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `filters` | object |  |  | Filters to apply when counting linked records |
| `filters.field` | string |  |  | Field the condition is evaluated against. |
| `filters.operator` | string |  |  | How the field value is compared, such as `equals`, `greaterThan` or `contains`. |
| `filters.value` | unknown |  |  | Value compared against, when the operator needs one. `isEmpty` and `isNotEmpty` need none; `in` and `notIn` take a list. |
| `filters.and` | array |  |  | Conditions that must ALL hold for a record to be kept. |
| `filters.and[]` | object (truncated) |  |  | A filter condition or a logical group (and/or) of filter nodes. |
| `filters.or` | array |  |  | Conditions of which at least one must hold for a record to be kept. |
| `filters.or[]` | object (truncated) |  |  | A filter condition or a logical group (and/or) of filter nodes. |

```yaml
- {
    id: 2,
    name: completed_task_count,
    type: count,
    relationshipField: tasks,
    filters: { field: status, operator: equals, value: completed },
  }
```

`relationshipField` must name an actual `relationship` field on the same table. That is checked when the configuration is decoded, so a typo or a reference to a field of another type stops the boot rather than counting nothing forever.

`filters` narrows what is counted, using the same condition grammar a view's filters use. Without it, every linked record counts — except a soft-deleted one, which never counts. Trashing a linked record lowers the count immediately, and restoring it raises the count again.

## `autonumber`

A database-assigned auto-incrementing integer — the field type behind invoice and order references. The column is a sequence, so the value is allocated by the database on insert and is never supplied by the client.

| 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. |
| `label` | string |  |  | External display name shown to end users, in place of the internal `name`. Resolution order on every surface: surface-level override, then this label, then the raw `name` verbatim. |
| `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 | `autonumber` |  | Constant value 'autonumber' for type discrimination in discriminated unions |

```yaml
- { id: 3, name: invoice_number, type: autonumber }
```

That produces `1`, `2`, `3`, and so on. `autonumber` takes no type-specific options at all: there is no prefix, no starting offset and no zero-padding. For a human-facing reference such as `INV-01000`, add a `formula` field that composes the number with the prefix and padding you want. That keeps presentation in one place and leaves the underlying sequence untouched, so the format can change without renumbering anything.

A sequence also does not promise to be gap-free. A transaction that allocates a number and then rolls back has still consumed it, which is correct — reusing it would let two records carry the same reference at different times.

## Both recompute rather than store

`count` recomputes when the links it counts change. `autonumber` is allocated once and then fixed. Neither is writable through the records API or a form: a write naming one is refused with `Cannot write to readonly field '<name>'` rather than accepted and discarded, and the refusal names every readonly column the request touched, not just the first.

## Behaviour

### Count Fields

- Computes count of related records
- Returns zero when no records are linked
- Auto-updates when linked records change
- Counts records for multiple relationship fields on the same table
- Applies conditions to filter counted records (a single flat filter; nested AND/OR is not asserted)
- Rejects a count field when `relationshipField` does not exist in the same table
- Rejects a count field when `relationshipField` is not a relationship type
- Resolves FK column via `reciprocalField` when `foreignKey` is omitted
- User can complete full count workflow (regression)
- Infers the FK column when the child has exactly one relationship back to the parent (no `foreignKey`, no `reciprocalField`)
- Rejects the config at validation time when the child has two relationships back to the parent, naming the candidates
- Exposes a count as a JSON-serialisable scalar, a string on Postgres and a number on SQLite
- Developer counts linked records without declaring a `foreignKey`

### Autonumber Fields

- Creates PostgreSQL SERIAL/BIGSERIAL column
- Auto-increments on each new record
- Is immutable after creation: a manual UPDATE is silently reverted, never raised
- Is always NOT NULL (automatically generated)
- Creates a unique index automatically
- User can complete full autonumber workflow (regression)
