Documentation

Traits

State machines that react to events, check guards, run effects and render the screen.

A trait is a state machine bound to an entity. It lists states; each state lists the events it accepts, where each one leads, and what happens on the way.

States and transitions

lolo
1trait Approval -> Expense [interaction, instance] { 2 initial: draft 3 4 state draft { 5 SUBMIT -> submitted when (> @entity.amount 0) 6 (render-ui main { type: typography, content: "Submitted", variant: h4 }) 7 } 8 9 state submitted { 10 APPROVE -> approved when (<= @entity.amount 500) 11 (set @entity.note "Approved") 12 REJECT -> rejected 13 (set @entity.note "Rejected") 14 } 15}
  • EVENT -> target is a transition. Effects follow on their own lines and run in order.
  • when (…) is a guard: if it is false, the event is refused and nothing changes.
  • The first state is initial unless initial: names another.
  • Each transition stands alone. Two transitions that differ only in their source state are written twice; nothing is shared implicitly.
  • A state with no way out is an error unless the program means it to end there.

The header's bracket holds a category (interaction, lifecycle, integration, …) and instance (one state machine per entity row, the default) or collection (one for the whole entity).

Here is the full approval flow running. Try submitting a claim over 500 EUR and approving it; the guard refuses:

State machine

Approval

on Expense · states: 4 · transitions: 7
emitsMOVED
INIT
RAISE
SUBMIT
(> @entity.amount 0)
APPROVE
(<= @entity.amount 500)
REJECT
NEW_CLAIM
REVISE
nowdraft
submitted
approved
rejected
Running live

Draft claim: EUR

The same trait as a state machine: each box is a state, each arrow an event that moves it, and the icons on an arrow are the effects that event runs. Hover a symbol to see what it does. How to read AVL

std-orb-docex-expense.lolololo
1app std-orb-docex-expense "1.0.0" 2"std-orb-docex-expense — an approval flow: four states, a guard on the amount, and a rule the shape of the program enforces." 3orbital ExpenseOrbital { 4 entity Expense [runtime] { 5 id : string! 6 amount : number = 120 7 note : string = "" 8 } 9 10 trait Approval -> Expense [interaction, instance] { 11 initial: draft 12 13 state draft { 14 INIT -> draft 15 (render-ui main { type: stack, direction: vertical, gap: sm, children: [ 16 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 }, 17 { type: stack, direction: horizontal, gap: sm, children: [ 18 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost }, 19 { type: button, label: "Submit", action: SUBMIT, variant: primary } 20 ] } 21 ] }) 22 RAISE -> draft 23 (emit MOVED { event: "RAISE" }) 24 (set @entity.amount (+ @entity.amount 50)) 25 SUBMIT -> submitted when (> @entity.amount 0) 26 (emit MOVED { event: "SUBMIT" }) 27 (render-ui main { type: stack, direction: vertical, gap: sm, children: [ 28 { type: typography, content: (str/concat "Submitted: " @entity.amount " EUR"), variant: h4 }, 29 { type: typography, content: "Claims over 500 EUR cannot be approved here.", variant: small, color: muted }, 30 { type: stack, direction: horizontal, gap: sm, children: [ 31 { type: button, label: "Approve", action: APPROVE, variant: primary }, 32 { type: button, label: "Reject", action: REJECT, variant: secondary } 33 ] } 34 ] }) 35 } 36 37 state submitted { 38 APPROVE -> approved when (<= @entity.amount 500) 39 (emit MOVED { event: "APPROVE" }) 40 (set @entity.note "Approved") 41 (render-ui main { type: stack, direction: vertical, gap: sm, children: [ 42 { type: badge, label: "Approved", variant: success }, 43 { type: button, label: "New claim", action: NEW_CLAIM, variant: ghost } 44 ] }) 45 REJECT -> rejected 46 (emit MOVED { event: "REJECT" }) 47 (set @entity.note "Rejected") 48 (render-ui main { type: stack, direction: vertical, gap: sm, children: [ 49 { type: badge, label: "Rejected", variant: error }, 50 { type: button, label: "Revise", action: REVISE, variant: primary } 51 ] }) 52 } 53 54 state approved { 55 NEW_CLAIM -> draft 56 (emit MOVED { event: "NEW_CLAIM" }) 57 (set @entity.amount 120) 58 (render-ui main { type: stack, direction: vertical, gap: sm, children: [ 59 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 }, 60 { type: stack, direction: horizontal, gap: sm, children: [ 61 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost }, 62 { type: button, label: "Submit", action: SUBMIT, variant: primary } 63 ] } 64 ] }) 65 } 66 67 state rejected { 68 REVISE -> draft 69 (emit MOVED { event: "REVISE" }) 70 (render-ui main { type: stack, direction: vertical, gap: sm, children: [ 71 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 }, 72 { type: stack, direction: horizontal, gap: sm, children: [ 73 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost }, 74 { type: button, label: "Submit", action: SUBMIT, variant: primary } 75 ] } 76 ] }) 77 } 78 79 emits { 80 MOVED -> external { 81 event : string! 82 } 83 @description "Each move names the event it took, so the diagram beside the demo can follow it." 84 } 85 } 86 87 page "/expenses" -> Approval 88}

