Skip to content

Naming Conventions

Consistent naming makes your state machines easier to read, maintain, and debug. This guide covers the recommended naming conventions for every part of an EventMachine definition.

Quick Reference

ElementStylePatternExample
Event classPascalCase{Subject}{PastVerb}EventOrderSubmittedEvent
Event typeSCREAMING_SNAKE_CASE{SUBJECT}_{PAST_VERB}ORDER_SUBMITTED
State (leaf)snake_caseadjective / participleawaiting_payment
State (parent)snake_casenoun (namespace)payment
Action classPascalCase{Verb}{Object}ActionSendNotificationAction
Guard classPascalCase{Prefix}{Condition}GuardIsPaymentValidGuard
Validation GuardPascalCase{Prefix}{Condition}ValidationGuardIsAmountValidValidationGuard
Calculator classPascalCase{Subject}{Noun}CalculatorOrderTotalCalculator
Result classPascalCase{Subject}{Noun}ResultInvoiceSummaryResult
Machine classPascalCase{Domain}MachineOrderWorkflowMachine
Machine IDsnake_case{domain_name}order_workflow
Context classPascalCase{Domain}ContextOrderWorkflowContext
Context keys (array)snake_case{descriptive_name}total_amount
Context properties (typed)camelCase$descriptiveName$totalAmount
Config keyssnake_case{descriptive_name}should_persist
Inline behavior keycamelCase{descriptiveName}{Type}sendEmailAction
Scenario namesnake_case{descriptive_name}express_checkout
Eloquent columnsnake_case{domain}_mreorder_workflow_mre

Class Names vs Internal References

Every element in EventMachine has two identities: its PHP class name and the string key used to reference it in configuration. Here is how they relate:

ElementClass Name (PascalCase)Inline Key (camelCase)Config String
ActionSendEmailActionsendEmailAction'sendEmailAction'
GuardIsPaymentValidGuardisPaymentValidGuard'isPaymentValidGuard'
Validation GuardIsAmountValidValidationGuardisAmountValidValidationGuard'isAmountValidValidationGuard'
CalculatorOrderTotalCalculatororderTotalCalculator'orderTotalCalculator'
ResultInvoiceSummaryResultinvoiceSummaryResult'invoiceSummaryResult'
EventOrderSubmittedEvent'ORDER_SUBMITTED'
MachineOrderWorkflowMachine'order_workflow'
ContextOrderWorkflowContext

The pattern is straightforward:

  • Class name → PascalCase with type suffix (SendEmailAction)
  • Inline key → camelCase version of the class name (sendEmailAction)
  • Event type → SCREAMING_SNAKE_CASE derived from meaning (ORDER_SUBMITTED)
  • Machine ID → snake_case derived from domain name (order_workflow)
php
MachineDefinition::define(
    config: [
        'id'      => 'order_workflow',                    // snake_case
        'initial' => 'idle',
        'states'  => [
            'idle' => [
                'on' => [
                    'ORDER_SUBMITTED' => [                 // SCREAMING_SNAKE_CASE
                        'target'  => 'submitted',
                        'cond'    => 'isPaymentValidGuard', // camelCase + Guard suffix
                        'actions' => ['sendEmailAction'],   // camelCase + Action suffix
                    ],
                ],
            ],
        ],
    ],
    behavior: [
        'actions' => [
            'sendEmailAction' => SendEmailAction::class,   // key matches class name
        ],
        'guards' => [
            'isPaymentValidGuard' => IsPaymentValidGuard::class,
        ],
    ],
);

Why Keep the Suffix in Inline Keys?

When you see 'sendEmail' in a config, it's unclear whether it's an action, a guard, or an event handler. But 'sendEmailAction' immediately tells you its role. The suffix adds clarity, especially in entry, exit, and cond fields where different behavior types can appear:

php
'entry'   => 'initializeOrderAction',   // clearly an action
'cond'    => 'hasItemsGuard',           // clearly a guard

Events

Events represent things that have happened — they are facts, not commands. Always name them in the past tense with an Event suffix.

Event Classes

