---
url: https://saas.dev/docs/nouns
md: https://saas.dev/docs/nouns.md
---

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

```yaml title="Noun/Lease.mdx"
---
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](https://saas.dev/docs/verbs.md) covers each line. Text after the closing `---` opens the Lease page in your docs at [leaseclock.com/docs/lease](https://leaseclock.com/docs/lease).

Push it with [`npx saas.dev push`](https://saas.dev/docs/saas-dev-cli.md), and leases are live in Test on every surface:

```bash title="Terminal"
npx saas.dev push
```

**API**

```bash
curl https://leaseclock.com/api/leases/4b \
  -H "Authorization: Bearer $LEASECLOCK_TOKEN"
```

```json
{
  "$id": "https://leaseclock.com/api/leases/4b",
  "$type": "Lease",
  "unit": "4b",
  "renter": "dana.whitfield@example.com",
  "endsOn": "2026-11-30",
  "status": "active"
}
```

**CLI**

```bash
npx leaseclock show leases 4b
```

```text
Lease 4B
  unit     4B, Alder Court
  renter   dana.whitfield@example.com
  endsOn   2026-11-30
  status   active
```

**SDK**

```ts title="lease.ts"
import { Leaseclock } from 'leaseclock'

const $ = new Leaseclock({ token: process.env.LEASECLOCK_TOKEN })

const lease = await $.Lease.get('4b')

lease.endsOn   // '2026-11-30'
lease.status   // 'active'
```

In the app, Priya Shah, the leasing coordinator at Northgate Rentals, opens the same lease at [leaseclock.com/app/leases/4b](https://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:

```text
422  Lease.mdx line 2: name "lease" must start upper-case. Rename it to "Lease".
```

Every error code links to its fix in [Errors](https://saas.dev/docs/errors.md), 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:

```yaml
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:

```diff title="Noun/Lease.mdx"
 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](https://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](https://saas.dev/docs/offers.md) 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:

```yaml title="Noun/Lease.mdx"
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:

```yaml title="Noun/Lease.mdx"
building: { type: lookup, lookup: { via: unit, field: building } }
```

```yaml title="Noun/Unit.mdx"
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](https://saas.dev/docs/verbs.md): what people and agents do to a Noun
- [Offers](https://saas.dev/docs/offers.md): the plan your customers buy, and the limits it sets on each Noun
- [Constraints](https://saas.dev/docs/constraints.md): every naming and field rule, and the surface that depends on it
