Nouns
A Noun is something your product keeps track of. Leaseclock has two: a Unit, which is an apartment, and a Lease, which ties a renter to a Unit until a date. A Noun's name becomes its URL, its screen title, its SDK method and its CLI command.
A Noun file
Each Noun is one file in your product's Noun/ folder. 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 }
---name and fields describe what a lease holds. verbs lists what people do to a lease, and Verbs covers each line. Text after the closing --- opens the Lease page in your docs at leaseclock.com/docs/lease.
Push it with npx saas.dev push, and leases are live in Test on every surface:
npx saas.dev pushcurl https://leaseclock.com/api/leases/4b \
-H "Authorization: Bearer $LEASECLOCK_TOKEN"{
"$id": "https://leaseclock.com/api/leases/4b",
"$type": "Lease",
"unit": "4b",
"renter": "dana.whitfield@example.com",
"endsOn": "2026-11-30",
"status": "active"
}In the app, Priya Shah, the leasing coordinator at Northgate Rentals, opens the same lease at leaseclock.com/app/leases/4b. Your customers' agents find it with the MCP server's search tool and read it with fetch.
Names
The name line sets what every surface calls the Noun. Change it and push, and every surface uses the new name.
| Where | name: Lease becomes |
|---|---|
| App | "Leases" in the sidebar, at leaseclock.com/app/leases |
| API | leaseclock.com/api/leases, and "$type": "Lease" on every record |
| SDK | $.Lease, with list, get, create and update, plus a method for each Verb |
| CLI | npx leaseclock show leases and npx leaseclock do Lease.renew |
| MCP | Lease as a type in search and fetch, and Lease.renew in do |
| Docs | A page at leaseclock.com/docs/lease |
A name is singular, starts with a capital letter, and holds letters and digits only, up to 64 characters. The URL uses the plural in lower case: Lease becomes leases, and a Noun named Property becomes properties. Write a two-word name joined, as RentReview, and screens show it as "Rent review".
A name that breaks a rule returns an error with the line and the fix:
422 Lease.mdx line 2: name "lease" must start upper-case. Rename it to "Lease".Every error code links to its fix in Errors, so you or your agent correct the file and push again.
Fields
fields lists what each record holds. A field is a key, a type and any settings the field needs:
endsOn: { type: date, required: true, indexed: true }A key starts with a lower-case letter and joins words in camelCase. Screens label the field from its key, so endsOn shows as "Ends on". Each line of Lease's fields sets what people see and what the API returns:
| Line | In the app | In the API |
|---|---|---|
unit: { type: link, target: Unit, required: true } | A Unit column, and a Unit picker in the form | "unit": "4b", filtered with unit=4b |
renter: { type: email, required: true } | A mail link, and an input that accepts only an address | "renter": "dana.whitfield@example.com" |
endsOn: { type: date, required: true, indexed: true } | A date column the list sorts by, and an "Ends before" filter | "endsOn": "2026-11-30", filtered with endsOn.before=2027-01-01 and sorted with sort=endsOn |
status: { type: choice, options: [...] } | A badge on each row, and a tab for each option | "status": "active", filtered with status=active |
Settings every field takes
| Setting | What it does |
|---|---|
required: true | The record saves only with a value. The form marks the field, and an empty value returns a 422 naming it. |
indexed: true | Lists sort and filter by the field at any size. The first indexed field sets a list's default order. |
label | The name on screens and in docs, when the key's own words are wrong: label: Move-out date. |
description | Help text under the input, on the docs page, and in the SDK type your editor shows on hover. |
default | The value a new record starts with: default: active. |
requiredWhen | Required only when other fields match: requiredWhen: { status: ended }. |
Add a field
Add one line and push. Leaseclock gains rent:
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 }] }
+ rent: { type: money }On the next request, the Leases list has a Rent column, the form has a money input, rent joins the API schema and the SDK's Lease type, and the changelog at leaseclock.com/blog/changelog reads "Leases now track rent". Existing leases have no rent until someone sets it, unless the field has a default.
Field types
The type says what a value is, and every surface reads it. A date gets a date picker, a date column and range filters, and a money field gets a currency input and totals, with nothing else written.
Text
| Type | Holds | Settings | In JSON |
|---|---|---|---|
text | One line, such as a unit's label | max, pattern | "4B" |
longText | Paragraphs, such as notes on a lease. rich: true allows formatting. | max, pattern, rich | "Renter asked about…" |
email | An email address, checked on save | pattern | "dana.whitfield@example.com" |
phone | A phone number, shown as a call link | pattern | "+1 612 555 0142" |
url | A web address, shown as a link | pattern | "https://…" |
pattern is a regular expression every value must match: pattern: '^[0-9]+[A-Z]$' holds a unit label to the form 4B.
Numbers
| Type | Holds | Settings | In JSON |
|---|---|---|---|
number | A count or a measure, such as bedrooms | unit, decimals, range | 2 |
money | An amount in a currency, such as monthly rent | currency, decimals, range | 1850, which is $1,850 |
percent | A rate, such as a late fee | decimals, range | 5, which is 5% |
autoNumber | A number assigned when the record is created, counting up | start | 1042 |
Money is always an amount in the currency's main unit, the same way an Offer writes its price. A money field takes the currency of your Offer unless it sets its own, as currency: EUR. range sets the least and most a value may be: range: { min: 0 }.
Dates
| Type | Holds | In JSON |
|---|---|---|
date | A day, such as when a lease ends | "2026-11-30" |
dateTime | A moment, stored in UTC and shown in each viewer's time zone | "2026-11-30T17:00:00.000Z" |
month | A month, such as the month rent is due | "2026-11" |
Choices
| Type | Holds | Settings | In JSON |
|---|---|---|---|
yesNo | Yes or no, such as whether pets are allowed | true | |
choice | One option from a list, such as a lease's status | options | "active" |
choices | Any of the options, such as a unit's amenities | options | ["parking", "laundry"] |
classification | A standard code from NAICS, O*NET or UNSPSC | classification | "531110" |
Each option is a key with an optional label: { key: active, label: Active }. Without a label, screens show the key with a capital letter.
Links, people and files
| Type | Holds | Settings | In JSON |
|---|---|---|---|
link | Another record, such as a lease's Unit | target, many, onDelete, reverseLabel | "4b" |
team | People or agents in the customer's workspace, such as who manages a unit | many, takes | "priya-shah" |
file | An uploaded file, up to 25 MB, such as a signed lease | accept, maxMb, many | { "id", "name", "mime", "size" } |
Worked out
| Type | Holds | Settings | In JSON |
|---|---|---|---|
lookup | A field read through a link, such as a lease's building | lookup | the source field's value |
rollup | A count, sum, minimum, maximum or average over linked records | rollup | 3 |
Lookups and rollups are read-only. Each stays current with the records it reads, and a request that sets one returns a 422.
Lists
Add many: true to hold a list. phones: { type: phone, many: true } keeps every number a renter gives, and max: 3 caps it at three.
Links
A link field points at a record of another Noun. Lease's unit points at a Unit:
unit: { type: link, target: Unit, required: true }The link reads both ways. The Lease form picks a Unit by its label, and each Unit page lists the leases that point at it, under the name reverseLabel sets. In the API, filter leases by unit with GET /api/leases?unit=4b. In the SDK, it is $.Lease.list({ unit: '4b' }).
onDelete decides what happens to Unit 4B's leases when someone deletes Unit 4B:
onDelete | What happens |
|---|---|
block | The delete returns a 409 while any lease points at the Unit. This is the default. |
clear | Each lease keeps its record and loses its Unit. |
cascade | The leases are deleted with the Unit. |
Every delete is recorded in the record's history, so each of these can be undone.
Read through a link
A lookup shows a field of the linked record. A rollup adds up over the records that link in. Add a building to each lease and a lease count to each Unit:
building: { type: lookup, lookup: { via: unit, field: building } }leaseCount: { type: rollup, rollup: { via: Lease.unit, fn: count } }via names the link: unit on Lease itself, or Lease.unit for the link that points in from Lease. A rollup's fn is count, sum, min, max or average, and every fn except count names the field it reads.
Ids and titles
Every record has an id, and the id is its address: Dana Whitfield's lease is 4b, at leaseclock.com/api/leases/4b. Screens title a record by its name, title or label field, or else its first text field, so Unit 4B shows as "4B" wherever a lease links to it.
Related
- Verbs: what people and agents do to a Noun
- Offers: the plan your customers buy, and the limits it sets on each Noun
- Constraints: every naming and field rule, and the surface that depends on it