Documentation

Build a task manager

Stored rows, a form that adds one, row actions that complete and delete, and a list that reloads after every write.

This tutorial builds a task manager with two traits on one page: a list and a form. The tasks are stored in the browser, so the app needs no server.

1. A stored entity

lolo
1entity Task [persistent: tasks, local] { 2 id : string! 3 title : string! 4 done : boolean = false 5 instances [ 6 { id: "T-1", title: "Sketch the states", done: true }, 7 { id: "T-2", title: "Write the transitions", done: false } 8 ] 9}

local keeps the rows in the viewer's browser. instances seeds them the first time the store is created.

2. Loading the list

The list starts in loading, fetches on INIT, and moves to ready when the rows arrive. The fetch reports through two events, which the trait declares in emits:

lolo
1state loading { 2 INIT -> loading 3 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } }) 4 (render-ui main { type: skeleton, variant: list, rows: 3 }) 5 TasksLoaded -> ready 6 (render-ui main { type: data-list, entity: ?data, fields: [{ name: "title", label: "Task", variant: h4 }] }) 7 TasksFailed -> ready 8 (render-ui main { type: alert, variant: error, message: ?error }) 9}

3. Row actions

itemActions put buttons on each row. A row action sends its event with { id, row }, and when decides per row whether the button shows:

lolo
1 2 3 4 ▼itemActions: [ { label: "Complete", event: COMPLETE, variant: ghost, when: (fn row (not (object/get @row done false))) }, { label: "Delete", event: DELETE, variant: danger } ]
lolo
1COMPLETE -> loading 2 (persist update Task (object/merge ?row { done: true })) 3 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } }) 4DELETE -> loading 5 (persist delete Task ?id) 6 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } })

4. The form

A second trait shows a form-section. Submitting sends SAVE with the form's values as ?data; the trait stores the row and emits TASK_ADDED:

lolo
1 2 3 SAVE -> editing (persist create Task ?data) (emit TASK_ADDED)

The list listens and reloads:

lolo
1 2 3 ▼listens { TaskForm.TASK_ADDED -> RELOAD }

5. The finished app

State machine

TaskList

on Task · states: 2 · transitions: 6
listensTASK_ADDEDemitsTasksLoadedemitsTasksFailedemitsMOVED
INIT
TasksLoaded
TasksFailed
COMPLETE
DELETE
RELOAD
1loading
nowready

TaskForm

on Task · states: 1 · transitions: 2
emitsTASK_ADDEDemitsMOVED
INIT
SAVE
nowediting
Running live

Sketch the states

Yes

Write the transitions

No

Run orb validate

No

The program above as state machines, one per trait: each box is a state, each arrow an event that moves it, and the icons on an arrow are the effects that event runs. A ◇ on an arrow is a guard; hover it to read the check. How to read AVL

std-orb-docex-tasks.lolololo
1app std-orb-docex-tasks "1.0.0" 2"std-orb-docex-tasks — a task manager: stored tasks, a form that adds one, row actions that complete or delete, and a list that reloads after every write." 3orbital TasksOrbital { 4 entity Task [persistent: docex_tasks, local] { 5 id : string! 6 title : string! 7 done : boolean = false 8 instances [ 9 { id: "T-1", title: "Sketch the states", done: true }, 10 { id: "T-2", title: "Write the transitions", done: false }, 11 { id: "T-3", title: "Run orb validate", done: false } 12 ] 13 } 14 15 trait TaskList -> Task [interaction, collection] { 16 initial: loading 17 18 state loading { 19 INIT -> loading 20 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } }) 21 (render-ui main { type: skeleton, variant: list, rows: 3 }) 22 TasksLoaded -> ready 23 (emit MOVED { event: "TasksLoaded" }) 24 (render-ui main { type: data-list, entity: ?data, fields: [{ name: "title", label: "Task", variant: h4 }, { name: "done", label: "Done", variant: badge, labels: { "true": "Done", "false": "Open" } }], itemActions: [{ label: "Complete", event: COMPLETE, variant: ghost, when: (fn row (not (object/get @row done false))) }, { label: "Delete", event: DELETE, variant: danger }], emptyTitle: "Nothing to do", emptyDescription: "Add a task with the form.", emptyIcon: "check-circle" }) 25 TasksFailed -> ready 26 (emit MOVED { event: "TasksFailed" }) 27 (render-ui main { type: alert, variant: error, message: ?error }) 28 } 29 30 state ready { 31 COMPLETE -> loading 32 (emit MOVED { event: "COMPLETE" }) 33 (persist update Task (object/merge ?row { done: true })) 34 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } }) 35 DELETE -> loading 36 (emit MOVED { event: "DELETE" }) 37 (persist delete Task ?id) 38 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } }) 39 RELOAD -> loading 40 (emit MOVED { event: "RELOAD" }) 41 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } }) 42 } 43 44 emits { 45 TasksLoaded { 46 data : [Task] 47 } 48 TasksFailed { 49 error : string 50 } 51 MOVED -> external { 52 event : string! 53 } 54 @description "Each move names the event it took, so the diagram beside the demo can follow it." 55 } 56 57 listens { 58 TaskForm.TASK_ADDED -> RELOAD 59 COMPLETE { 60 id : Task.id! 61 row : Task! 62 } 63 DELETE { 64 id : Task.id! 65 } 66 } 67 } 68 69 trait TaskForm -> Task [interaction, instance] { 70 initial: editing 71 72 state editing { 73 INIT -> editing 74 (render-ui main { type: form-section, fields: ["title"], submitEvent: SAVE, mode: create, entity: {} }) 75 SAVE -> editing 76 (emit MOVED { event: "SAVE" }) 77 (persist create Task ?data) 78 (emit TASK_ADDED) 79 (render-ui main { type: form-section, fields: ["title"], submitEvent: SAVE, mode: create, entity: {} }) 80 } 81 82 emits { 83 TASK_ADDED 84 @description "A task was stored." 85 MOVED -> external { 86 event : string! 87 } 88 @description "Each move names the event it took, so the diagram beside the demo can follow it." 89 } 90 91 listens { 92 SAVE { 93 data : @entity! 94 } 95 } 96 } 97 98 page "/tasks" -> TaskList, TaskForm 99}

What happens when COMPLETE fires:

readyCOMPLETE
→
loading
Effects (2)What runs, in order, when the transition fires.
(persist "update" "Task" (object/merge @payload.row { done: true }))
"update""Task"@payload.rowtruedone{ }object/mergepersist
(fetch "Task" { emit: { failure: "TasksFailed", success: "TasksLoaded" } })
"Task""TasksFailed"failure"TasksLoaded"success{ }emit{ }fetch

How the traits talk to each other, in order:

TaskListTaskFormTaskListTaskFormUserSAVEpersist create TaskTASK_ADDED → RELOADfetch TaskTasksLoaded → readyrender-ui mainUser

Add a task, complete one, delete one. Reload the page: the tasks are still there, because they are stored in your browser.

Orb

The language where the rule is the program.

© 2026 Almadar. Ljubljana, Slovenia.
Not reported here