---
title: "Types, rules and writes"
url: /model/types
---

# Types, rules and writes

> The field types a Noun holds, embedded lists and links, the rules every write keeps, computed fields, and how two writers never overwrite each other.

Types, rules and writes say what a Noun's fields may hold, what every write must keep true, and how two writers never overwrite each other.

**One write, checked in order.** A write on any Interface.

1. Types: each field. Or 400: invalid_param.
2. Version (fits): If-Match. Or 412: a stale ETag.
3. Rules (current): the invariants. Or 422: rule_failed.
4. Kept (kept true): a new ETag.
5. Computed (its Event): fields rebuilt.

Each check answers before anything is kept, and a refused write costs nothing.

## What a field can hold
The types are the same on every [Interface](/every-product.md): OpenAPI, the SDK types, MCP's schemas, forms and the docs all print them from the file.

| Family | What it holds |
| --- | --- |
| Scalars | `string`, `number`, `boolean` and `decimal(p,s)`; `date`, `datetime` with its zone, and `duration`; `url`, `markdown` and `file` |
| Contact types | `email` and `phone`, checked on write and stored normalised, and `address` as its parts with a country |
| Money | `money`: an amount with its currency, summed only with amounts in the same currency |
| Closed sets | `enum(low,normal,high)`: one of the words listed, printed as choices in every form and SDK |
| Lists | `[]` after any type, such as `email[]`, kept in order |
| Embedded objects | `{ type: 'list', of }`: entries kept inside the record and versioned with it, such as an invoice's lines |
| Links | `->` and `<-` both ways, a link Noun with two `->` for many-to-many with attributes of its own, and a link to the same Noun for a tree |
| Unique keys | `!#` for one unique key the record also answers at, and `unique` for a key made of several fields |
| Rules | `rules`: invariants over the record and its links, kept by every write and answered 422 with the rule that failed |
| Computed fields | `compute`: a formula over the record and its links, such as a sum of lines, kept as a projection and never written |
| Data classes | `class` on a field (`personal`, `sensitive`, `payment`, `health`, `privileged`); [data governance](/trust/governance.md) says what each changes |

A rule and a computed field read one expression language. It holds comparisons, arithmetic, `and`, `or` and `not`, field paths across links and `now()`, with `count`, `sum`, `min`, `max` and `avg` over a link.

### Example
Each Deal holds typed lines, a total computed from them, a contact marked personal, a priority from a closed set, and rules every write keeps.

```ts title="definition.ts"
import { define } from 'apis.dev'

/** Deals for each Customer, kicked off when won. */
export default define({
  name: 'pipeline', domain: 'pipeline.apis.dev',
  Company: { name: 'string', domain: 'url!#', deals: '<-Deal.company' },
  Deal: {
    title: 'string', company: '->Company.deals',
    priority: 'enum(low,normal,high)',
    contact: { type: 'email?', class: 'personal' },
    closesAt: 'datetime?',
    lines: { type: 'list', of: { item: 'string', qty: 'number', price: 'money' } },
    amount: { type: 'money', compute: 'sum(lines.qty * lines.price)' },
    rules: ['amount >= 0', 'closesAt >= createdAt'],     // kept by every write
    unique: [['company', 'title']],                       // one title per Company
    stage: { open: [], qualify: 'opened', win: 'qualified', lose: ['opened', 'qualified'] },
  },
  offer: { Team: { month: 49, seats: 5, seat: 9 }, 'Deal.open': 0.001 },
})
```

Two members edit one Deal. The second write sends the `ETag` it read, the first has changed the record since, and nothing is overwritten.

```http title="Response"
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json
X-Cost: price=0, currency="USD"
X-Request-Id: req_5e1c8a04b7d29f36c0a4

{
  "type": "https://pipeline.apis.dev/docs/errors#version_conflict",
  "title": "Precondition Failed",
  "status": 412,
  "detail": "deal_1 changed since it was read: lines and amount.",
  "code": "version_conflict",
  "etag": "\"v7\"",
  "instance": "req_5e1c8a04b7d29f36c0a4"
}
```

That is `PATCH /deals/deal_1` with `If-Match: "v6"`, `updateDeal` over MCP with `_meta.ifMatch`, and `ifMatch` in the SDK.

## How every write behaves
One set of write rules holds on every Interface, so an agent and a form make the same change the same way.

| Write | What it does |
| --- | --- |
| Create | Checks types, rules and keys, assigns the `id`, and answers 201 with the record and its `ETag`; an `Idempotency-Key` makes a retry create once |
| Update | Changes only the fields sent; `null` clears an optional one, and a list or an embedded list is replaced whole |
| A move | Changes the state and the fields its input names, guarded by its state, its Role and the rules, in one write |
| `If-Match` | Optional on every write and sent by every generated UI. A record changed since answers 412 `version_conflict` with its current `ETag` |
| Delete | Keeps the record, restorable; a [Relationship](/patterns/headless-saas.md)'s rule says what happens to the other side |
| Long text together | A `markdown` field several people edit at once merges as they type, shows who is in it, and saves each pause as one change |
| Bulk | Each row of an import or a bulk update passes the same checks, and a failed row carries its [Problem](/reference.md#errors) |

## Types and rules over time
A change to a type or a rule is a diff the plan prints, marked when it breaks a caller.

| Change | What the plan does |
| --- | --- |
| A new optional field, or a new `enum` word | Ships with the next [Version](/reference/platform.md#version); nothing breaks |
| A new required field | Asks for a default or a migration that fills every live record first |
| A widened type, such as `string` to `markdown` | Migrates every record once, checked first; old callers keep reading the old shape on their Version |
| A removed `enum` word, or a narrowed type | Marks it breaking and lists every record that holds a value the new type refuses |
| A new rule | Checks it against every live record first; the plan lists the records that break it, and the rule waits until they are fixed |

| Thing | States | What moves it |
| --- | --- | --- |
| A record's version | `current`, then `superseded` with its `ETag` kept in history | Each write; a read with `at` answers any version |
| A rule | `declared`, `checking`, then `enforced` or `waiting` | The deploy that adds it; the records it lists, fixed by Verbs |
| A computed field | `building`, `current`, `behind` | The Events it reads; a reply says how far behind it is |
| A shared edit | `open`, `merging`, `saved` | The people typing; each save is a change with its author |

## What members, builders and agents meet
- **A member.** Forms that offer only the values a field takes, totals nobody types, and a message that names the rule a save would break.
- **Two people on one record.** The second save never erases the first; it answers what changed, and long text merges.
- **A builder.** One grammar for types, links, keys, rules and formulas, and a plan that says which change breaks whom.
- **An agent.** Typed schemas on every tool, the `ETag` on every read, and a 412 or a 422 that names what to fix.

---

Types, rules and the record's version are checked in one order on every Interface, so no UI keeps a rule of its own.