Rules: how a Service meets licensing by design
One table your counsel writes, with a row per act. Each act is reserved to a credential, open to the brand, refused here, or the client's own decision, and the gate holds every job to the version it was ordered under.
A rules table answers one question: which acts in this work need a credentialed person, which does the brand perform itself, and which are never performed here?
Rules() is how a Service meets licensing by design. Your counsel writes one table, with rows per Service. The AI still does the work. The table decides which acts a credentialed person performs, and the gate holds every job to it, the same way for AI and people. services.dev ships no rules: every position in the table is your brand's.
One row, and the Service that names the table:
cat rules.tsimport { Rules } from 'services.dev'
export default Rules({
version: '2026-10',
services: {
translation: {
sign: { reserved: 'sworn translator', where: '*', position: 'A sworn translator signs a translation filed with a registry.' },
},
},
})check reads the row back on the Service it binds:
npx services.dev checkrules.ts · 6 lines · rules 2026-10 · 1 Service · 1 row
who A translator signs every document before delivery. (rules 2026-10)
bounds reserved sign to sworn translatorEach row maps an act, by the name a step performs, to one of four kinds, and a reserved row may name a credential the brand holds instead of a person's. As check reads them, from the legal example and the filing brand:
| Row | What it means | check reads |
|---|---|---|
{ reserved: 'bar admission', where: 'client' } | A person who holds the credential performs it, in the region the job's facts give | reserved sign to bar admission where the client is |
{ reserved: { brand: 'efin' } } | The brand performs it, under a credential it holds | reserved file to the brand's efin |
{ open: true } | The brand performs it directly | open send and courier |
{ refused: true, route } | Never performed here. An order for it gets the route | refused litigate |
{ client: true } | The client's decision alone | the client decides settle, alone |
npx services.dev check # each Service's bounds lineservices/demand-letter.ts
bounds reserved sign to bar admission where the client is · the attorney sells the fee · open send and courier · the client decides settle, alone · refused litigateEvery row records its position: the reason counsel gives for it. The Service page prints it beside the act, so a buyer reads the boundary before they order:
open http://localhost:8787/contract-reviewThe boundary
advise: reserved to bar admission where the client is, in US-CA, US-NY and US-TX only · Advice on a contract is legal advice in the client's state.
send: the brand performs it · Sending a signed memo to its client is a delivery act.
litigate: never performed here · Litigation is retained directly, never ordered here.There is no global default. A step that performs an act with no row, under its Service or shared, fails act_unruled, and a reserved act no role can perform fails reserved_act_unstaffed. A Service can't ship with an act the table doesn't decide.
The table is deep-frozen. New positions are a new version. A job pins the version it was ordered under, and every attestation on its record names it:
→ POST /reviews/tsk_7Hq2/attest {"checklist":["the claims read on the disclosure","every inventor is named","a reasonable inquiry was made"]} # as mbr_7Hq2 14:02 · step.committed · sign · person · patent agent · * · mbr_7Hq2
attested {"member":"mbr_7Hq2","credential":"patent agent","region":"*","hash":"sha256:27d7bdec3be22fd633af279053beef779c9c5080629816867bbfc4e4593a5b33","rules":"2026-10"}Product config can't edit the table, and neither can policy, a reseller or an escalation. Only the brand's next version changes a position, and only for jobs ordered under it. A reserved act commits only with the attestation of a person who holds the credential, under the pinned version: that is the gate's authority check, in Propose, then the gate commits.
| Key | What it says | Default |
|---|---|---|
version | The table's version. A job pins the one it was ordered under | required |
services.<id>.<act> | A row for one act of one Service | none: no fallback |
shared.<act> | A row every Service that names the act shares | none |
position | Counsel's reason, printed on every surface that shows the row | required |
reserved | A credential expression, or { brand } | — |
and | A credential the brand holds as well: { brand: 'efin' } | none |
where | Where the act happens: 'client', 'governing', 'recipient', '*' or your function | '*' |
regions | Where the row is ratified, in ISO 3166 | every region |
kind | 'advice' reserves a class of answer in the job's thread, apart from a signature | 'act' |
rail | Who sells the fee for the act's role: 'brand', 'member' or 'entity' | 'brand' |
self | The client may perform the act themselves | off |
flags | Advisory signals raised when facts straddle regions. They never block | none |
open | The brand performs the act | — |
refused, route | Never performed here, and where it is: a URL, or a Service of yours that does it | — |
client | The client's decision alone | — |
cite | The authorities counsel relies on, printed after the row on every refusal that cites it | none |
Where an act happens has where, regions and flags, Talk has kind: 'advice', and When the client does it themselves has self.
| Surface | What a row changes |
|---|---|
| API | An order for a refused act, or in a region a row doesn't ratify, answers 403 and charges nothing |
| CLI | check prints each Service's bounds line, and the CLI refuses as the API does |
| MCP | A tool call for a refused act answers with the route to hand on |
| SDK | The API's answer, unchanged |
| Site | The Service page prints the boundary, each act with its position |
| Docs | Each refusal's code has a page on the brand's docs, at the doc_url the refusal carries |
| App | Every attestation on the record each app reads names the version |
| Code | When | Charged |
|---|---|---|
act_refused | An order names an act a row refuses | nothing |
region_not_ratified | The job's region is outside a row's regions | nothing |
brand_unverified | A brand credential the row needs is unverified or stale | nothing |
reserved | The person who attested doesn't hold the credential | nothing |
act_unruled | check: a step's act has no row | — |
reserved_act_unstaffed | check: no role holds the reserved credential | — |
rail_conflict | check: a role's pay.rail contradicts its row's | — |
brand_credential_missing | check: a row names a brand credential Brand() doesn't declare | — |
row_unpositioned | the file won't load: Rules() refuses a row with no position | — |
row_kind | the file won't load: Rules() refuses a row that isn't exactly one of reserved, open, refused or the client's | — |
Refusals and cures has what each answer carries.
The rows in the examples are illustrative. Your counsel writes your brand's. For the legal side, law.org.ai collects positions by act and region.
- Legal. Five Services in one table: advice reserved where the client is, signing and filing reserved to a practitioner, litigation refused. The rules table.
- Tax. An e-filing reserved to the brand's own e-file number,
{ reserved: { brand: 'efin' } }. Credentials the brand holds. - Sales. Its setters and closers hold no credential, so no row reserves their acts. A sales brand that needs one adds a row, the same way.