7. Contracts
A contract is a border where values cross: to and from a system, a person, or another owner. It is the only place anything happens in the world. If something only decides, it is a rule (chapter 6); if it moves values or does something, it is a contract.
Each system declares one contract and maps only to facets, never to another system. Ten systems need ten
maps, not forty-five bridges, and contracts chain without knowing each other: one contract's gives
satisfies another's needs.
7.1 Kinds #
| Kind | Values go | Examples |
|---|---|---|
send |
out to a system | a label printer, a marketplace listing, a product feed, structured data on a page |
receive |
in from a system | a scanner, a catalog lookup, an import, OCR, AI text, an ID allocator, a pricer |
store |
both ways, between facets and an object's own fields | a location, an order |
form |
to a person and back | an editor, checkout, signup, a customer's correction |
offer · take |
between owners (7.8) | a distributor's catalog, a collector's records |
7.2 The long form #
contract: ebayListing
kind: send
label: eBay listing
for: OpticalDisc
needs:
!title$80
!format{DVD|Blu-ray|4K}
!unit::price
writes:
title -> Title
format -> aspects.Format
4K -> "4K UHD Blu-ray"
unit::price -> price.value
reads:
itemId -> ebay:itemId
gives: ebay:itemId
when: unit::offerStatus is listed
who: caller wears Staff
auth: oauth2
scope: https://api.ebay.com/oauth/api_scope/sell.inventory
credential: ebay-seller
retry: 3 every 10m
version: 2026-10-01
| Part | Holds | For kinds |
|---|---|---|
kind |
7.1 | all; required |
label · describe |
what people see | all |
for |
which records it runs on: a class, a kind or a level | all |
needs |
an unnamed class (chapter 5): what must be present, and how narrow, before it may run | all |
gives |
the facets it brings back, so other contracts can chain on it | receive, send, form |
writes |
the map out (7.3) | send, form |
reads |
the map in (7.3) | receive, send (its answer), form |
stores |
the map both ways (7.3) | store |
when |
the trigger: a condition (chapter 6.3) that starts a run when it becomes true, asked for runs started by a caller, entry for a stream contract that runs as its views publish (chapter 12.6.2), or a schedule (7.11) |
all; default asked |
missed |
for a scheduled contract: once, all or skip (7.11) |
send, receive; default once |
stream |
the views whose projection streams the contract carries (chapter 12.6.2); it takes the place of writes and needs |
send |
to |
the party the data goes to (an offer's audience, 7.8; a stream's index, chapter 12.6.2) | offer, send with stream |
from |
the presenter, by domain (7.8) | take |
mode · terms |
how shared data is held, and the presenter's terms for controlled (7.8) |
offer, take · offer |
who |
who may run it: a condition over the caller (7.6) | all |
audience |
the lowest audience (chapter 12.4) that may run it: public, member, staff or owner; the menu (chapter 14.6) and who still apply |
all; default public for a contract with a relation (it grants the consumer's edge, chapter 14.4), member for any other form, public otherwise |
A form a consumer runs on a record they do not own (a buyer cancelling their order, a member keeping
their profile) needs no ownership: audience: member admits them when an edge of theirs covers the record
(chapter 14.4), and a contract that grants a relation admits the caller it is about to admit. The menu
and who narrow from there; nothing in a form's run reads who owns the record.
| origin | the origin of the values it brings in (chapter 1.9) | receive, form, take, store; default reported for systems and declared for forms |
| lands | value (the default), suggestion (7.5) or observation (chapter 9.7) | receive, form, take |
| ref | the field of their payload that is their own ID for the thing; arrivals are bound by it (chapter 9.2a) | receive, form, take |
| relation | the relation of the edge a run grants the caller on a record it creates for them (chapter 14.4): buyer on an order | receive, form |
| evidence | with lands: observation: the evidence type (photo, scan, disc-read, scale …) | receive |
| absorb · ignore · keep-raw · prefix | intake bypass, and the prefix raw fields are carried under (chapter 9.4) | receive, take |
| auth · scope · credential | what the other side requires (7.6) | send, receive |
| retry · undo | run handling (7.5) | send, receive |
| version | the dated version | all |
A part key with a prefix is a package part, carried unchanged.
A contract's name is its identity, like a facet's key (chapter 4.1): it never renames. Bindings (chapter 9.2a), runs and log entries name it, and a renamed contract would orphan every one of them. A new label is not a rename; a contract that should go away is retired and replaced by one with a new name.
7.3 Maps #
Each map line is from -> to, in the direction values flow:
- in
writes,ourAddress -> theirField; - in
reads,theirField -> ourAddress; - in
stores,ourAddress -> theirField, read both ways.
Our side is an address (chapter 1.10), with the level prefix allowed (unit::price). Their side is a
field path in their payload: names separated by ., a name in quotes when it holds anything but letters,
digits and _ (aspects."Run Time"), [n] for an array position and [*] for every item.
Value maps are lines inside a map line, also in the direction of flow:
reads:
aspects.Format -> format
"4K UHD Blu-ray" -> 4K
"Blu-ray Disc" -> Blu-ray
Our side of a value map MUST be a real value of the facet (never an alias). A value the map doesn't name
passes through unchanged and is folded as usual (chapter 9): so a value map holds only what aliases and
shapes can't do. A value that means "does not apply" in their vocabulary maps to -.
A field of their payload that no reads line names stays on the intake record only (outcome unknown,
chapter 9): it is kept as received and searchable by staff, but nothing reaches the record. To carry a field
onto the record uninterpreted, map it to a key under a prefix: aspects.Studio -> market:Studio.
A map line MAY instead go to or from a literal: "USD" -> price.currency. A contract never computes:
anything that isn't a map, a value map or a literal is the other side's work.
7.4 Running a contract #
A run goes through seven steps, in order. Only step 4 has an effect.
- Permission and trigger. The caller holds
runfor the contract on the record (chapter 14.6; for an arrival, chapter 9.2a, on the record it is within, else anywhere by an admin or control edge, or as an admitted member taking part, chapter 14.4), the contract'saudienceadmits them,whoholds for the caller when the contract has one, andwhenholds (or a caller asked). - Needs. The record satisfies
needsas a class: every!line has a value, and every value fits its narrowing. If not, the run stops before acting, naming what is missing. - Map out.
writesbuilds the payload. - Act. The host performs the act: calls the system, prints the label, shows the form.
- Map back.
readsturns the answer into values, each with a source:originfrom the contract,viathe log entry for this run. A contract MAY abstain for a field, or for the whole answer: it says it doesn't know. An abstention is recorded on the intake record and never becomes a value; "a wrong value is worse than none". - Rules. What came in runs through intake (chapter 9) and the rules (chapter 6).
- Record. The run is logged with the versions of the contract, the package and the rules it used.
7.5 Runs #
Every run has a run ID (R-118), and the log entry for its writes names it (chapter 8).
- Idempotency. A run's key is the contract, the record (or the
refwhen the run has no record yet, chapter 9.2a) and the hash of its mapped-out payload; for a scheduled run, the tick takes the payload's place (7.11), and for a stream batch, the view and itsseqrange (chapter 12.6.2). A host MUST NOT act twice for the same key unless the earlier run failed before acting. - Many records, one record at a time. A run over several records (an import, a batch listing) is one
entry and one idempotency key per record (chapter 8.5): each item is
actedorfailedon its own, a refusal refuses that item, and a retry acts only on the items that failed. There is no partial success of one item. - Two entries around the act. A send is logged
startedbefore the host acts (its idempotency key, the hash of its mapped-out payload, and the versions it uses) andactedorfailedafter, so a host that restarts between the two finds the run and never acts twice. The payload itself is kept beside the log, as intake bytes are (chapter 9.1), never in it: a run write holds its hash, so a payload holding a person's data can be erased (chapter 8.5.2). - Retry.
retry: 3 every 10mretries a failed act up to three times, ten minutes apart, from the kept payload, never re-mapping it. With noretry, a failed act is not retried. A run still failed after its last attempt stays failed and opens a review itemrun:<contract>(chapter 9.5), grouped by contract across records; a person's decision is a new run or nothing. - Late answers. An answer that arrives after its run closed names the run (the run ID travels with the
act) and is taken as intake against it, folding like any other with that run as its
via. An answer that names no run the store knows lands nowhere: it opens a review item and is never guessed onto a record. - Undo.
undo: ebayEndListingnames the contract that reverses this one; a run of it names the run it reverses ascause. The engine never assumes a contract is reversible. - Suggestions. With
lands: suggestion, values that arrive don't change the record: they are kept as suggestions on it, for a person to accept. Customer corrections land this way.
7.6 Authorization and authentication #
Authorization is inside CRUBAL. Whether a caller may run a contract, edit a value or see one is decided
by the caller's edges and the package's permission menu (chapter 14): that is the one spelling of
permission. who is a further condition (chapter 6.3) over the caller, which is a record: a person, an
organization or a system. Its values are read with the caller prefix (caller.role is staff), and
caller wears Staff tests its classes. who narrows what the menu allows and never widens it. A who over
the caller's values or classes reads the caller's record in this owner's log; a caller hosted elsewhere
(chapter 14.8) has none here, so for them only caller may … (what their edges hold, chapter 14.6) can hold,
and audience is the usual gate. What a caller
sees is decided by show rules (chapter 12) and the view's partition (chapter 12.6).
Authentication is the host's job. Sign-in, OAuth flows and API keys belong to whatever runs the contracts. A contract only declares what the other side requires:
auth: the scheme, such asoauth2,apikey,basicornone;scope: the scopes or permissions it asks for;credential: a name the host resolves to its secret (ebay-seller).
No secret ever appears in CRUBAL text or its JSON. A package MUST be publishable as it is.
7.7 Forms #
A form is a contract with a person. Its screen assembles itself:
- the fields are
needs, in its lines' order and sections (chapter 5.3.3; aneedsblock MAY holdsection:lines), each drawn with its facet's own control (a choice as bubbles, a date at its precision); writesshows current values,readstakes the person's answers; a form with noreadsof its own takes each answer by its facet's name, since its fields are its needs;whois the person, andoriginsays what the person is doing:declaredwhen they state it,observedwhen they read it off a real instance in front of them.
7.8 Offer and take #
Sharing between owners is two contracts, one on each side. Only the overlap flows.
An offer travels as a border wrapper (chapter 3.8): with the records, their sources and the signed entries they came from, it names the package version they were written under. The receiver applies the version rule (chapter 10.1): the same version is taken; an older one is restamped through the recipes between; a newer one is held, as a review item, until the receiver upgrades.
contract: catalogOffer
kind: offer
for: Movie
gives: title, format, releaseDate, runtime, cast
to: public
mode: gathered
contract: catalogTake
kind: take
from: distributor.example
reads:
title -> title
runtime -> runtime
mode: gathered
lands: value
to(offer):public,followers, named parties,link, orprivate.linkshares with anyone holding an unguessable token: unpublishing revokes it without deleting anything, and republishing keeps the same token, so links already handed out keep working. Visibility per record, facet or value is decided byshowrules.from(take): the presenter, by domain.modeis defined by three properties. The four named modes are presets, and a package MAY name other combinations:
| Mode | copy |
updates |
control |
|---|---|---|---|
dump |
yes | no | consumer |
observed |
no (read through) | yes | presenter |
gathered |
yes | yes, as intake | consumer; their edits win |
controlled |
yes | yes | the presenter's terms |
terms(offer, controlled):reshare: no,expires: 2027-01-01,revocable: yes.- Only
observedis enforced by the engine (a read-through is never stored). The others are agreements the engine honours, backed by licence and law. - A presenter's update never overrides a value a person entered on the consumer's side, unless the consumer
took
controlledterms. - Parties are identified by domain, with their keys published in the domain's well-known file,
https://<domain>/.well-known/crubal.json(chapter 11.7). A request one owner's host makes to another's proves its party by a signature over the request with that key (an HTTP message signature, RFC 9421, over the method, the target, the body's digest and the date).
7.9 The JSON form #
{
"contract": "ebayListing",
"kind": "send",
"for": "OpticalDisc",
"needs": [
{ "facet": "title", "required": true, "length": "80" },
{ "facet": "format", "required": true, "values": ["DVD", "Blu-ray", "4K"] },
{ "facet": "price", "required": true, "level": "unit" }
],
"writes": [
{ "from": "title", "to": "Title" },
{ "from": "format", "to": "aspects.Format", "values": [ { "from": "4K", "to": "4K UHD Blu-ray" } ] },
{ "from": "unit::price", "to": "price.value" }
],
"reads": [ { "from": "itemId", "to": "ebay:itemId" } ],
"gives": ["ebay:itemId"],
"when": "unit::offerStatus is listed",
"who": "caller wears Staff",
"auth": "oauth2",
"credential": "ebay-seller",
"retry": { "times": "3", "every": "10m" },
"version": "2026-10-01"
}
needs uses chapter 5.9's class line objects. A literal map side is {"literal": value}.
7.10 Well-formed contracts #
A package MUST be refused at publish when a contract breaks these:
- Every part is one its kind allows (7.2).
needsis a well-formed class (chapter 5.10).- Every our-side address names a facet the package defines (or an unknown-key prefix in
reads). - Every our-side value in a value map is a real value of its facet.
credentialis a name, never a value; nothing in the contract is a secret.whenisasked,entry, a schedule (7.11) or a condition that parses (chapter 6.3), andwhois a condition that parses.undonames a contract of the same package.streamnames views of the package and comes withtoand withoutwritesorneeds;when: entrycomes only withstream;missedcomes only with a schedule.relationis a relation the package declares (chapter 14.6).
7.11 Schedules #
A contract's when MAY be a schedule:
schedule = "every " ( N " minutes" / N " hours" / "minute" / "hour"
/ "day" [ " at " hh:mm ]
/ weekday [ " at " hh:mm ]
/ "month on " D [ " at " hh:mm ] ) [ " [zone: " iana "]" ]
every day at 03:00, every hour, every 15 minutes, every monday at 09:00, every month on 1 at 00:00 [zone: America/Chicago]. A time of day is UTC unless the zone says otherwise; a monthly day is 1 to
28, so every month has it.
- Ticks are computed. The ticks of a schedule follow from the schedule and a moment alone, so every
store agrees on them. The host offers its clock as
now(chapter 6.3), and every tick after the last one run and at or beforenowis due. - A schedule begins when its contract is published (chapter 11.5): nothing before that is due, so a contract published in October never owes a year of nightly runs.
- A tick runs once. A run's idempotency key is the contract, the tick and the record when there is one (7.5), so a host that restarts never runs a tick twice; the run's entry carries the tick as its cause.
- Missed ticks are the contract's choice,
missed:once(the default: one run, for the latest tick due),all(one run per tick, in order) orskip(none). What was skipped is reported, never run. - What runs. A contract with
forruns over every record it selects, one entry each (chapter 8.5). A stream contract pushes its views' entries (chapter 12.6.2). Any other runs once, with no record. - Rules never schedule anything: a schedule is a trigger, and only a contract has one (chapter 6).