4. Guarding transitions

So far every transition fires the moment its event arrives. Real machines need conditions: a payment moves the order forward only if it was authorized; a declined payment goes the other way. That condition is a guard — a predicate on the where clause.

where and the event’s data

An event carries data (Event(kind="PaymentResult", data={...})), and a guard tests it. Both edges below react to the same event, PaymentResult, but branch on its status:

event PlaceOrder {}
event PaymentResult { status: string  amount: int }
event Deliver {}

guard authorized = status == "authorized"

machine order {
  initial Cart
  state Cart {}
  state AwaitingPayment {}
  state Paid {}
  final Delivered success {}
  final Cancelled cancelled {}

  from Cart to AwaitingPayment on PlaceOrder
  from AwaitingPayment to Paid       on PaymentResult where authorized and amount <= 100
  from AwaitingPayment to Cancelled  on PaymentResult where status == "declined"
  from Paid to Delivered on Deliver
}
        stateDiagram-v2
    [*] --> Cart
    Cart --> AwaitingPayment : PlaceOrder
    AwaitingPayment --> Paid : PaymentResult<br/>[authorized and amount ≤ 100]
    AwaitingPayment --> Cancelled : PaymentResult<br/>[status == declined]
    Paid --> Delivered : Deliver
    Delivered --> [*]
    Cancelled --> [*]
    

Two things are new here.

Named guards. guard authorized = status == "authorized" names a reusable predicate. A guard reference is a predicate atom: you can use it alone (where authorized) or compose it with more conditions (where authorized and amount <= 100). Comparisons use ==  !=  <  <=  >  >=  in, and you combine them with and / or / not.

event declarations. event PaymentResult { status: string  amount: int } declares the event’s shape. It is optional, but once you declare events, validation can check that your guards reference fields that actually exist.

Run the branches

from harel import definition_from_dsl, DurableRunner, DictStore, Event

SOURCE = """
event PlaceOrder {}
event PaymentResult { status: string  amount: int }
event Deliver {}

guard authorized = status == "authorized"

machine order {
  initial Cart
  state Cart {}
  state AwaitingPayment {}
  state Paid {}
  final Delivered success {}
  final Cancelled cancelled {}

  from Cart to AwaitingPayment on PlaceOrder
  from AwaitingPayment to Paid       on PaymentResult where authorized and amount <= 100
  from AwaitingPayment to Cancelled  on PaymentResult where status == "declined"
  from Paid to Delivered on Deliver
}
"""

defn = definition_from_dsl(SOURCE, "order")
runner = DurableRunner(DictStore(), {defn.id: defn})


def after_payment(data):
    exe = runner.create(defn.id)
    exe = runner.process(exe.id, Event(kind="PlaceOrder"))
    exe = runner.process(exe.id, Event(kind="PaymentResult", data=data))
    return exe.active_path


print("authorized, 50  ->", after_payment({"status": "authorized", "amount": 50}))
print("declined        ->", after_payment({"status": "declined"}))
print("authorized, 500 ->", after_payment({"status": "authorized", "amount": 500}))
authorized, 50  -> Paid
declined        -> Cancelled
authorized, 500 -> AwaitingPayment

The third case is the important one. The payment was authorized, but amount <= 100 is false, so the guard fails and no edge fires — the order stays in AwaitingPayment. The event was delivered, found no enabled transition, and was simply not acted on.

Note

A predicate on a field the event does not carry evaluates to false, not an error. So where amount <= 100 against an event with no amount fails the guard — the transition won’t fire. Keep that in mind when an event “mysteriously” doesn’t transition: a typo’d or missing field silently fails the guard rather than blowing up.

Binding guards from the outside

Just as actions can be bound programmatically with actions=, guards can be supplied with guards={…} — a map from guard name to a predicate dict (e.g. {"status__eq": "authorized"} or {"all": [...]}). It overrides the in-DSL guard declaration, giving tests and callers the same swap-the-implementation seam they have for actions.

A guard answers yes/no. When you need to route more than two ways on a computed value, you reach for a selectorthe next step.