What happens when APPROVE fires:

submittedAPPROVE
→
approved
GuardThe transition only fires when this check is true.
(<= @entity.amount 500)
@entity.amount500<=
Effects (2)What runs, in order, when the transition fires.
(set @entity.note "Approved")
@entity.note"Approved"set

Effects

EffectWhat it does
(set @entity.f v)Assign a field. The only assignment form.
(fetch E { … }) / (persist create|update|delete E data)Read and write stored rows.
(emit EVENT { … })Broadcast a declared event.
(render-ui slot { … })Draw a pattern tree into a slot (main, modal, drawer, toast, …); null clears it.
(navigate "/path") / (navigate-back)Move to another page.
(notify info "Saved")Show a toast.
(call-service provider action { … })Call an integration (LLM, email, payments, …).

Operators compute values: (+ a b), (str/concat …), (array/map xs (fn x …)). They are all prefix; there is no infix. The operator reference lists every one.

Bindings

SigilMeaning
@entity.fieldA field of the bound entity
?fieldA field of the incoming event's payload
@config.keyA value from the trait's config
@user.fieldThe signed-in viewer
now / stateThis dispatch's timestamp / the current state

Emits and listens

A trait declares what it broadcasts, and another trait declares what it reacts to. Here the dial emits TARGET_CHANGED, and the status listens for it and maps it to its own SHOW event:

State machine

Dial

on Thermostat · states: 1 · transitions: 3
emitsTARGET_CHANGEDemitsMOVED
INIT
UP
(< @entity.target 28)
DOWN
(> @entity.target 16)
nowidle

Status

on Thermostat · states: 1 · transitions: 2
listensTARGET_CHANGEDemitsMOVED
INIT
SHOW
nowwatching
Running live

21 °C

Set a temperature between 16 and 28 °C.

std-orb-docex-thermostat.lolololo
1app std-orb-docex-thermostat "1.0.0" 2"std-orb-docex-thermostat — two traits in one orbital: one guards and emits, the other listens and reacts." 3orbital ThermostatOrbital { 4 entity Thermostat [runtime, shared] { 5 id : string! 6 target : number = 21 7 } 8 9 trait Dial -> Thermostat [interaction, instance] { 10 initial: idle 11 12 state idle { 13 INIT -> idle 14 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [ 15 { type: button, label: "Colder", action: DOWN, variant: secondary }, 16 { type: typography, content: (str/concat @entity.target " °C"), variant: h2 }, 17 { type: button, label: "Warmer", action: UP, variant: secondary } 18 ] }) 19 UP -> idle when (< @entity.target 28) 20 (emit MOVED { event: "UP" }) 21 (set @entity.target (+ @entity.target 1)) 22 (emit TARGET_CHANGED { target: @entity.target }) 23 DOWN -> idle when (> @entity.target 16) 24 (emit MOVED { event: "DOWN" }) 25 (set @entity.target (- @entity.target 1)) 26 (emit TARGET_CHANGED { target: @entity.target }) 27 } 28 29 emits { 30 TARGET_CHANGED { 31 target : number! 32 } 33 @description "The target temperature moved." 34 MOVED -> external { 35 event : string! 36 } 37 @description "Each move names the event it took, so the diagram beside the demo can follow it." 38 } 39 } 40 41 trait Status -> Thermostat [interaction, instance] { 42 initial: watching 43 44 state watching { 45 INIT -> watching 46 (render-ui main { type: typography, content: "Set a temperature between 16 and 28 °C.", variant: body, color: muted }) 47 SHOW -> watching 48 (emit MOVED { event: "SHOW" }) 49 (render-ui main { type: badge, label: (if (> ?target 23) "Heating up" (if (< ?target 19) "Cooling down" "Comfortable")), variant: (if (> ?target 23) warning (if (< ?target 19) info success)) }) 50 } 51 52 emits { 53 MOVED -> external { 54 event : string! 55 } 56 @description "Each move names the event it took, so the diagram beside the demo can follow it." 57 } 58 59 listens { 60 Dial.TARGET_CHANGED -> SHOW 61 SHOW { 62 target : number! 63 } 64 } 65 } 66 67 page "/thermostat" -> Dial, Status 68}

