Types, rules and writes
Open in
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.
Each check answers before anything is kept, and a refused write costs nothing. Filled: what the writer sees.
What a field can hold
The types are the same on every Interface: 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,; 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,: 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:: 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 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.
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/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/ with If-Match:, 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'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 |
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; 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
ETagon 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.
Model and records
Each Customer's Things, with their Relationships, state, history, files and content, and the search, views and metrics rebuilt from them.
Query, search and analytics
One query grammar on every list, search by words, meaning and facets, metrics as a layer, scheduled reports, and sinks that stream a product's data out.