← Back to Documentation

State Transitions

ARO handles state machines elegantly: define states as OpenAPI enums, transition with the Accept action, and let the runtime validate everything.

States Are Just Enums

No special state machine syntax. No transition tables. Define your states in OpenAPI, use Accept to transition, and get automatic validation with clear error messages.

State Machine Example

Consider an order lifecycle. Orders flow through these states:

draft placed paid shipped delivered

Defining States in OpenAPI

States are defined as string enums in your openapi.yaml:

# openapi.yaml
openapi: 3.0.3
info:
  title: Order Management
  version: 1.0.0

components:
  schemas:
    OrderStatus:
      type: string
      enum:
        - draft
        - placed
        - paid
        - shipped
        - delivered
        - cancelled

    Order:
      type: object
      properties:
        id:
          type: string
        status:
          $ref: '#/components/schemas/OrderStatus'
        customerId:
          type: string
      required:
        - id
        - status

The Accept Action

Use Accept to validate and apply state transitions. The syntax clearly shows the expected transition:

(* Transition from draft to placed *)
Accept the <transition: draft_to_placed> on <order: status>.

(* Transition from placed to paid *)
Accept the <transition: placed_to_paid> on <order: status>.

(* Transition from paid to shipped *)
Accept the <transition: paid_to_shipped> on <order: status>.

The syntax uses _to_ as the separator because to is a reserved preposition in ARO.

Complete Example

Here's how state transitions work in practice with order management:

(placeOrder: Order Management) {
    Extract the <order-id> from the <pathParameters: id>.
    Retrieve the <order> from the <order-repository>.

    (* Accept state transition from draft to placed *)
    Accept the <transition: draft_to_placed> on <order: status>.

    Store the <order> into the <order-repository>.
    Emit to <Send Order Confirmation> with <order>.
    Return an <OK: status> with <order>.
}

(payOrder: Order Management) {
    Extract the <order-id> from the <pathParameters: id>.
    Retrieve the <order> from the <order-repository>.

    (* Must be placed to accept payment *)
    Accept the <transition: placed_to_paid> on <order: status>.

    Store the <order> into the <order-repository>.
    Return an <OK: status> with <order>.
}

(shipOrder: Order Management) {
    Extract the <order-id> from the <pathParameters: id>.
    Retrieve the <order> from the <order-repository>.

    (* Must be paid to ship *)
    Accept the <transition: paid_to_shipped> on <order: status>.

    Store the <order> into the <order-repository>.
    Emit to <Send Shipping Notification> with <order>.
    Return an <OK: status> with <order>.
}

Error Handling

If the current state doesn't match the expected state, the runtime throws a clear error:

Cannot accept state draft->placed on order: status. Current state is "paid".

This follows ARO's "Code Is The Error Message" philosophy. The error tells you exactly what was expected and what was found.

Why This Approach?

Simplicity

No new keywords. No special constructs. Just OpenAPI enums + Accept. Use standard control flow (if, match) for complex logic.

Clarity

The transition is explicit in the code. Reading Accept the <transition: draft_to_placed> on <order: status> tells you exactly what's happening.

Safety

The runtime validates:

State Observers

State observers react to transitions after they occur. Use them for audit logging, notifications, analytics, and side effects.

Observer Pattern

A feature set becomes a state observer when its business activity ends with StateObserver. Add an optional transition filter in angle brackets to observe only specific transitions:

(* Observe ALL status field transitions *)
(Audit Status: status StateObserver) {
    Extract the <fromState> from the <transition: fromState>.
    Extract the <toState> from the <transition: toState>.
    Extract the <orderId> from the <transition: entityId>.
    Log the <audit> for the <console>
        with "Order ${orderId}: ${fromState} -> ${toState}".
    Return an <OK: status> for the <audit>.
}

(* Observe ONLY draft->placed transition *)
(Notify Placed: status StateObserver<draft_to_placed>) {
    Extract the <orderId> from the <transition: entityId>.
    Log the <notification> for the <console>
        with "Order ${orderId} has been placed!".
    Return an <OK: status> for the <notification>.
}

Transition Data

Observers access transition information via the <transition> object:

Use Cases

Observer Semantics

Observers execute after the transition succeeds. Multiple observers can react to the same transition. Observers run in isolation - failures don't affect the Accept action or other observers.

Learn More

See the full state objects specification in ARO-0013: State Objects.