Skip to content

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:

flow
on> :checkout from @customer
  ? cart is not empty
    order moves to #awaiting_payment
    emit :payment_request to @payment

If cart is not empty is false, the indented actions are skipped.

Multiple Guards (AND)

Multiple ? lines are combined with AND logic - all must pass:

flow
on> :checkout from @customer
  ? cart is not empty
  ? user is logged in
  ? payment method is valid
    order moves to #awaiting_payment

All three conditions must be true for the action to execute.

OR Guards

Use ?? for OR conditions - any one passing is enough:

flow
on> :access_admin from @user
  ? user is admin
  ?? user is super_admin
  ?? user has admin_override
    show admin panel

Any one of these conditions passing will allow access.

Combined Logic

You can combine AND and OR:

flow
on> :checkout from @customer
  ? cart is not empty        // AND
  ? total is greater than 0  // AND
  ?? user has override       // OR
    process checkout

Reads as: (cart is not empty AND total > 0) OR user has override

Complex Guard Example

flow
on> :process_refund from @customer
  ? 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 @customer

Default Case (Otherwise)

When no guards pass, use otherwise for the default case:

flow
on> :categorize_order from @system
  ? $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 ?:

flow
? $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

flow
? order is in #pending
  process order

? order is not in #cancelled
  allow modifications

Context Guards

flow
? $total is greater than 100
  apply free shipping

? $items is not empty
  show cart summary

? $retry_count is less than 3
  retry operation

Actor Guards

flow
? @customer is logged in
  show account details

? @customer has verified email
  allow password reset

Comparison Guards

flow
? $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 100

Collection Guards

flow
? $items contains "Gift Card"
  apply gift card rules

? $items is empty
  show empty message

? $tags is not empty
  process tags

Time-Based Guards

flow
? created more than 24 hours ago
  mark as expired

? updated within last 1 hour
  skip refresh

? days since purchase < 7
  allow full refund

Nested Guards

Guards can be nested for complex logic:

flow
on> :process_order from @system
  ? 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 @customer

Guard Execution Order

Guards are evaluated top-to-bottom. The first matching guard block executes:

flow
on> :apply_discount from @customer
  ? $total > 500
    $discount becomes 50      // First check

  ? $total > 100
    $discount becomes 20      // Second check

  otherwise
    $discount becomes 0       // Default

If $total is 600:

  • First guard passes → $discount becomes 50
  • Subsequent guards are not checked

Guards Without Actions

Sometimes you just want to validate:

flow
on> :checkout from @customer
  ? cart is empty
    emit :error to @customer
    // Implicit: don't continue

  // If we get here, cart is not empty
  order moves to #processing

Guards in Given Blocks

Guards in given: blocks set up test conditions:

flow
scenario: premium checkout

  given:
    @customer is logged in
    @customer is premium member
    cart has items
    $total: number is 500

  on> :checkout from @customer
    ? @customer is premium member
      $discount becomes 10%
      $total decreases by $discount

  expect:
    = $total equals 450

Natural Language Flexibility

EventFlow accepts natural variations:

flow
// All equivalent
? cart is not empty
? cart is non-empty
? cart has items

// All equivalent
? $total is greater than 100
? $total > 100
? $total exceeds 100

Use what reads most naturally for your context.

Best Practices

Be Explicit

flow
// Good - clear conditions
? order is in #pending
? payment is authorized
  process order

// Avoid - unclear
? everything is ok
  do stuff

Use Otherwise for Completeness

flow
// 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 pending
flow
// Related conditions together
? payment is authorized
? payment amount matches order total
? payment currency is valid
  complete payment

Released under the MIT License.