Task tracker — the reference example, annotated
This is the real reference program that ships with the toolchain (examples/task-tracker/task-tracker.ml in the source repository, and the starter program every scaffolded app begins with). It is quoted here in full, section by section, with the reasoning behind each part — and the wireframes the compiler generates from it.
Entities
entity Workspace {
id: string req
view: enum(board, projects)
}
entity Project {
id: string req
name: string
}
entity Task {
id: string req
project: ref(Project)
title: string
status: enum(todo, doing, done)
assignee: string
created_at: string
actor_id: string
}
Workspace is the singleton flow entity — one enum member per screen. Project is deliberately create-only (no set-actions later), which is what makes its uniqueness constraint legal. Task is the mutable workhorse: a ref to its project, an enum status, and two runtime-injected bookkeeping fields (created_at, actor_id) stored on the row.
Constraints
constraint TitleRequired {
on: Task
validate: NOT(self.title == '')
message: 'title is required'
}
constraint AssigneeRequiredWhenDoing {
on: Task
validate: NOT(self.status == 'doing' AND self.assignee == '')
message: 'assignee is required when a task is doing'
}
constraint ProjectExists {
on: Task
validate: count(Project as p, p == self.project) == 1
message: 'project does not exist'
}
constraint ProjectNameRequired {
on: Project
validate: NOT(self.name == '')
message: 'project name is required'
}
constraint UniqueProjectName {
on: Project
validate: count(Project as p, p.name == name) == 0
message: 'a project with this name already exists'
}
Every pattern from the style guide appears once:
TitleRequired/AssigneeRequiredWhenDoing—self.-boundNOT(invalid state)forms, valid on a mutable entity. The second one is a conditional requirement: it guards both creating adoingtask without an assignee and moving a task todoingwithout one — same constraint, all paths.ProjectExists— referential integrity via a whole-reference count join (p == self.project, neverp.id == ...).UniqueProjectName— input-bound uniqueness with the== 0boundary, allowed becauseProjectis create-only.
Actions
action CreateProject(id: string, name: string, actor_id: string) {
on: Project
create(Project, { id: id, name: name })
}
action CreateTask(id: string, project: ref(Project), title: string, status: enum(todo, doing, done), assignee: string, created_at: string, actor_id: string) {
on: Task
create(Task, { id: id, project: project, title: title, status: status, assignee: assignee, created_at: created_at, actor_id: actor_id })
}
action MoveTask(id: string, status: enum(todo, doing, done), actor_id: string) {
on: Task
set(status, status)
}
action RenameTask(id: string, title: string, actor_id: string) {
on: Task
set(title, title)
}
action OpenProjects(workspace: ref(Workspace), actor_id: string) {
on: Workspace
set(view, 'projects')
}
action OpenBoard(workspace: ref(Workspace), actor_id: string) {
on: Workspace
set(view, 'board')
}
Pure mutation only — not an if in sight. Note the id semantics: on CreateTask it’s a fresh id (auto-filled, hidden); on MoveTask/RenameTask it selects the row, so those forms render with a task picker. OpenProjects/OpenBoard are the navigation pattern: a set on the flow entity.
Queries and screens
query TaskBoard(workspace: ref(Workspace)) -> list<Task> {
from Task
}
query ProjectList(workspace: ref(Workspace)) -> list<Project> {
from Project
}
query TaskDetail(task: ref(Task)) -> Task {
from Task
}
screen Board {
when workspace.view == 'board'
title "Task board"
hint "Create tasks, move them between statuses, and rename them."
board TaskBoard
primary { label "Add task" action CreateTask }
button { label "Move task" action MoveTask }
button { label "Rename task" action RenameTask }
button { label "Projects" action OpenProjects }
}
screen Projects {
when workspace.view == 'projects'
title "Projects"
body "Every task belongs to a project."
board ProjectList
primary { label "Add project" action CreateProject }
button { label "Back to board" action OpenBoard }
}
Each screen is: an activation condition, copy, one collection, and the actions it dispatches. Every action with user inputs becomes a form; the dispatching control’s label becomes the submit label (“Add task”, “Move task”, …). All copy is static string literals — the compiler never invents text.
Tests
test CreateTaskSucceeds {
setup: [
create(Workspace, { id: "w1", view: "board" }),
create(Project, { id: "p1", name: "General" })
]
when: CreateTask on Task { id: "t1", project: "p1", title: "Ship the compiler", status: "todo", assignee: "" }
then: [
entity_count(Task) == 1,
entity_has(Task.1, title == "Ship the compiler")
]
}
test TitleRequiredRejectsEmpty {
setup: [
create(Workspace, { id: "w1", view: "board" }),
create(Project, { id: "p1", name: "General" })
]
when: CreateTask on Task { id: "t1", project: "p1", title: "", status: "todo", assignee: "" }
then: [
error_raised('title is required'),
entity_count(Task) == 0
]
}
test AssigneeRequiredWhenDoingRejects {
setup: [
create(Workspace, { id: "w1", view: "board" }),
create(Project, { id: "p1", name: "General" })
]
when: CreateTask on Task { id: "t1", project: "p1", title: "Ship the compiler", status: "doing", assignee: "" }
then: [
error_raised('assignee is required when a task is doing'),
entity_count(Task) == 0
]
}
test ProjectExistsRejectsUnknown {
setup: [
create(Workspace, { id: "w1", view: "board" })
]
when: CreateTask on Task { id: "t1", project: "ghost", title: "Ship the compiler", status: "todo", assignee: "" }
then: [
error_raised('project does not exist'),
entity_count(Task) == 0
]
}
test MoveTaskSucceeds {
setup: [
create(Workspace, { id: "w1", view: "board" }),
create(Project, { id: "p1", name: "General" }),
create(Task, { id: "t1", project: "p1", title: "Ship the compiler", status: "todo", assignee: "casey", created_at: "2026-01-01T00:00:00Z", actor_id: "a1" })
]
when: MoveTask on Task { id: "t1", status: "doing" }
then: [
entity_has(Task.1, status == "doing"),
entity_count(Task) == 1
]
}
test MoveTaskRejectsDoingWithoutAssignee {
setup: [
create(Workspace, { id: "w1", view: "board" }),
create(Project, { id: "p1", name: "General" }),
create(Task, { id: "t1", project: "p1", title: "Ship the compiler", status: "todo", assignee: "", created_at: "2026-01-01T00:00:00Z", actor_id: "a1" })
]
when: MoveTask on Task { id: "t1", status: "doing" }
then: [
error_raised('assignee is required when a task is doing'),
entity_has(Task.1, status == "todo")
]
}
test RenameTaskSucceeds {
setup: [
create(Workspace, { id: "w1", view: "board" }),
create(Project, { id: "p1", name: "General" }),
create(Task, { id: "t1", project: "p1", title: "Old name", status: "todo", assignee: "", created_at: "2026-01-01T00:00:00Z", actor_id: "a1" })
]
when: RenameTask on Task { id: "t1", title: "New name" }
then: [
entity_has(Task.1, title == "New name")
]
}
test CreateProjectSucceeds {
setup: [
create(Workspace, { id: "w1", view: "board" })
]
when: CreateProject on Project { id: "p1", name: "Platform" }
then: [
entity_count(Project) == 1,
entity_has(Project.1, name == "Platform")
]
}
test ProjectNameRequiredRejectsEmpty {
setup: [
create(Workspace, { id: "w1", view: "board" })
]
when: CreateProject on Project { id: "p1", name: "" }
then: [
error_raised('project name is required'),
entity_count(Project) == 0
]
}
test UniqueProjectNameRejectsDuplicate {
setup: [
create(Workspace, { id: "w1", view: "board" }),
create(Project, { id: "p0", name: "General" })
]
when: CreateProject on Project { id: "p1", name: "General" }
then: [
error_raised('a project with this name already exists'),
entity_count(Project) == 1
]
}
test OpenProjectsSwitchesView {
setup: [
create(Workspace, { id: "w1", view: "board" })
]
when: OpenProjects on Workspace { workspace: "w1" }
then: [
entity_has(Workspace.1, view == "projects")
]
}
The triads to notice:
AssigneeRequiredWhenDoingis tested on both write paths: rejected at create (AssigneeRequiredWhenDoingRejects) and rejected atset(MoveTaskRejectsDoingWithoutAssignee, whose state-unchanged assertion isentity_has(Task.1, status == "todo")— the move didn’t happen). That second test is the lifecycle coverage detector D7 made concrete.- Rejected creates assert
entity_count(...) == 0; the rejected duplicate asserts== 1(the original row survives). - Tests pass runtime-injected values (
created_at,actor_id) as plain literals insetup:— that’s all determinism asks.
The wireframes
The compiler emits these from the screens above (generated/ui/wire/board.mlui and projects.mlui, mobile sections shown; each file also contains an 80-column desktop rendering):
┌──────────────────────────────────────┐
│ Task board │
├──────────────────────────────────────┤
│ Create tasks, move them between │
│ statuses, and rename them. │
│ │
│ ┌─────┬─────┬─────┬─────┬─────┬────┐ │
│ │ Pro │ Tit │ Sta │ Ass │ Cre │ Ac │ │
│ ├─────┼─────┼─────┼─────┼─────┼────┤ │
│ │ ░░░ │ ░░░ │ ░░░ │ ░░░ │ ░░░ │ ░░ │ │
│ │ ░░░ │ ░░░ │ ░░░ │ ░░░ │ ░░░ │ ░░ │ │
│ └─────┴─────┴─────┴─────┴─────┴────┘ │
│ │
│ ┌ Create task ─────────────────────┐ │
│ │ Project: ________ │ │
│ │ Title: ________ │ │
│ │ Status: ________ │ │
│ │ Assignee: ________ │ │
│ │ [ Add task ] │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌ Move task ───────────────────────┐ │
│ │ Id: ________ │ │
│ │ Status: ________ │ │
│ │ [ Move task ] │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌ Rename task ─────────────────────┐ │
│ │ Id: ________ │ │
│ │ Title: ________ │ │
│ │ [ Rename task ] │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌ Open projects ───────────────────┐ │
│ │ Workspace: ________ │ │
│ │ [ Projects ] │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ Projects │
├──────────────────────────────────────┤
│ Every task belongs to a project. │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Name │ │
│ ├──────────────────────────────────┤ │
│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌ Create project ──────────────────┐ │
│ │ Name: ________ │ │
│ │ [ Add project ] │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌ Open board ──────────────────────┐ │
│ │ Workspace: ________ │ │
│ │ [ Back to board ] │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────┘
Read across: the Create task form has four visible fields — id, created_at, and actor_id from the action signature are absent, exactly as the input classification promises. The Move task form’s Id field is the row selector. These text files are what a reviewer diffs when a PR changes a screen (recipe).
This program is the scaffold’s starter — npm create minlang-app my-app gives you exactly this app running, ready to be rewritten into yours.