php
// Class name: {Subject}{PastVerb}Event — PascalCase
class OrderSubmittedEvent extends EventBehavior { ... }
class PaymentReceivedEvent extends EventBehavior { ... }
class DocumentsUploadedEvent extends EventBehavior { ... }
class ItemAddedToCartEvent extends EventBehavior { ... }

The getType() method should return the event type in SCREAMING_SNAKE_CASE, derived from the full class meaning — not abbreviated:

php
class OrderSubmittedEvent extends EventBehavior
{
    public function getType(): string
    {
        return 'ORDER_SUBMITTED';
    }
}

Avoid Abbreviations

Don't abbreviate event types. Use ORDER_SUBMITTED instead of ORD_SUB or OS. Abbreviated types are cryptic and make debugging harder.

Event Types in Configuration

When referencing events as string keys in the machine config, use SCREAMING_SNAKE_CASE:

php
'on' => [
    'ORDER_SUBMITTED'    => 'processing',
    'PAYMENT_RECEIVED'   => 'paid',
    'DOCUMENTS_UPLOADED' => 'under_review',
],

Multi-Word Events

For events with multiple words, ensure every word is clearly separated:

Class NamegetType()
OrderSubmittedEventORDER_SUBMITTED
PaymentMethodUpdatedEventPAYMENT_METHOD_UPDATED
UserEmailVerifiedEventUSER_EMAIL_VERIFIED
BulkImportCompletedEventBULK_IMPORT_COMPLETED

Raised Events

Raised events follow the same naming rules. When an action raises an event internally, use SCREAMING_SNAKE_CASE:

php
class ProcessPaymentAction extends ActionBehavior
{
    public function __invoke(ContextManager $context, EventBehavior $event): void
    {
        // ... process payment ...

        $this->raise('PAYMENT_PROCESSED', ['transaction_id' => $txId]);
    }
}

In machine configuration, raised event targets use the same SCREAMING_SNAKE_CASE:

php
'on' => [
    'PAYMENT_PROCESSED'   => 'paid',
    'VALIDATION_COMPLETED' => 'verified',
],

Why Past Tense?

Events describe facts — something that already occurred. Using past tense (OrderSubmitted) instead of imperative (SubmitOrder) makes the intent clear and avoids confusion with commands or actions.

States

States represent conditions — what the machine currently "is". A state is not an action being performed; it is a description of the system's current situation.

The "is" Test

Every state name must complete the sentence: "The {entity} is ___"

"The order is idle"               ✓
"The order is processing"         ✓
"The order is awaiting_payment"   ✓
"The order is submitted"          ✓

"The order is submit"             ✗ (imperative verb)
"The order is payment"            ✗ (noun — ambiguous)

If the name doesn't read naturally in this sentence, it's probably not a good state name.

Three Grammatical Forms

State names fall into three categories, each serving a specific purpose:

1. Pure Adjectives — Stable / Resting States

For states where the system is at rest, waiting, or in a steady condition:

php
'idle'      => [...],   // "The order is idle"
'active'    => [...],   // "The user is active"
'ready'     => [...],   // "The document is ready"
'pending'   => [...],   // "The request is pending"
'overdue'   => [...],   // "The invoice is overdue"
'suspended' => [...],   // "The account is suspended"

2. Past Participles (-ed / -en) — Completed Action Results

For states entered after an action has completed. This is the most common form and naturally pairs with the event that caused the transition:

php
'submitted' => [...],   // entered via ORDER_SUBMITTED
'paid'      => [...],   // entered via PAYMENT_RECEIVED
'shipped'   => [...],   // entered via SHIPMENT_DISPATCHED
'verified'  => [...],   // entered via IDENTITY_VERIFIED
'frozen'    => [...],   // entered via ACCOUNT_FROZEN
'approved'  => [...],   // entered via REQUEST_APPROVED

3. Present Participles (-ing) — Ongoing Processes

For states where the system is actively doing something and will transition out when the process finishes:

php
'processing'  => [...],   // actively working on something
'validating'  => [...],   // validation in progress
'retrying'    => [...],   // retry attempt in progress
'calculating' => [...],   // computation running
'uploading'   => [...],   // file upload in progress

