Documentation

Entities

The data an orbital owns, where it is stored, and who may read or change it.

An entity is the data an orbital owns. Every orbital declares exactly one primary entity; a second or third entity block becomes an auxiliary entity of the same orbital. Traits bind to an entity with ->, and read and write it through @entity.

Declaring fields

lolo
1entity Invoice [persistent: invoices] { 2 id : string! 3 client : string! 4 amount : money = 0 5 status : "draft" | "sent" | "paid" = draft 6 dueOn : date 7 paidAmount : number = derived (if (= @entity.status "paid") @entity.amount 0) 8}
  • ! marks a required field, ? a nullable one.
  • = value sets a default. Defaults are expressions; = now stamps the creation time.
  • "a" | "b" | "c" is a closed vocabulary: setting any other value is a validation error.
  • derived (…) is computed from other fields whenever they change; it is never stored.

The types are the usual primitives (string, number, int, boolean, date, timestamp) plus refinements that render and validate better: money, email, url, phone, file, duration, icon.

Where the data lives

The tag in brackets says where rows are kept:

TagMeaning
[runtime]In memory, for the life of the page. View state, counters, wizards.
[persistent: invoices]Stored rows in the named collection, read with fetch and written with persist.
[persistent: invoices, local]Stored rows that live in the viewer's browser (IndexedDB). The app needs no server.
[runtime, shared]One frame shared by every trait bound to the entity, within one viewer's session.
[persistent: people, identity]This entity is the shape of the signed-in viewer, @user.

A trait's @entity is its entity's defaults, then the stored row it is bound to (if any), then its own frame, the view state that (set @entity.x v) writes. Two traits on the same non-shared entity never see each other's writes; mark the entity shared when they should.

Seeding rows

A browser-stored entity can start with rows. instances writes them out, and each row is type-checked against the fields:

State machine
NoteList.lolololo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 trait NoteList -> Note [interaction, collection] { state loading { INIT -> loading (fetch Note { emit: { failure: NotesFailed, success: NotesLoaded } }) (render-ui main <Skeleton rows={2} variant={list} />) NotesLoaded -> ready (emit MOVED { event: NotesLoaded }) (render-ui main <DataList emptyDescription={"Add one with the form."} emptyIcon={file-text} emptyTitle={"No notes yet"} entity={?data} fields={[{ label: Title name: title variant: h4 }, { label: Note name: body variant: body }]} />) NotesFailed -> ready (emit MOVED { event: NotesFailed }) (render-ui main <Alert message={?error} variant={error} />) } state ready { RELOAD -> loading (emit MOVED { event: RELOAD }) (fetch Note { emit: { failure: NotesFailed, success: NotesLoaded } }) } emits { NotesLoaded { data : [Note] } NotesFailed { error : string } MOVED -> external { event : string! } @description "Each move names the event it took, so the diagram beside the demo can follow it." } listens { NoteForm.NOTE_SAVED -> RELOAD } }
INIT
NotesLoaded
NotesFailed
RELOAD
1loading
nowready
NoteForm.lolololo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 trait NoteForm -> Note [interaction, instance] { state editing { INIT -> editing (render-ui main <FormSection entity={{}} fields={(title body)} mode={create} submitEvent={SAVE} />) SAVE -> editing (emit MOVED { event: SAVE }) (persist create Note ?data) (emit NOTE_SAVED) (render-ui main <FormSection entity={{}} fields={(title body)} mode={create} submitEvent={SAVE} />) } emits { NOTE_SAVED @description "A note was stored." MOVED -> external { event : string! } @description "Each move names the event it took, so the diagram beside the demo can follow it." } listens { SAVE { data : Note! } } }
INIT
SAVE
nowediting
Running live

Read about the closed circuit

Note:Event, guard, transition, effects, UI.

Run orb validate

