Documentation

Guards

Make a transition conditional on the entity, the payload or the viewer, and keep every rule in one place.

A guard is an expression after when. If it is false, the event is refused: no state change, no effects. Guards are where business rules live.

On the entity

lolo
1 2 APPROVE -> approved when (<= @entity.amount 500) (set @entity.note "Approved")

On the payload

lolo
1 2 PAY -> paid when (> ?amount 0) (set @entity.total ?amount)

?amount is a field of the incoming event. It must be declared, either in the emitting trait's emits or in a local listens entry, so the validator can check its type.

Here the checkout refuses a payment of zero, and an unpaid order has no way to ship:

State machine

Checkout

on Order · states: 3 · transitions: 4
emitsPAYemitsMOVED
INIT
PAY
(> @payload.amount 0)
SHIP
NEW_ORDER
nowpending
paid
shipped
Running live

Unpaid

std-orb-sample-checkout.lolololo
1app std-orb-sample-checkout "1.0.0" 2"std-orb-sample-checkout — the checkout program shown on orb.almadar.io's home page, running: an order that cannot ship before it is paid. The page shows this file as code beside the running trait, and each move is reported (MOVED) so its state machine can follow along." 3orbital Orders { 4 entity Order [runtime] { 5 id : string! 6 total : number 7 } 8 9 trait Checkout -> Order [interaction, instance] { 10 initial: pending 11 12 state pending { 13 INIT -> pending 14 (render-ui main { type: stack, gap: sm, children: [ 15 { type: typography, content: "Unpaid", variant: h4 }, 16 { type: button, label: "Pay 40", action: PAY, actionPayload: { amount: 40 } } 17 ] }) 18 PAY -> paid when (> ?amount 0) 19 (set @entity.total ?amount) 20 (emit MOVED { event: "PAY" }) 21 (render-ui main { type: stack, gap: sm, children: [ 22 { type: typography, content: "Paid 40", variant: h4 }, 23 { type: button, label: "Ship", action: SHIP } 24 ] }) 25 } 26 27 state paid { 28 SHIP -> shipped 29 (emit MOVED { event: "SHIP" }) 30 (render-ui main { type: stack, gap: sm, children: [ 31 { type: typography, content: "Shipped", variant: h4 }, 32 { type: button, label: "New order", action: NEW_ORDER } 33 ] }) 34 } 35 36 state shipped { 37 NEW_ORDER -> pending 38 (emit MOVED { event: "NEW_ORDER" }) 39 (render-ui main { type: stack, gap: sm, children: [ 40 { type: typography, content: "Unpaid", variant: h4 }, 41 { type: button, label: "Pay 40", action: PAY, actionPayload: { amount: 40 } } 42 ] }) 43 } 44 45 emits { 46 PAY { 47 amount : number! 48 } 49 MOVED -> external { 50 event : string! 51 } 52 @description "Each move names the event it took, so a diagram beside the order can follow it." 53 } 54 } 55 56 page "/checkout" -> Checkout 57}

What happens when PAY fires:

pendingPAY
→
paid
GuardThe transition only fires when this check is true.
(> @payload.amount 0)
@payload.amount0>
Effects (2)What runs, in order, when the transition fires.
(set @entity.total @payload.amount)
@entity.total@payload.amountset

On the viewer

lolo
1 2 APPROVE -> approved when (= @user.role "manager") (set @entity.approvedBy @user.id)

@user is the signed-in viewer. Declare its shape once with an [identity] entity, and the validator rejects a role that is not in the vocabulary:

lolo
1 2 3 4 ▼entity Person [persistent: people, identity] { id : string! @key role : "employee" | "manager" = employee }

Combining conditions

(and …), (or …) and (not …) combine conditions. When an event should do different things depending on a value, write one transition per case with exclusive guards, or let match write them for you:

lolo
1 2 3 4 ▼REVIEW -> reviewed match @entity.priority { "high" -> (notify warning "Escalated") _ -> (notify info "Queued") }

Guards versus hiding

A guard refuses an event; it does not hide the button that sends it. If a button should not appear, say so in the screen: a row action's when, or an (if …) around the node. Nothing is hidden implicitly, so the program reads exactly as it behaves.

Orb

The language where the rule is the program.

© 2026 Almadar. Ljubljana, Slovenia.
Not reported here