When to Use -ing vs -ed?

Use -ing when there is genuinely ongoing work happening in that state (e.g., an async job, a timeout, or waiting for an external callback). Use -ed when the state represents a settled condition after something happened.

A good heuristic: if the state will transition out on its own (via @always, a timer, or a callback), it's likely an -ing state. If it waits for an external event, it's likely an -ed or adjective state.

The Event → State Derivation

A powerful pattern is to derive the state name from the event that causes entry into that state. This creates a natural, consistent relationship between events and states:

EventTarget State
ORDER_SUBMITTEDsubmitted
PAYMENT_RECEIVEDpaid
REVIEW_STARTEDunder_review
DOCUMENTS_UPLOADEDawaiting_verification
ORDER_COMPLETEDcompleted
php
'states' => [
    'idle' => [
        'on' => [
            'ORDER_SUBMITTED' => 'submitted',    // event → past participle
        ],
    ],
    'submitted' => [
        'on' => [
            'PAYMENT_RECEIVED' => 'paid',         // event → past participle
        ],
    ],
    'paid' => [
        'on' => [
            'SHIPMENT_DISPATCHED' => 'shipped',   // event → past participle
        ],
    ],
],

This pattern makes your machine self-documenting — you can read the event-to-state mapping and immediately understand the flow.

Multi-Word States

Always use snake_case. Combine participles with nouns or prepositions for clarity:

php
// Participle + noun
'awaiting_payment'     => [...],   // "The order is awaiting payment"
'pending_approval'     => [...],   // "The request is pending approval"

// Adverb + participle
'partially_fulfilled'  => [...],   // "The order is partially fulfilled"
'manually_overridden'  => [...],   // "The setting is manually overridden"

// Preposition + noun
'under_review'         => [...],   // "The application is under review"
'on_hold'              => [...],   // "The shipment is on hold"
'in_transit'           => [...],   // "The package is in transit"

What to Avoid

php
// Imperative verbs — states are conditions, not commands
'process'   // → 'processing' or 'processed'
'submit'    // → 'submitted'
'validate'  // → 'validating' or 'validated'

// Bare nouns — ambiguous, don't describe a condition
'payment'   // → 'awaiting_payment' or 'paid'
'review'    // → 'under_review' or 'reviewed'
'error'     // → 'failed' or 'errored'

// camelCase or PascalCase — use snake_case
'awaitingPayment'  // → 'awaiting_payment'
'UnderReview'      // → 'under_review'

// Generic or numbered names — not descriptive
'state1'    // → use a meaningful name
'step_two'  // → describe the condition, not the sequence
'next'      // → describe what condition the system is in

Why Not Bare Nouns?

payment as a state name is ambiguous — does it mean "awaiting payment", "payment received", or "payment processing"? A participle or adjective removes ambiguity: awaiting_payment, paid, or processing_payment each tell a clear story.

Final States

Final states represent the end of a machine's lifecycle. Use past participles that imply completion or termination:

php
'states' => [
    'completed'  => ['type' => 'final'],   // successful completion
    'cancelled'  => ['type' => 'final'],   // cancelled by user/system
    'rejected'   => ['type' => 'final'],   // rejected by business rule
    'expired'    => ['type' => 'final'],   // timed out
    'archived'   => ['type' => 'final'],   // moved to archive
],

Hierarchical (Compound) States

Parent states serve as namespaces — they group related child states. This is the one exception where nouns are acceptable, because the parent isn't describing a condition; it's categorizing a phase of the workflow:

php
'states' => [
    // Parent: noun (namespace) — OK here
    'payment' => [
        'initial' => 'pending',
        'states'  => [
            // Children: adjectives/participles (conditions)
            'pending'    => [...],   // "The payment is pending"
            'processing' => [...],   // "The payment is processing"
            'settled'    => [...],   // "The payment is settled"
            'refunded'   => [...],   // "The payment is refunded"
        ],
    ],
    'shipping' => [
        'initial' => 'preparing',
        'states'  => [
            'preparing'  => [...],   // "The shipping is preparing"
            'in_transit' => [...],   // "The shipping is in transit"
            'delivered'  => [...],   // "The shipping is delivered"
        ],
    ],
],

