Cookbook
Recipes for the situations every MinLang app runs into, plus three complete, annotated programs:
| Program | What it shows |
|---|---|
| Guestbook | The smallest useful app: one screen, create-only entity, uniqueness |
| Task tracker | The reference example, annotated section by section, with its wireframes |
| Sales CRM | A pipeline board: flow entity, create-only + mutable entities, lifecycle rules |
Both the guestbook and the CRM listings are validator-clean against current bundle rules — the raw .ml files live next to these pages (guestbook.ml, crm-pipeline.ml).
Add a screen + navigation action
Problem: your app needs a second view and a way to get there.
Screens activate off a singleton flow entity. Adding a screen is three edits — extend the enum, add a navigation action, declare the screen:
entity Workspace {
id: string req
view: enum(board, projects, settings) # 1. new member
}
action OpenSettings(workspace: ref(Workspace), actor_id: string) {
on: Workspace
set(view, 'settings') # 2. navigation = a set-action
}
screen Settings {
when workspace.view == 'settings' # 3. the new screen
title "Settings"
button { label "Back to board" action OpenBoard }
}
Then put a button { label "Settings" action OpenSettings } on the screens that should link to it, and add a test asserting the view switch (entity_has(Workspace.1, view == "settings")). Navigation has no special machinery — it is ordinary state your screens select on.
Enforce uniqueness correctly
Problem: “no two companies with the same name” — and the validator rejects your first three attempts.
The rule (the D3/D4/D7 interplay, explained in the style guide): uniqueness counts compare the input variable with the proactive == 0 boundary, and they may only live on create-only entities.
constraint UniqueCompanyName {
on: Company # Company has no set-actions
validate: count(Company as c, c.name == name) == 0
message: 'a company with this name already exists'
}
If the entity must also be mutable, split the unique identity into its own create-only entity and reference it. And complete the triad — success test, duplicate-rejection test with the byte-exact message, and entity_count(Company) == 1 after the rejection.
Per-row-ish flows without per-row delete
Problem: users want to “remove” a task, but delete Entity clears the whole set and there is no per-row delete.
Model removal as a status change. Give the entity a terminal member and a normal set-action:
entity Task {
id: string req
title: string
status: enum(todo, doing, done, archived)
}
action ArchiveTask(id: string, actor_id: string) {
on: Task
set(status, 'archived')
}
The id input on a set-action renders as a row selector, so “archive task” becomes a form where the user picks the row — that is the idiomatic per-row interaction. Rows stay in the data (queries return all rows of the source entity). A real bulk reset is what delete is for: a ClearBoard action that wipes and reseeds.
Theme an app from Figma
Problem: the design team has a Figma file; the app ships with default tokens.
- Export design tokens from Figma — native Variables export, the Tokens Studio plugin, or any W3C design-tokens JSON.
-
Run the importer against your app:
ml1 design tokens export.json --apply my-app.mlIt maps token names (
brand→primary,bg→surface,fg→text, …) onto the theme vocabulary and splices atheme Default { ... }block into the file. Unmapped tokens print as notes, never errors. The run is idempotent. -
Compile:
make compileThe compile gate checks the vocabulary and WCAG AA contrast (≥ 4.5:1 for every text-on-surface pair) — a palette that fails reports the measured ratio, and nothing ships until it passes.
Full mapping and alias table: UI & UX.
Swap a widget with a skin
Problem: the default table doesn’t match the design system; you want your own component without touching generated code.
- Create
app/skins/table.tsx— a server component taking exactlyWidgetProps<"table">. Rows come fromdeps.viewModels[node.query]; all copy comes from the schema node; styling uses token classes (bg-surface-raised,text-ink,border-edge). -
Register it in
seed.ts:import { bindRegistryOverrides } from "@minlang/runtime-web"; import { TableSkin } from "./skins/table"; bindRegistryOverrides({ table: TableSkin }); -
Lint and test:
make lint-skins # rejects hardcoded copy, color literals, fetch/Date.now, adapter imports make test
If you start from a Figma Make export, the adaptation is mostly deletion: remove its data fetching, its local data state, its hardcoded strings, and its literal colors. Details and a worked example: UI & UX.
Read a PR (wire diff + previews)
Problem: a PR claims “adds an archive button to the board” — how do you review it without checking out and running the app?
Read it top-down, in this order:
- The
.mldiff — the only source change. New action? New constraint with its message? Tests present (success + byte-exact failure + state-unchanged assertions)? -
The wireframe diff (
generated/ui/wire/<screen>.mlui) — the UI change as plain text. A new[ Archive task ]form box appearing inboard.mluiat both mobile and desktop widths is the feature, visually:+ │ ┌ Archive task ────────────────────┐ │ + │ │ Id: ________ │ │ + │ │ [ Archive task ] │ │ + │ └──────────────────────────────────┘ │ - The rest of
generated/— you don’t review it line by line; CI’s--checkmode proves it matches the.ml, and the generated Vitest suite proves the rules. - Screen previews — for pixel-level changes (themes, skins), download the
screen-previewsCI artifact: rendered screenshots of every screen at mobile and desktop viewports.
Wireframes are projections rendered from the same schema the app uses — they cannot disagree with the real UI.