What happens when UP fires:

idleUP
→
idle
GuardThe transition only fires when this check is true.
(< @entity.target 28)
@entity.target28<
Effects (2)What runs, in order, when the transition fires.
(set @entity.target (+ @entity.target 1))
@entity.target@entity.target1+set
(emit "TARGET_CHANGED" { target: @entity.target })
"TARGET_CHANGED"@entity.targettarget{ }emit
lolo
1emits { 2 TARGET_CHANGED { 3 target : number! 4 } 5} 6 7listens { 8 Dial.TARGET_CHANGED -> SHOW 9 SHOW { 10 target : number! 11 } 12}

internal events (the default) stay within the orbital; external events cross to other orbitals and must declare their payload. Trait.EVENT resolves within the listener's own orbital; across orbitals, write Orbital.Trait.EVENT. Cross-orbital events shows both.

Ticks

A tick is a periodic effect. It runs only while its when holds, so a tick bound to a state stops the moment the trait leaves it:

State machine

Timing

on Stopwatch · states: 2 · transitions: 4
emitsMOVED
INIT
START
RESET
STOP
nowstopped
running
Running live

s

std-orb-docex-stopwatch.lolololo
1app std-orb-docex-stopwatch "1.0.0" 2"std-orb-docex-stopwatch — a tick: a periodic effect that runs only while the trait is in the running state." 3orbital StopwatchOrbital { 4 entity Stopwatch [runtime] { 5 id : string! 6 seconds : number = 0 7 } 8 9 trait Timing -> Stopwatch [interaction, instance] { 10 initial: stopped 11 12 state stopped { 13 INIT -> stopped 14 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [ 15 { type: typography, content: (str/concat @entity.seconds " s"), variant: h2 }, 16 { type: button, label: "Start", action: START, variant: primary }, 17 { type: button, label: "Reset", action: RESET, variant: ghost } 18 ] }) 19 START -> running 20 (emit MOVED { event: "START" }) 21 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [ 22 { type: typography, content: (str/concat @entity.seconds " s"), variant: h2 }, 23 { type: button, label: "Stop", action: STOP, variant: secondary } 24 ] }) 25 RESET -> stopped 26 (emit MOVED { event: "RESET" }) 27 (set @entity.seconds 0) 28 } 29 30 state running { 31 STOP -> stopped 32 (emit MOVED { event: "STOP" }) 33 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [ 34 { type: typography, content: (str/concat @entity.seconds " s"), variant: h2 }, 35 { type: button, label: "Start", action: START, variant: primary }, 36 { type: button, label: "Reset", action: RESET, variant: ghost } 37 ] }) 38 } 39 40 emits { 41 MOVED -> external { 42 event : string! 43 } 44 @description "Each move names the event it took, so the diagram beside the demo can follow it." 45 } 46 47 ticks { 48 second every 1s when (= state "running") 49 (set @entity.seconds (+ @entity.seconds 1)) 50 } 51 } 52 53 page "/stopwatch" -> Timing 54}

What happens when START fires:

stoppedSTART
→
running
Effects (1)What runs, in order, when the transition fires.
lolo
1 2 3 4 ▼ticks { second every 1s when (= state "running") (set @entity.seconds (+ @entity.seconds 1)) }

Intervals are milliseconds, a duration (5s, 1m, 1h, 1d) or a cron string. A tick on a stored entity runs on the server while a page holds the trait; tag it [background] to run with nobody watching.

Config

config { pageSize : int = 10 @label "Items per page" } declares a knob the trait reads as @config.pageSize. Knobs are what a call site may change when it imports the trait; see composition.

Orb

The language where the rule is the program.

© 2026 Almadar. Ljubljana, Slovenia.
Not reported here