Guards & Conditions
Guards are conditions that control whether actions execute. They're marked with the ? prefix.
Basic Guards
A guard is a condition that must be true for the indented actions to run:
on :checkout from @customer (api)
? cart is not empty
order moves to #awaiting_payment
emit :payment_request to @paymentIf cart is not empty is false, the indented actions are skipped.
Multiple Guards (AND)
Multiple ? lines are combined with AND logic - all must pass:
on :checkout from @customer (api)
? cart is not empty
? user is logged in
? payment method is valid
order moves to #awaiting_paymentAll three conditions must be true for the action to execute.
OR Guards
Use ?? for OR conditions - any one passing is enough:
on :access_admin from @user (api)
? user is admin
?? user is super_admin
?? user has admin_override
show admin panelAny one of these conditions passing will allow access.
Combined Logic
You can combine AND and OR:
on :checkout from @customer (api)
? cart is not empty // AND
? total is greater than 0 // AND
?? user has override // OR
process checkoutReads as: (cart is not empty AND total > 0) OR user has override
Complex Guard Example
on :process_refund from @customer (api)
? order exists
? order belongs to @customer
? days since purchase < 7
full refund
$refund_amount becomes $order_total
order moves to #fully_refunded
? days since purchase < 30
partial refund (50%)
$refund_amount becomes $order_total * 0.5
order moves to #partially_refunded
? days since purchase < 90
?? customer is premium
partial refund (25%)
$refund_amount becomes $order_total * 0.25
order moves to #partially_refunded
otherwise
refund denied
emit :refund_rejected to @customerDefault Case (Otherwise)
When no guards pass, use otherwise for the default case:
on :categorize_order from @system (api)
? $total > 1000
$priority becomes "high"
emit :alert to @vip_team
? $total > 100
$priority becomes "medium"
otherwise
$priority becomes "low"You can also use an empty ?:
? $total > 1000
$priority becomes "high"
? $total > 100
$priority becomes "medium"
?
$priority becomes "low"Both are equivalent - otherwise is more readable, empty ? is more concise.
Guard Patterns
State Guards
? order is in #pending
process order
? order is not in #cancelled
allow modificationsContext Guards
? $total is greater than 100
apply free shipping
? $items is not empty
show cart summary
? $retry_count is less than 3
retry operationActor Guards
? @customer is logged in
show account details
? @customer has verified email
allow password resetComparison Guards
? $total equals 0
? $total is 0
? $total is equal to 0
show empty cart
? $count is greater than 10
? $count is less than 5
? $count is at least 3
? $count is at most 100Collection Guards
? $items contains "Gift Card"
apply gift card rules
? $items is empty
show empty message
? $tags is not empty
process tagsTime-Based Guards
? created more than 24 hours ago
mark as expired
? updated within last 1 hour
skip refresh
? days since purchase < 7
allow full refundNested Guards
Guards can be nested for complex logic:
on :process_order from @system (api)
? order is valid
? payment is authorized
? inventory is available
order moves to #confirmed
emit :prepare_shipment to @warehouse
otherwise
order moves to #backordered
emit :backorder_notice to @customer
otherwise
order moves to #payment_failed
emit :payment_retry to @customer
otherwise
order moves to #invalid
emit :validation_error to @customerGuard Execution Order
Guards are evaluated top-to-bottom. The first matching guard block executes:
on :apply_discount from @customer (api)
? $total > 500
$discount becomes 50 // First check
? $total > 100
$discount becomes 20 // Second check
otherwise
$discount becomes 0 // DefaultIf $total is 600:
- First guard passes →
$discount becomes 50 - Subsequent guards are not checked
Guards Without Actions
Sometimes you just want to validate:
on :checkout from @customer (api)
? cart is empty
emit :error to @customer
// Implicit: don't continue
// If we get here, cart is not empty
order moves to #processingInline Error Responses
For API endpoints, guards can include inline error responses. When the guard fails, the error response is returned immediately:
on :checkout from @customer (api)
? cart is not empty | 400 cart cannot be empty
? user is logged in | 401 please log in first
? payment method is valid | 400 invalid payment method
order moves to #awaiting_payment
reply 201 with:
| id | $order_id |The syntax is:
? <condition> | <http_status> <error_message>When a guard with an inline error fails:
- HTTP Status: Set in the response header (e.g., 400, 401, 403)
- Response Body:
{"message": "<error_message>"}
Variable Interpolation
Error messages can include context variables:
? $quantity is at most $max_quantity | 400 cannot order more than $max_quantity items
? $total is under $limit | 400 order total $total exceeds limit of $limitWhen to Use Inline vs Full Reply
Use inline errors for simple validation failures:
? cart is not empty | 400 cart cannot be empty
? user is logged in | 401 please log in firstUse full reply blocks when you need richer error responses:
? cart total exceeds $max_limit
reply 400 with:
| error | LIMIT_EXCEEDED |
| message | order exceeds limit |
| limit | $max_limit |
| current | $cart_total |Complete Example
on :checkout from @customer (api)
// Simple validations with inline errors
? cart is not empty | 400 cart cannot be empty
? user is logged in | 401 please log in first
// Complex validation with full reply
? $total exceeds $spending_limit
reply 403 with:
| error | SPENDING_LIMIT_EXCEEDED |
| message | you have exceeded your spending limit |
| limit | $spending_limit |
| total | $total |
// Happy path
$order_id becomes uuid()
order moves to #awaiting_payment
reply 201 with:
| id | $order_id |
| status | current_state |Guards in Given Blocks
Guards in given: blocks set up test conditions:
scenario: premium checkout
given:
@customer is logged in
@customer is premium member
cart has items
$total: number is 500
on :checkout from @customer (api)
? @customer is premium member
$discount becomes 10%
$total decreases by $discount
expect:
= $total equals 450Natural Language Flexibility
EventFlow accepts natural variations:
// All equivalent
? cart is not empty
? cart is non-empty
? cart has items
// All equivalent
? $total is greater than 100
? $total > 100
? $total exceeds 100Use what reads most naturally for your context.
Best Practices
Be Explicit
// Good - clear conditions
? order is in #pending
? payment is authorized
process order
// Avoid - unclear
? everything is ok
do stuffUse Otherwise for Completeness
// Good - all cases handled
? status is active
process active
? status is pending
process pending
otherwise
handle unknown status
// Risky - what if status is something else?
? status is active
process active
? status is pending
process pendingGroup Related Guards
// Related conditions together
? payment is authorized
? payment amount matches order total
? payment currency is valid
complete payment