Guards & Business Rules
Source:
tests/schemas/03-guards.orb
Guards are conditions that must be true for a transition to fire. They act as the gatekeepers of your business rules — written once, enforced everywhere, for both the UI and the API.
What is a Guard?
A guard is a condition on a transition, written with ? before the expression. If it evaluates to false, the transition is blocked:
state active {
WITHDRAW -> active
when (>= @entity.balance @payload.amount)
;; effects...
}
The user can only withdraw if balance >= amount. If not, the transition is silently blocked (the UI can surface a disabled state or error message).
S-Expression Syntax
Guards use Lisp-style prefix notation — the operator comes first, then its arguments:
(operator arg1 arg2 ...)
Arguments can be:
- Literals:
100,"active",true - Bindings:
@entity.field,@payload.field,@state,@now - Nested expressions:
(+ @entity.count 1)
Comparison Operators
| Operator | Meaning | Example |
|---|---|---|
= | Equal | (= @entity.status "active") |
!= | Not equal | (!= @entity.role "guest") |
> | Greater than | (> @entity.score 0) |
>= | Greater or equal | (>= @entity.balance @payload.amount) |
< | Less than | (< @entity.attempts 3) |
<= | Less or equal | (<= @entity.age 65) |
Boolean Operators
Combine conditions with and, or, not:
when (and (>= @entity.balance @payload.amount) (= @entity.isVerified true))
when (or (= @entity.role "admin") (= @entity.role "manager"))
when (not (= @entity.status "frozen"))
Full Example: Account Manager
This is the complete AccountManager from 03-guards.orb. It demonstrates:
- A guard using
andto combine two conditions - Using
@payload.amountto check against user input - Simple state transitions (freeze/unfreeze) without guards
orbital AccountManager {
entity Account [persistent: accounts] {
id : string!
balance : number
isVerified : boolean
}
trait AccountActions -> Account [interaction] {
initial: active
state active {
INIT -> active
(fetch Account)
(render-ui main { type: "entity-table", entity: "Account", fields: ["balance", "isVerified"], columns: ["balance", "isVerified"], itemActions: [{ event: "WITHDRAW", label: "Withdraw" }, { event: "FREEZE", label: "Freeze" }] })
WITHDRAW -> active
when (and (>= @entity.balance @payload.amount) (= @entity.isVerified true))
(set @entity.balance (- @entity.balance @payload.amount))
FREEZE -> frozen
}
state frozen {
UNFREEZE -> active
}
}
page "/accounts" -> AccountActions
}
Reading the WITHDRAW guard:
when (and
(>= @entity.balance @payload.amount) ;; Account has enough funds
(= @entity.isVerified true)) ;; Account is verified
Both conditions must be true. If the account is unverified, or the balance is too low, the withdrawal is blocked.
Guards with Computed Values
Guards can use arithmetic operators — the result of a nested expression is used as an argument:
;; Only allow if balance after withdrawal stays above minimum
when (>= (- @entity.balance @payload.amount) 100)
;; Only allow if item count is within limit
when (< (+ @entity.itemCount 1) 50)
Common Guard Patterns
Role-based access
state listing {
DELETE -> listing
when (= @user.role "admin")
(persist delete Task @entity.id)
}
Ownership check
state Pending {
START -> InProgress
when (= @entity.assigneeId @user.id)
(persist update Task @entity)
}
Field validation
SUBMIT -> processing
when (and (>= ?score 0) (<= ?score 100))
Status precondition
APPROVE -> approved
when (= @entity.status "review")
Guards vs. Effects
Guards run before the transition. Effects run after. Never use effects to enforce business rules — that's what guards are for.
;; Wrong: using effects to simulate a guard
WITHDRAW -> active
(if (< @entity.balance 0) (notify "Insufficient funds" error))
;; Correct: guard blocks the transition entirely
WITHDRAW -> active
when (>= @entity.balance @payload.amount)
(set @entity.balance (- @entity.balance @payload.amount))
Next Steps
- Cross-Orbital Communication — guards can reference data from other orbitals
- UI Patterns & render-ui — rendering feedback when guards block actions
- Building a Full App — guards in a real multi-orbital application