Decider
Syntax
decider <Name> { commands: <Command1>, <Command2>, ... events: <Event1>, <Event2>, ... state: <State1> | <State2>(<field>: <Type>) | ... initial: <StateName> terminal: <State1>, <State2>, ...
decide(<Command>, <State>) -> [<Event>] decide(<Command>, <State>) -> already|forbidden|impossible "<message>" decide(<Command>, <State>) { ... }
evolve(<FromState>, <Event>) -> <ToState> { ... }}A decider is the central construct. It declares the commands it handles, the events it produces, the states it transitions between, the initial state, and optionally which states are terminal.
Initial State
The initial: declaration is mandatory. It names the state the decider starts in. The grammar enforces exactly one initial state per decider — this is a parse-time guarantee, not a validation check.
Fieldless Initial State
initial: UnregisteredWhen the initial state has no fields, the initial: declaration names the state directly. No bindings are required or permitted.
Initial State with Field Bindings
When the initial state has fields, each field must be bound to a literal value:
state: Config(maxRequests: Int, windowSeconds: Int) | Exhaustedinitial: Config(maxRequests = 100, windowSeconds = 60)Inline form — bindings appear inside parentheses, separated by commas.
state: Config(maxRequests: Int, windowSeconds: Int) | Exhaustedinitial: Config { maxRequests = 100 windowSeconds = 60}Block form — bindings appear in a braced block, one per line.
Both forms are equivalent. The compiler validates:
- All declared fields are bound — no missing bindings
- No extra bindings target undeclared fields
- No field is bound more than once
- Each literal matches the declared field type (
Int,Float,String,Boolean, or a branded alias of a primitive)
Value type fields (non-primitive composite types) are not supported in initial state bindings. See ADR-021 for the design decision and the deferred value-type-factories feature for the planned extension.
Decide Clauses
Short Form
decide(OpenCart, Empty) -> [CartOpened]Block Form
decide(AddItem, Active) { require items.length < 50 else reject "Cart is full" -> [ItemAdded { cartId, productId, quantity }] ensure items.length == old.items.length + 1}decide returns Result<Events[], RejectionError>. On rejection, no events are produced.
Rejection Clause
decide(Register, Active) -> already "User is already registered"decide(Deposit, New) -> impossible "no account exists"decide(Withdraw, Overdrawn) -> forbidden "account is overdrawn"For (Command, State) pairs that should never produce events, a rejection clause expresses why. Three keywords: already (idempotent), forbidden (business rule), impossible (structural). See Rejection Clauses for details.
Evolve Clauses
evolve(Empty, CartOpened) -> Active { items: [], discountApplied: false }evolve(Active, ItemAdded) -> Active { items: items + item }evolve(Active, CartEmptied) -> EmptyThe evolve function is a pure left-fold. Events are applied sequentially; the entire sequence is atomically committed.
Terminal States
terminal: CheckedOutterminal: Closed, CancelledThe terminal: declaration is optional. It lists one or more states that are absorbing — once the system enters a terminal state, no further commands are accepted. Multiple terminal states are separated by commas. Terminal states reject all commands. They represent completed lifecycles.
Compiler Checks
See Validation for details, error messages, and formal basis.
| Check | What It Verifies |
|---|---|
| Exhaustiveness | Every (Command, State) pair has a decide clause |
| Evolve totality | Every reachable (State, Event) pair has an evolve clause |
| Guard consistency | No contradictory guards making code unreachable |
require false deprecation | Suggests rejection clause syntax |
| Postconditions | Every ensure is derivable from the evolve definition |