Rules: how a Service meets licensing by design

The running jobjob_7Hq2 · waiting · on approval · $49.00 held

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:

Run
cat rules.ts
magic values: none
import { 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:

Run
npx services.dev check
magic values: none
rules.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 translator

Each 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:

RowWhat it meanscheck reads
{ reserved: 'bar admission', where: 'client' }A person who holds the credential performs it, in the region the job's facts givereserved sign to bar admission where the client is
{ reserved: { brand: 'efin' } }The brand performs it, under a credential it holdsreserved file to the brand's efin
{ open: true }The brand performs it directlyopen send and courier
{ refused: true, route }Never performed here. An order for it gets the routerefused litigate
{ client: true }The client's decision alonethe client decides settle, alone
Run
npx services.dev check   # each Service's bounds line
magic values: none
services/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 litigate

Every 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:

Run
open http://localhost:8787/contract-review
magic values: none
The 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:

Call
→ POST /reviews/tsk_7Hq2/attest {"checklist":["the claims read on the disclosure","every inventor is named","a reasonable inquiry was made"]}   # as mbr_7Hq2
magic values: applicant.email = signs@example.test, client.email = signs@example.test, member = mbr_eligible
  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.

KeyWhat it saysDefault
versionThe table's version. A job pins the one it was ordered underrequired
services.<id>.<act>A row for one act of one Servicenone: no fallback
shared.<act>A row every Service that names the act sharesnone
positionCounsel's reason, printed on every surface that shows the rowrequired
reservedA credential expression, or { brand }—
andA credential the brand holds as well: { brand: 'efin' }none
whereWhere the act happens: 'client', 'governing', 'recipient', '*' or your function'*'
regionsWhere the row is ratified, in ISO 3166every region
kind'advice' reserves a class of answer in the job's thread, apart from a signature'act'
railWho sells the fee for the act's role: 'brand', 'member' or 'entity''brand'
selfThe client may perform the act themselvesoff
flagsAdvisory signals raised when facts straddle regions. They never blocknone
openThe brand performs the act—
refused, routeNever performed here, and where it is: a URL, or a Service of yours that does it—
clientThe client's decision alone—
citeThe authorities counsel relies on, printed after the row on every refusal that cites itnone

Where an act happens has where, regions and flags, Talk has kind: 'advice', and When the client does it themselves has self.

SurfaceWhat a row changes
APIAn order for a refused act, or in a region a row doesn't ratify, answers 403 and charges nothing
CLIcheck prints each Service's bounds line, and the CLI refuses as the API does
MCPA tool call for a refused act answers with the route to hand on
SDKThe API's answer, unchanged
SiteThe Service page prints the boundary, each act with its position
DocsEach refusal's code has a page on the brand's docs, at the doc_url the refusal carries
AppEvery attestation on the record each app reads names the version
CodeWhenCharged
act_refusedAn order names an act a row refusesnothing
region_not_ratifiedThe job's region is outside a row's regionsnothing
brand_unverifiedA brand credential the row needs is unverified or stalenothing
reservedThe person who attested doesn't hold the credentialnothing
act_unruledcheck: a step's act has no row—
reserved_act_unstaffedcheck: no role holds the reserved credential—
rail_conflictcheck: a role's pay.rail contradicts its row's—
brand_credential_missingcheck: a row names a brand credential Brand() doesn't declare—
row_unpositionedthe file won't load: Rules() refuses a row with no position—
row_kindthe 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.