Constraints
A Noun is a name, its fields and its Verbs. Field keys are camelCase, and a Verb moves a record between named states. Each rule exists because some surface depends on it, and each one names that surface.
Inside these rules, your agent writes a Noun file right the first time, and each improvement to a generated surface reaches your product the day it ships.
The shape of a Noun
This is Leaseclock's Lease, in full:
---
name: Lease
fields:
unit: { type: link, target: Unit, required: true }
renter: { type: email, required: true }
endsOn: { type: date, required: true, indexed: true }
status: { type: choice, options: [{ key: active }, { key: ended }] }
verbs:
renew:
does: Move the end date forward
input: { endsOn: { says: the new end date, type: date, required: true } }
from: [active]
end: { does: End the lease, from: [active], to: ended }
---The file holds the three keys that make up the Noun: name, fields and verbs. A Noun that people only keep, such as Unit, leaves out verbs. The folder says what kind of definition a file is, and its path is its address, so Noun/Lease.mdx is the Noun at leaseclock.com/Noun/Lease and never repeats either.
What one name becomes
Each name in the file appears on every surface, spelled the same way or derived from it by one fixed rule:
| Surface | Lease | endsOn | renew |
|---|---|---|---|
| App | "Leases" in the sidebar | An "Ends on" column and a date picker | A Renew button on each active lease |
| API | leaseclock.com/api/leases | "endsOn": "2027-11-30" | POST /api/leases/4b/renew |
| CLI | npx leaseclock show leases | --endsOn 2027-11-30 | npx leaseclock do Lease.renew 4b |
| SDK | $.Lease | lease.endsOn | $.Lease.renew('4b', { endsOn }) |
| MCP | search returns leases/4b | "input": { "endsOn": … } | do with "verb": "Lease.renew" |
| Docs | leaseclock.com/docs/lease | The Ends on row of the Lease page | leaseclock.com/docs/lease#renew |
Rename Lease to Tenancy and every cell follows: /api/tenancies, $.Tenancy, "Tenancies" in the sidebar.
The rules
Keys a Noun takes
A Noun file takes name, fields and verbs, and no other key. Every surface reads those three keys, so a key outside them would reach one surface and miss the rest.
422 Unit.mdx line 3: a Noun takes name, fields and verbs. Rename "properties" to "fields".Noun names
A Noun's name starts with a capital letter and holds letters and digits, up to 64 characters: Lease, Unit, RentalAgreement. The name is the URL segment (leases), the screen title ("Leases"), the SDK namespace ($.Lease), the CLI target (Lease.renew) and the docs page (/docs/lease), and each is derived from that one spelling.
422 Lease.mdx line 2: name "lease" must start upper-case. Rename it to "Lease".Field keys and Verb names
Field keys and Verb names are camelCase: endsOn, renew, markPaid. Each one is a JSON key in the API, a property in the SDK, a flag in the CLI and an argument your customers' agents send. The App writes its label from the same key, so endsOn heads a column "Ends on" and markPaid labels a button "Mark paid".
422 Lease.mdx line 6: field "EndsOn" must start lower-case. Rename it to "endsOn".Field types
Every field has a type from the set on Nouns: text, email, date, money, link, choice and the rest. The type decides the list column, the form input, the check on every write, the JSON schema and the SDK type, so endsOn gets a date picker in the App and an ISO date such as 2027-11-30 in the API and the SDK.
422 Unit.mdx line 6: field "bedrooms" has no type "integer". Use "number".Links
A link field names its target, and the target is a Noun in the same product. Lease's unit points at Unit, so the form offers a Unit picker, the list shows the unit as a link, and the API rejects a lease for a unit that does not exist.
422 Lease.mdx line 4: field "unit" links to "Units", and no Noun has that name. Use "Unit".Verbs and states
A Verb says what it does in does, and that sentence is the docs line, the CLI help, the SDK hover text and the description your customers' agents read. A Verb that changes state names it: from lists the states a record must be in, and to names the state it lands in. Both are options of the Noun's status field.
Only Verbs change status. The Lease form has no status box, and every change of state has a button, an endpoint, a history entry and an undo. The App disables Renew on an ended lease and shows why, and every other surface answers the same way:
npx leaseclock do Lease.renew 5a --endsOn 2027-09-30
# 409 Lease 5A is ended. Renew runs only on an active lease.A state that status does not list is an error on push:
422 Lease.mdx line 13: verb "end" moves to "closed", which is not a status. Use "ended", or add "closed" to status.Prices
What your customers pay you lives in an Offer, as an amount, a currency and a unit:
price: { amount: 29, currency: USD, unit: month }
limits: { Unit: 200 }The pricing page, the Stripe price, Checkout and the upgrade prompt in the App all read that one line, so they always agree. limits sets how many Units each customer keeps, and the API holds every customer to it. A money field such as a lease's rent is a record's data, and it charges nobody.
422 pro.mdx line 3: price has no key "interval". Use "unit: month".Logic
Code you write runs as a Verb's function, in fn. Renew's function sets the new end date, and the Verb around it gives that code its button, its endpoint, its CLI command, its tool for agents, a preview before it runs and an undo after. A screen of your own reads and writes through the same Verbs, so permissions and undo hold there too. Defaults and overrides shows how to replace a screen.
When a file breaks a rule
npx saas.dev push checks every file before it saves anything. When a line breaks a rule, the push saves nothing and prints the file, the line and the fix:
npx saas.dev push
# 422 Lease.mdx line 6: field "EndsOn" must start lower-case. Rename it to "endsOn".
# After the fix:
npx saas.dev push
# Pushed to Test: Unit.mdx, Lease.mdx, pro.mdxYour agent reads the same line, makes the fix and pushes again. Every code has an entry in Errors with its cause and fix.
What fits
SaaS.Dev fits products made of records and the things people do to them. Leaseclock is one: it keeps Units and Leases, and property managers renew and end leases. Trackers, CRMs, booking and scheduling tools, client portals and back offices take the same shape.
Inside a product that fits, the parts that are yours have a place. A custom screen replaces one view and keeps your theme and permissions, and custom logic is a Verb's function. The fewer defaults you replace, the more improvements reach your product for free.
Games, document and design editors, and video or audio tools need a different platform. Their core is a canvas, a game loop or a media stream, and no list, form or endpoint describes it.