Notice how the "is" test adapts naturally: "The {parent} is {child}" — "The payment is pending", "The shipping is in transit".

Parallel States and Regions

Parallel state regions follow the same rule as compound parents — use nouns as region names:

php
'fulfillment' => [
    'type'   => 'parallel',
    'states' => [
        // Region names: nouns (namespaces for parallel tracks)
        'payment'   => [
            'initial' => 'pending',
            'states'  => [
                'pending'  => [...],
                'settled'  => [...],
            ],
        ],
        'shipping'  => [
            'initial' => 'preparing',
            'states'  => [
                'preparing' => [...],
                'shipped'   => [...],
            ],
        ],
        'documents' => [
            'initial' => 'awaiting',
            'states'  => [
                'awaiting'  => [...],
                'collected' => [...],
            ],
        ],
    ],
],

State ID References (#id)

When targeting states across machine boundaries or deep hierarchies, use the # prefix with the machine ID:

php
// Target a state by its absolute ID
'on' => [
    'REVIEW_APPROVED' => '#order_workflow.approved',
],

The ID after # is the machine ID (snake_case), followed by a dot and the state path:

#machine_id.parent_state.child_state

Complete Example

Here is an order workflow that demonstrates all the patterns:

php
'states' => [
    // Adjective — stable resting state
    'idle' => [
        'on' => ['ORDER_SUBMITTED' => 'submitted'],
    ],

    // Past participle — entered after ORDER_SUBMITTED
    'submitted' => [
        'on' => ['PAYMENT_RECEIVED' => 'processing'],
    ],

    // Present participle — ongoing work
    'processing' => [
        'on' => [
            'FULFILLMENT_COMPLETED' => 'shipped',
            'PROCESSING_FAILED'     => 'failed',
        ],
    ],

    // Past participle — entered after FULFILLMENT_COMPLETED
    'shipped' => [
        'on' => ['DELIVERY_CONFIRMED' => 'delivered'],
    ],

    // Past participle — entered after DELIVERY_CONFIRMED
    'delivered' => [
        'on' => ['ORDER_CLOSED' => 'completed'],
    ],

    // Past participle — terminal failure state
    'failed' => [
        'on' => ['RETRY_REQUESTED' => 'processing'],
    ],

    // Past participle — final state
    'completed' => ['type' => 'final'],

    // Past participle — final state
    'cancelled' => ['type' => 'final'],
],

Actions

Actions represent side effects — things the machine does. Name them as verb phrases with an Action suffix.

Action Classes

php
// Class name: {Verb}{Object}Action — PascalCase
class SendNotificationAction extends ActionBehavior { ... }
class UpdateInventoryAction extends ActionBehavior { ... }
class ChargePaymentMethodAction extends ActionBehavior { ... }
class GenerateInvoicePdfAction extends ActionBehavior { ... }

Inline Behavior Keys

When registering actions in behavior arrays, use camelCase with the type suffix:

php
'behavior' => [
    'actions' => [
        'sendNotificationAction'    => SendNotificationAction::class,
        'updateInventoryAction'     => UpdateInventoryAction::class,
        'chargePaymentMethodAction' => ChargePaymentMethodAction::class,
    ],
],

Entry and Exit Actions

Entry actions typically use verbs that describe initialization or setup. Exit actions use verbs that describe cleanup or teardown:

php
'states' => [
    'processing' => [
        'entry' => 'startProcessingAction',    // initialize, start, load, notify
        'exit'  => 'logCompletionAction',      // log, cleanup, save, release, stop
        'on'    => [...],
    ],
],

Common entry action verbs: initialize, start, load, notify, acquire, begin

Common exit action verbs: log, cleanup, save, release, stop, flush

Multi-Word Actions

Start with the verb, then describe the object:

Class NameInline Key
SendNotificationActionsendNotificationAction
UpdateInventoryActionupdateInventoryAction
ChargePaymentMethodActionchargePaymentMethodAction
GenerateInvoicePdfActiongenerateInvoicePdfAction

Use Specific Verbs

Prefer specific verbs over generic ones. sendNotificationAction is better than handleNotificationAction. Other good verbs: create, update, delete, calculate, validate, notify, log, sync, assign.

Guards

Guards represent conditions — boolean checks that determine whether a transition should proceed. Name them with a boolean prefix and a Guard suffix.

Boolean Prefixes

PrefixUse ForExample
isState or identity checksIsPaymentValidGuard
hasPossession or existence checksHasSufficientBalanceGuard
canCapability or permission checksCanUserApproveGuard
shouldBusiness rule checksShouldRetryPaymentGuard

Guard Classes

php
// Class name: {Prefix}{Condition}Guard — PascalCase
class IsPaymentValidGuard extends GuardBehavior { ... }
class HasSufficientBalanceGuard extends GuardBehavior { ... }
class CanUserApproveGuard extends GuardBehavior { ... }
class ShouldRetryPaymentGuard extends GuardBehavior { ... }

Validation Guards

For guards that perform Laravel validation, use ValidationGuard suffix:

php
// Class name: {Prefix}{Condition}ValidationGuard — PascalCase
class IsAmountValidValidationGuard extends ValidationGuardBehavior { ... }
class IsEmailFormatValidValidationGuard extends ValidationGuardBehavior { ... }

Inline Behavior Keys

php
'behavior' => [
    'guards' => [
        'isPaymentValidGuard'       => IsPaymentValidGuard::class,
        'hasSufficientBalanceGuard' => HasSufficientBalanceGuard::class,
        'canUserApproveGuard'       => CanUserApproveGuard::class,
    ],
],

Calculators

Calculators compute values for the context. Name them with a descriptive noun and a Calculator suffix.

php
// Class name: {Subject}{Noun}Calculator — PascalCase
class OrderTotalCalculator extends CalculatorBehavior { ... }
class ShippingCostCalculator extends CalculatorBehavior { ... }
class TaxAmountCalculator extends CalculatorBehavior { ... }
class WeightedAverageCalculator extends CalculatorBehavior { ... }

Inline keys:

php
'behavior' => [
    'calculators' => [
        'orderTotalCalculator'   => OrderTotalCalculator::class,
        'shippingCostCalculator' => ShippingCostCalculator::class,
    ],
],

Results

Results compute the final output of a state machine. Name them with a descriptive noun and a Result suffix.

php
// Class name: {Subject}{Noun}Result — PascalCase
class InvoiceSummaryResult extends ResultBehavior { ... }
class OrderConfirmationResult extends ResultBehavior { ... }
class RiskAssessmentResult extends ResultBehavior { ... }

Machine Definition

Machine Classes

php
// Class name: {Domain}Machine — PascalCase
class OrderWorkflowMachine extends MachineDefinition { ... }
class PaymentProcessingMachine extends MachineDefinition { ... }
class UserOnboardingMachine extends MachineDefinition { ... }

Machine IDs

Use snake_case for machine IDs in configuration. The ID is derived from the domain name without the Machine suffix:

php
MachineDefinition::define(
    config: [
        'id' => 'order_workflow',      // snake_case, no Machine suffix
        // ...
    ],
);
Machine ClassMachine ID
OrderWorkflowMachineorder_workflow
PaymentProcessingMachinepayment_processing
UserOnboardingMachineuser_onboarding
TrafficLightsMachinetraffic_lights

The machine ID appears in internal event names. With snake_case, these read naturally:

order_workflow.state.processing.enter
order_workflow.transition.ORDER_SUBMITTED.start
order_workflow.guard.isPaymentValidGuard.pass

Eloquent Column Names

When using MachineCast, column names should use snake_case with an _mre suffix (Machine Root Event):

php
// Column name: {domain}_mre — snake_case
protected $casts = [
    'order_workflow_mre'      => MachineCast::class,
    'payment_processing_mre'  => MachineCast::class,
];

The _mre suffix makes it clear that the column stores a machine root event reference, not a plain string or JSON field.

Context

Array Context Keys

When defining context as an inline array, use snake_case:

php
'context' => [
    'total_amount'     => 0,
    'items_count'      => 0,
    'customer_email'   => null,
    'retry_count'      => 0,
    'last_error_code'  => null,
    'is_priority'      => false,
],

Typed Context Classes

When using a custom ContextManager subclass, properties follow PHP conventioncamelCase:

php
class OrderWorkflowContext extends ContextManager
{
    public int $totalAmount = 0;
    public int $itemsCount = 0;
    public ?string $customerEmail = null;
    public int $retryCount = 0;
    public ?string $lastErrorCode = null;
    public bool $isPriority = false;
}

Why Two Different Styles?

Array keys use snake_case to match PHP array and Laravel conventions. Typed class properties use camelCase to match PHP's object property conventions. This distinction is intentional — each follows the standard of its own context.

What to Avoid

php
// Don't use abbreviations
'amt'           // → 'amount'
'qty'           // → 'quantity'
'cnt'           // → 'count'

// Don't use generic names
'data'          // → 'order_data' or a specific key
'value'         // → 'payment_value' or a specific key
'status'        // → 'payment_status' (machine state handles status)

Scenarios

Scenario names use snake_case with descriptive, domain-specific identifiers:

php
MachineDefinition::define(
    config: [
        'scenarios_enabled' => true,
        'states' => [
            'idle' => [
                'on' => [
                    'ORDER_SUBMITTED' => [
                        [
                            'target'       => 'express_processing',
                            'scenarioType' => 'express_checkout',
                        ],
                        [
                            'target'       => 'standard_processing',
                            'scenarioType' => 'standard_checkout',
                        ],
                    ],
                ],
            ],
        ],
    ],
);

Good scenario names describe the business variant, not technical details:

php
// Descriptive business variants
'express_checkout'      // fast-track order flow
'enterprise_onboarding' // multi-step enterprise setup
'ab_test_v2'            // A/B test variant

// Avoid generic names
'scenario_1'            // → describe what makes it different
'test'                  // → describe the business case

Configuration Keys

All configuration keys should use snake_case:

php
MachineDefinition::define(
    config: [
        'id'                => 'order_workflow',
        'initial'           => 'idle',
        'context'           => [...],
        'states'            => [...],
        'should_persist'    => true,
        'scenarios_enabled' => false,
    ],
);

Internal Keys — @ Prefix Convention

Internal framework keys use the @ prefix: @always, @done, @fail. These are distinct from user-defined event types (SCREAMING_SNAKE_CASE) and state names (snake_case). The only remaining XState-inherited camelCase key is scenarioType.

File Organization

Organize behavior classes in a directory structure that mirrors the machine domain:

app/
└── MachineDefinitions/
    └── OrderWorkflow/
        ├── OrderWorkflowMachine.php
        ├── OrderWorkflowContext.php
        ├── Actions/
        │   ├── SendConfirmationEmailAction.php
        │   ├── UpdateInventoryAction.php
        │   └── ChargePaymentMethodAction.php
        ├── Guards/
        │   ├── IsPaymentValidGuard.php
        │   ├── HasSufficientBalanceGuard.php
        │   └── IsAmountValidValidationGuard.php
        ├── Events/
        │   ├── OrderSubmittedEvent.php
        │   └── PaymentReceivedEvent.php
        ├── Calculators/
        │   └── OrderTotalCalculator.php
        └── Results/
            └── OrderConfirmationResult.php

Summary

The key principles behind these conventions:

  1. Events are facts — past tense, describing what happened (OrderSubmitted, not SubmitOrder)
  2. States are conditions — adjectives or participles, passing the "is" test (processing, not process)
  3. Actions are verbs — describing what the machine does (SendNotification, not Notification)
  4. Guards are questions — boolean predicates with is/has/can/should prefix (IsPaymentValid, not PaymentValid)
  5. Context keys match their containersnake_case in arrays, camelCase in typed classes
  6. Inline keys include the type suffix'sendEmailAction' not 'sendEmail' for clarity
  7. Suffixes prevent ambiguityEvent, Action, Guard, Calculator, Result suffixes on class names make the role immediately clear
  8. Consistency over cleverness — pick one pattern and apply it everywhere

Released under the MIT License.