Skip to content

Types, rules and writes

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

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.

FamilyWhat it holds
Scalarsstring, number, boolean and decimal(p,s); date, datetime with its zone, and duration; url, markdown and file
Contact typesemail and phone, checked on write and stored normalised, and address as its parts with a country
Moneymoney: an amount with its currency, summed only with amounts in the same currency
Closed setsenum(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
Rulesrules: invariants over the record and its links, kept by every write and answered 422 with the rule that failed
Computed fieldscompute: a formula over the record and its links, such as a sum of lines, kept as a projection and never written
Data classesclass 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.

definition.ts
19 lines
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.

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.

WriteWhat it does
CreateChecks types, rules and keys, assigns the id, and answers 201 with the record and its ETag; an Idempotency-Key makes a retry create once
UpdateChanges only the fields sent; null clears an optional one, and a list or an embedded list is replaced whole
A moveChanges the state and the fields its input names, guarded by its state, its Role and the rules, in one write
If-MatchOptional on every write and sent by every generated UI. A record changed since answers 412 version_conflict with its current ETag
DeleteKeeps the record, restorable; a Relationship's rule says what happens to the other side
Long text togetherA markdown field several people edit at once merges as they type, shows who is in it, and saves each pause as one change
BulkEach 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.

ChangeWhat the plan does
A new optional field, or a new enum wordShips with the next Version; nothing breaks
A new required fieldAsks for a default or a migration that fills every live record first
A widened type, such as string to markdownMigrates every record once, checked first; old callers keep reading the old shape on their Version
A removed enum word, or a narrowed typeMarks it breaking and lists every record that holds a value the new type refuses
A new ruleChecks it against every live record first; the plan lists the records that break it, and the rule waits until they are fixed
ThingStatesWhat moves it
A record's versioncurrent, then superseded with its ETag kept in historyEach write; a read with at answers any version
A ruledeclared, checking, then enforced or waitingThe deploy that adds it; the records it lists, fixed by Verbs
A computed fieldbuilding, current, behindThe Events it reads; a reply says how far behind it is
A shared editopen, merging, savedThe 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.