---
url: https://saas.dev/docs/how-it-works
md: https://saas.dev/docs/how-it-works.md
reads: [every Noun, theme.json]
generates: 2 of 34 outputs
next: https://saas.dev/docs/get-paid.md
---

# How generation works

> Your files compile to one model that generates every output on each request. Every output has a default, and the constraints make generation possible.

## One model

Your four files compile to one model, and every output reads it. Nothing is copied into your repo, so no generated code drifts from the definition.

0. **Send.** [You send the files](https://saas.dev/docs/definition.md#sample) as they are, in try mode first.
1. **Parse.** The engine reads each file's frontmatter, its `$type` and its `$id`.
2. **Check.** It refuses any key, name or input outside the shape, and the refusal names the fix.
3. **Compile.** It builds your [Startup](https://startupbuilder.dev/business/nouns/startup)'s model on the platform's own [Nouns](https://startupbuilder.dev/business/nouns/noun) and stamps it as a dated [Version](https://startupbuilder.dev/business/nouns/version).
4. **Generate.** The API routes, the typed `$`, the MCP tools, the CLI, the schemas, the App screens and the Docs each read that model.
5. **Serve.** Every request reads the current Version, so an improvement to a default reaches you on the next request.

### Change one line

Add rent to Lease:

```yaml title="leaseclock.com/Noun/Lease.mdx"
  rent: { type: money, currency: USD }
```

Send the field in try mode:

```bash title="PATCH /leaseclock.com/nouns/Lease"
curl -X PATCH "https://api.sb/leaseclock.com/nouns/Lease" -H 'Prefer: mode=try' \
  -H "authorization: Bearer $SB_TOKEN" -H 'content-type: application/json' \
  -d '{"fields":{"rent":{"type":"money","currency":"USD"}}}'
# "mode": "try", "major": false, "stored": false,
# with the Version it would become and every Interface it touches
```

The answer arrives before you commit, and it covers every [Interface](https://startupbuilder.dev/business/nouns/interface) at once:

| | Before | After |
| --- | --- | --- |
| `$.Lease` | no `rent` | `rent?: number` |
| REST operations | 18 | 18 |
| Admin list | 4 of 7 columns | Rent joins before Unit, 5 of 7, with a money input |

Removing `renter` answers `"major": true`, because records hold it. Renaming its key in the file is refused once records exist, and the refusal names the one way to rename a field, which carries the records with it:

```bash title="PATCH /leaseclock.com/nouns/Lease"
curl -X PATCH "https://api.sb/leaseclock.com/nouns/Lease" -H 'Prefer: mode=try' \
  -H "authorization: Bearer $SB_TOKEN" -H 'content-type: application/json' \
  -d '{"ops":[{"op":"rename","path":"/fields/renter","to":"tenantEmail"}]}'
```

Each commit becomes the head [Version](https://startupbuilder.dev/business/nouns/version). A commit made on an older head is rebased, or refused with [`409 Conflict`](https://startupbuilder.dev/code/api/errors/conflict) where the two overlap; send `baseVersionId` to say which head you read. `POST …/nouns/Lease/publish` moves the published pointer, and `GET …/nouns/Lease@2026-10-24` reads the Noun as of a date.

## The defaults

Every SaaS answers the same questions. The platform answers each one once, for every Startup on it.

| Question | Answered for you |
| --- | --- |
| Who sees a new record? | Only those you grant: private by default |
| What does delete do? | Records a Version, so nothing is lost |
| How many come back in a list? | At most 200 |
| Can a GET change anything? | No. A GET on a Verb's URL previews and runs nothing |
| Does a retried Verb run twice? | No. A Verb POST outside try needs an Idempotency-Key |
| Who may do what? | Four Access levels: owner, admin, member, viewer |
| What are the methods called? | get, list, find, create, update, revert, plus each declared Verb |
| What does your agent's script do by default? | A $ call without a mode runs in try and stores nothing |
| How many things does a script commit? | All its writes, or none |
| Where does the price live? | In the Offer, as one amount and currency every output reads |
| What does a refusal look like? | Typed, with its message, and it costs nothing |
| What does a list screen show? | At most 7 columns, and one button per Verb |

## Overrides

The defaults are the product. An override replaces one output, never the definition, and everything else stays generated.

| Override | What you then maintain | What stays generated |
| --- | --- | --- |
| Theme | The token values | Everything |
| CSS | Your rules | Everything; a rule can mask a later token change |
| A field’s input | That one input | The field’s API, schema and filter |
| A Noun’s view | That view’s layout | Routes, data, Verb buttons and the other views |
| A page or screen | That page’s or screen’s layout | Theme, Access levels, try and revert |
| A Function body | What the Verb does | Its URL, preview, undo, button and agent call |
| An API route of your own | Refused: declare a Verb | — |

A Startup with no theme file renders in the default theme, on its Site and in every App screen. Three lines restyle Leaseclock's Site, Admin and SaaS App:

```json title="leaseclock.com/theme.json"
{
  "fonts": ["Libre Caslon Text:wght@700"],
  "light": { "accent": "#8A4B0F" }
}
```

Every primary button takes the accent. A value that could end a CSS declaration is refused, never emitted.

A page of your own returns catalog blocks with computed props. The renderer writes the markup and the CSS:

```tsx title="leaseclock.com/components/NoticeBy.tsx"
import { block } from 'startup-builder/blocks'

export default function NoticeBy({ endsOn, days = 60 }: { endsOn: string; days?: number }) {
  const by = new Date(Date.parse(endsOn) - days * 86_400_000).toISOString().slice(0, 10)
  return block('facts', { title: 'Renewal notice', items: [
    { label: 'Lease ends', value: endsOn },
    { label: `Give notice by (${days} days)`, value: by },
  ] })
}
```

The renderer applies the theme and checks the props, and you maintain that one component. A screen you add to the App reads and writes through the generated API, so [Access levels](https://startupbuilder.dev/business/nouns/access-level), try mode and revert still apply.

An API route of your own is refused. New behaviour is a declared [Verb](https://startupbuilder.dev/business/nouns/verb), which gets its URL, preview, undo, button and agent call from one block.

## The constraints

The engine refuses what it cannot generate from. Each refusal buys something every output relies on:

| Rule | What it buys | The refusal |
| --- | --- | --- |
| Every key is declared | Every Interface reads the same fields | `"properties" is not part of a definition` |
| Names follow one grammar | The name is the URL, the method and the screen title | `PascalCase, letters and digits` |
| Prices live in Offers | The Site, pricing and Checkout read one number | `Price has no field "interval"` |

An agent writes, is refused, fixes and commits. Here is the whole loop on one Unit:

```text title="Your agent"
Read https://saas.dev/llms.txt. Write the Nouns, Verbs and Offer for <your product> in Leaseclock's shape. Check each file with Prefer: mode=try, fix every refusal, and show me the files before you commit.
```

**Refused.**

```bash title="PATCH /leaseclock.com/nouns/Unit"
curl -X PATCH "https://api.sb/leaseclock.com/nouns/Unit" -H 'Prefer: mode=try' \
  -H "authorization: Bearer $SB_TOKEN" -H 'content-type: application/json' \
  -d '{"properties":{"bedrooms":{"type":"number"}}}'
```

```json title="400 · application/problem+json"
{
  "type": "https://startupbuilder.dev/code/api/errors/invalid-input",
  "status": 400,
  "title": "Invalid input",
  "code": "invalid_input",
  "detail": "\"properties\" is not part of a definition (name, supertypes, collection, description, fields, abstract, trait, traits, bounds, value, rules, open, indexes, counters, counted, facets, records, definition, subjects, verbs)"
}
```

**Fixed, in try mode.**

```bash title="PATCH /leaseclock.com/nouns/Unit"
curl -X PATCH "https://api.sb/leaseclock.com/nouns/Unit" -H 'Prefer: mode=try' \
  -H "authorization: Bearer $SB_TOKEN" -H 'content-type: application/json' \
  -d '{"fields":{"bedrooms":{"type":"number"}}}'
```

```json title="200 · application/json"
{
  "name": "you update Noun Unit",
  "verb": "update",
  "object": "https://api.sb/leaseclock.com/nouns/Unit",
  "mode": "try",
  "status": "Done",
  "input": { "fields": { "bedrooms": { "type": "number" } } },
  "major": false,
  "stored": false
}
```

**Committed**, the same call without `Prefer`:

```json title="200 · application/json"
{
  "$id": "https://api.sb/leaseclock.com/actions/7k2m",
  "name": "you update Noun Unit",
  "verb": "update",
  "object": "https://api.sb/leaseclock.com/nouns/Unit",
  "mode": "do",
  "status": "Done",
  "stored": true,
  "revert": "https://api.sb/leaseclock.com/actions/7k2m/revert"
}
```

api.sb's own Nouns take the same shape, and their types need no key:

```bash title="Terminal"
curl -s 'https://api.sb/$.d.ts?nouns=Startup' | grep -A1 'Sell it'
# /** Sell it, with its Customers, to another party · Live|Dormant → Sold · modes try|send · a legal Decision */
# sell(target: string, input: StartupSellInput, o?: { mode?: 'try' | 'send' }): Promise<Action>
```

| Noun | Verbs |
| --- | --- |
| [`Capability`](https://api.sb/$.d.ts?nouns=Capability) | 3 |
| [`Change`](https://api.sb/$.d.ts?nouns=Change) | 7 |
| [`Code`](https://api.sb/$.d.ts?nouns=Code) | 3 |
| [`Collection`](https://api.sb/$.d.ts?nouns=Collection) | 5 |
| [`ContactPoint`](https://api.sb/$.d.ts?nouns=ContactPoint) | 5 |
| [`Dataset`](https://api.sb/$.d.ts?nouns=Dataset) | 3 |
| [`Decision`](https://api.sb/$.d.ts?nouns=Decision) | 3 |
| [`Deck`](https://api.sb/$.d.ts?nouns=Deck) | 3 |
| [`Domain`](https://api.sb/$.d.ts?nouns=Domain) | 6 |
| [`Field`](https://api.sb/$.d.ts?nouns=Field) | 3 |
| [`Goal`](https://api.sb/$.d.ts?nouns=Goal) | 4 |
| [`Hypothesis`](https://api.sb/$.d.ts?nouns=Hypothesis) | 7 |
| [`KeyResult`](https://api.sb/$.d.ts?nouns=KeyResult) | 3 |
| [`Limit`](https://api.sb/$.d.ts?nouns=Limit) | 3 |
| [`Meter`](https://api.sb/$.d.ts?nouns=Meter) | 3 |
| [`Metric`](https://api.sb/$.d.ts?nouns=Metric) | 3 |
| [`Noun`](https://api.sb/$.d.ts?nouns=Noun) | 5 |
| [`Offer`](https://api.sb/$.d.ts?nouns=Offer) | 8 |
| [`Page`](https://api.sb/$.d.ts?nouns=Page) | 3 |
| [`Play`](https://api.sb/$.d.ts?nouns=Play) | 3 |
| [`Problem`](https://api.sb/$.d.ts?nouns=Problem) | 3 |
| [`Process`](https://api.sb/$.d.ts?nouns=Process) | 3 |
| [`Product`](https://api.sb/$.d.ts?nouns=Product) | 7 |
| [`Project`](https://api.sb/$.d.ts?nouns=Project) | 3 |
| [`Prospect`](https://api.sb/$.d.ts?nouns=Prospect) | 6 |
| [`RateCard`](https://api.sb/$.d.ts?nouns=RateCard) | 4 |
| [`Referral`](https://api.sb/$.d.ts?nouns=Referral) | 3 |
| [`ReferralReward`](https://api.sb/$.d.ts?nouns=ReferralReward) | 3 |
| [`Relationship`](https://api.sb/$.d.ts?nouns=Relationship) | 3 |
| [`Role`](https://api.sb/$.d.ts?nouns=Role) | 5 |
| [`Service`](https://api.sb/$.d.ts?nouns=Service) | 3 |
| [`Session`](https://api.sb/$.d.ts?nouns=Session) | 3 |
| [`Software`](https://api.sb/$.d.ts?nouns=Software) | 7 |
| [`Startup`](https://api.sb/$.d.ts?nouns=Startup) | 10 |
| [`Story`](https://api.sb/$.d.ts?nouns=Story) | 3 |
| [`Strategy`](https://api.sb/$.d.ts?nouns=Strategy) | 3 |
| [`Task`](https://api.sb/$.d.ts?nouns=Task) | 17 |
| [`Template`](https://api.sb/$.d.ts?nouns=Template) | 3 |
| [`Theme`](https://api.sb/$.d.ts?nouns=Theme) | 3 |
| [`Variant`](https://api.sb/$.d.ts?nouns=Variant) | 3 |
| [`Verb`](https://api.sb/$.d.ts?nouns=Verb) | 3 |
| [`Version`](https://api.sb/$.d.ts?nouns=Version) | 3 |
| [`Webhook`](https://api.sb/$.d.ts?nouns=Webhook) | 3 |
| [`Workflow`](https://api.sb/$.d.ts?nouns=Workflow) | 3 |

44 Nouns · 190 Verbs.

### Why the constraints

Made by hand, screens multiply: 2 audiences, 4 views plus 1 per Verb with input, 3 states, 2 themes and 2 widths.

| Noun | Screens built by hand |
| --- | --- |
| Unit | 96 |
| Lease | 120 |

The platform carries that count while you stay inside the constraints.

### What fits

Made for products that are records and the actions people take on them. A Lease and its renewal fit; so does a Unit and its building. Inside that shape, an agent writes the definition and one person checks it.

## Blog and changelog

Every Startup gets a blog at `/blog`, under its own theme, and a changelog. Each changelog entry is a dated Version of the definition, so the day Lease gained rent is an entry with no post to write.

Read next: [Get paid with Stripe](https://saas.dev/docs/get-paid.md)