Note:Zero errors and zero warnings is the bar.

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-notes.lolololo
1app std-orb-docex-notes "1.0.0" 2"std-orb-docex-notes — a stored list with a form: the list loads on mount, the form saves a new note, and a listens route reloads the list." 3orbital NotesOrbital { 4 entity Note [persistent: docex_notes, local] { 5 id : string! 6 title : string! 7 body : string = "" 8 instances [ 9 { id: "N-1", title: "Read about the closed circuit", body: "Event, guard, transition, effects, UI." }, 10 { id: "N-2", title: "Run orb validate", body: "Zero errors and zero warnings is the bar." } 11 ] 12 } 13 14 trait NoteList -> Note [interaction, collection] { 15 initial: loading 16 17 state loading { 18 INIT -> loading 19 (fetch Note { emit: { success: NotesLoaded, failure: NotesFailed } }) 20 (render-ui main { type: skeleton, variant: list, rows: 2 }) 21 NotesLoaded -> ready 22 (emit MOVED { event: "NotesLoaded" }) 23 (render-ui main { type: data-list, entity: ?data, fields: [{ name: "title", label: "Title", variant: h4 }, { name: "body", label: "Note", variant: body }], emptyTitle: "No notes yet", emptyDescription: "Add one with the form.", emptyIcon: "file-text" }) 24 NotesFailed -> ready 25 (emit MOVED { event: "NotesFailed" }) 26 (render-ui main { type: alert, variant: error, message: ?error }) 27 } 28 29 state ready { 30 RELOAD -> loading 31 (emit MOVED { event: "RELOAD" }) 32 (fetch Note { emit: { success: NotesLoaded, failure: NotesFailed } }) 33 } 34 35 emits { 36 NotesLoaded { 37 data : [Note] 38 } 39 NotesFailed { 40 error : string 41 } 42 MOVED -> external { 43 event : string! 44 } 45 @description "Each move names the event it took, so the diagram beside the demo can follow it." 46 } 47 48 listens { 49 NoteForm.NOTE_SAVED -> RELOAD 50 } 51 } 52 53 trait NoteForm -> Note [interaction, instance] { 54 initial: editing 55 56 state editing { 57 INIT -> editing 58 (render-ui main { type: form-section, fields: ["title", "body"], submitEvent: SAVE, mode: create, entity: {} }) 59 SAVE -> editing 60 (emit MOVED { event: "SAVE" }) 61 (persist create Note ?data) 62 (emit NOTE_SAVED) 63 (render-ui main { type: form-section, fields: ["title", "body"], submitEvent: SAVE, mode: create, entity: {} }) 64 } 65 66 emits { 67 NOTE_SAVED 68 @description "A note was stored." 69 MOVED -> external { 70 event : string! 71 } 72 @description "Each move names the event it took, so the diagram beside the demo can follow it." 73 } 74 75 listens { 76 SAVE { 77 data : @entity! 78 } 79 } 80 } 81 82 page "/notes" -> NoteList, NoteForm 83}

What happens when SAVE fires:

editingSAVE
→
editing
Effects (3)What runs, in order, when the transition fires.
(persist "create" "Note" @payload.data)
"create""Note"@payload.datapersist
(emit "NOTE_SAVED")
"NOTE_SAVED"emit

How the traits talk to each other, in order:

NoteListNoteFormNoteListNoteFormUserSAVEpersist create NoteNOTE_SAVED → RELOADfetch NoteNotesLoaded → readyrender-ui mainUser

Rows are seeded once, into an empty store, and never over rows the viewer already has. A program that declares locales [en, ar, sl] can seed each language separately with instances ar [ … ] and instances sl [ … ]; see localization.

Reading and writing

lolo
1(fetch Note { emit: { success: NotesLoaded, failure: NotesFailed } }) 2(fetch Note { id: ?id }) 3(fetch Note { filter: ["=", (object/get @entity status), "open"], limit: 20 }) 4(persist create Note ?data) 5(persist update Note (object/merge ?row { done: true })) 6(persist delete Note ?id)

fetch loads rows and reports through the events in emit; without them nothing observes the load. Both result events are declared in the trait's emits. Inside a filter, (object/get @entity field) is the row being tested; a bare @entity.field is the trait's own frame.

Relations

Typing a field as another entity creates a relation: customerId : Customer (one) or attendees : [Person] (many). (fetch Order { include: ["customerId"] }) hydrates it. T.f names the type of a field of another entity, so tagId : Tag.id! is a string that is known to hold a tag's id.

Who may read and write

Access policies sit at the top of the entity body and are enforced once, at the data layer, for every trait bound to the entity:

lolo
1entity Session [persistent: sessions] { 2 @read (or ["=", @user.role, "manager"] ["=", (object/get @entity trainerId), @user.id]) 3 @update ["=", (object/get @entity trainerId), @user.id] 4 @delete none "sessions are archived, never deleted" 5 trainerId : Person 6 title : string! 7}

@user is the viewer; @entity is the row in question. No policy means everyone may perform that operation; none "reason" records a deliberate omission. Policies protect data. What a screen shows or hides is written explicitly in the program, with guards.

Orb

A programming language of composable state machines.

© 2026 Almadar. Ljubljana, Slovenia.
Not reported here