Skip to content

Implicit State Derivation Proposal

Status: ❌ REJECTED

Rejection Reason

This proposal was rejected because automatic state derivation is not possible. The relationship between event handlers (whether they form a sequential process or are independent operations) is semantic information that cannot be inferred from code structure alone.

Example: Given two handlers :checkout and :payment_received, the CLI cannot determine if these are part of a sequential checkout process, or two independent operations (like a calculator with separate :add, :subtract operations).

The moves to #state syntax remains the explicit way to define process flow.


Problem Statement

EventFlow's current state definition approach has inconsistencies:

  1. moves to repetition: Every event handler writes order moves to #state, duplicating states
  2. Conflicts with lane diagram philosophy: Sessions 1-3 decided not to mention states, but flow files have inline state transitions
  3. Outcome ambiguity: Terminal states don't explicitly mark success/failure/neutral status
  4. Mixed event flow and state structure: Same file contains both event communication and state transitions

Current Approach

flow
machine: @order

on :checkout from @customer (api)
  validate cart
  order moves to #awaiting_payment  // ← State written inline

on :payment_received
  process payment
  order moves to #confirmed  // ← Repeated in every handler

Problems:

  • #awaiting_payment can be written in multiple places
  • Terminal state's "success" status is not explicit
  • State transitions distract from reading the event flow

Proposed Solution

Make states implicit and define them in a separate .states.flow file.

New File Structure

order.flow           # Event flow (NO moves to)
order.test.flow      # Test variations
order.states.flow    # State definitions (CLI generated + developer edited)

New Flow File (no moves to)

flow
machine: @order

on :checkout from @customer (api)
  validate cart
  // NO moves to line - state is derived by CLI

on :payment_received
  process payment
  emit :confirmation to @customer
  // terminal state is clear from .states.flow

Generated States File

flow
// order.states.flow
// Auto-generated by: eventflow derive-states order.flow
// Edit state names and outcomes as needed

machine: @order

#idle
  type: initial
  description: Before any event is received

#awaiting_payment
  type: waiting
  after: :checkout from @customer
  waiting_for: :payment_received

#confirmed
  type: terminal
  outcome: success
  after: :payment_received

State Derivation Algorithm

The core principle of state derivation: Every wait point is a state.

Derivation Rules

SituationState TypeDescription
Before event handlerinitialNo event received yet
After handler, expecting another eventwaitingWaiting for next event
After emit async, expecting responsewaitingBackground process started
Final action, no more handlers expectedterminalProcess ended

State Naming Rules

State names should reflect what is being waited for, not how we got there:

PatternExampleDescription
#awaiting_{expected}#awaiting_paymentWaiting for a response/event
#processing_{what}#processing_orderAsync operation in progress
#{outcome}#confirmed, #cancelledTerminal state, indicates outcome
#{what}_failed#payment_failedWaiting after error

Example 1: Single Machine - Simple Linear Flow

The simplest case: one machine with sequential events, no branching, no external actors.

Flow File

flow
machine: @order

on :place_order (api)
  validate order details
  calculate total

on :confirm_order (api)
  finalize order
  generate receipt

Step-by-Step Derivation

╔══════════════════════════════════════════════════════════════════════╗
║  STEP 1: List Event Handlers                                         ║
╚══════════════════════════════════════════════════════════════════════╝

┌─ HANDLER 1 ────────────────────────────────────────────────────────┐
│                                                                    │
│  on :place_order (api)                                             │
│    ├─ validate order details                                       │
│    ├─ calculate total                                              │
│    └─ (handler ends, waiting for next event)                       │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

┌─ HANDLER 2 ────────────────────────────────────────────────────────┐
│                                                                    │
│  on :confirm_order (api)                                           │
│    ├─ finalize order                                               │
│    ├─ generate receipt                                             │
│    └─ (no more handlers expected - TERMINAL)                       │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘


╔══════════════════════════════════════════════════════════════════════╗
║  STEP 2: Identify Wait Points                                        ║
╚══════════════════════════════════════════════════════════════════════╝

@order machine timeline:

╭──────────╮         ╭──────────────────╮         ╭───────────╮
│ INITIAL  │─:place──│     WAITING      │─:confirm│ TERMINAL  │
│  #idle   │ _order  │#awaiting_confirm │ _order  │#completed │
╰──────────╯         ╰──────────────────╯         ╰───────────╯

┌─ WAIT POINT 1 ─────────────────────────────────────────────────────┐
│  Type: INITIAL                                                     │
│  Location: Before Handler 1                                        │
│  Waiting for: :place_order                                         │
│  Generated name: #idle                                             │
└────────────────────────────────────────────────────────────────────┘

┌─ WAIT POINT 2 ─────────────────────────────────────────────────────┐
│  Type: WAITING                                                     │
│  Location: After Handler 1                                         │
│  Waiting for: :confirm_order                                       │
│  Generated name: #after_place_order                                │
│  ✎ Suggested rename: #awaiting_confirmation                        │
└────────────────────────────────────────────────────────────────────┘

┌─ WAIT POINT 3 ─────────────────────────────────────────────────────┐
│  Type: TERMINAL                                                    │
│  Location: After Handler 2                                         │
│  No more handlers expected                                         │
│  Generated name: #after_confirm_order                              │
│  ✎ Suggested rename: #completed                                    │
│  ? Outcome: success                                                │
└────────────────────────────────────────────────────────────────────┘


╔══════════════════════════════════════════════════════════════════════╗
║  STEP 3: Generated State Diagram                                     ║
╚══════════════════════════════════════════════════════════════════════╝

    ┌──────────┐  :place_order   ┌────────────────────┐
    │          │ ──────────────► │                    │
    │  #idle   │                 │ #awaiting_confirm  │
    │ (initial)│                 │     (waiting)      │
    └──────────┘                 └─────────┬──────────┘

                                           │ :confirm_order

                                 ┌────────────────────┐
                                 │    #completed      │
                                 │ (terminal: success)│
                                 └────────────────────┘

Generated States File

flow
// order.states.flow

machine: @order

#idle
  type: initial
  waiting_for: :place_order

#awaiting_confirmation
  type: waiting
  after: :place_order
  waiting_for: :confirm_order

#completed
  type: terminal
  outcome: success
  after: :confirm_order

Example 2: Single Machine - Guarded Multi-Path Transitions

One machine with conditional branching based on guards.

Flow File

flow
machine: @order

on :checkout (api)
  ? cart is valid
    ? total > 1000
      apply bulk discount
    calculate final price
  otherwise
    log cart error
    // Terminal - invalid cart

on :payment_received
  ? payment amount matches total
    process payment
    generate confirmation
  otherwise
    log payment mismatch
    // Back to waiting for correct payment

on :cancel_order (api)
  ? order is cancellable
    refund if paid
    log cancellation
  otherwise
    log cancel rejected

Step-by-Step Derivation

╔══════════════════════════════════════════════════════════════════════╗
║  STEP 1: Extract All Execution Paths                                 ║
╚══════════════════════════════════════════════════════════════════════╝

  ┌─ PATH A ─────────────────────────────────────────────────────────┐
  │  :checkout → [cart invalid] → log error                          │
  │  Result: TERMINAL (failure)                                      │
  └──────────────────────────────────────────────────────────────────┘

  ┌─ PATH B ─────────────────────────────────────────────────────────┐
  │  :checkout → [cart valid] → calculate price → WAIT for payment   │
  │  Result: WAITING                                                 │
  └──────────────────────────────────────────────────────────────────┘

  ┌─ PATH C ─────────────────────────────────────────────────────────┐
  │  (from WAITING) → :payment_received → [match] → confirmation     │
  │  Result: TERMINAL (success)                                      │
  └──────────────────────────────────────────────────────────────────┘

  ┌─ PATH D ─────────────────────────────────────────────────────────┐
  │  (from WAITING) → :payment_received → [mismatch] → log error     │
  │  Result: LOOP BACK to WAITING                                    │
  └──────────────────────────────────────────────────────────────────┘

  ┌─ PATH E ─────────────────────────────────────────────────────────┐
  │  (from WAITING) → :cancel_order → [cancellable] → refund         │
  │  Result: TERMINAL (neutral)                                      │
  └──────────────────────────────────────────────────────────────────┘

  ┌─ PATH F ─────────────────────────────────────────────────────────┐
  │  (from WAITING) → :cancel_order → [not cancellable] → log error  │
  │  Result: LOOP BACK to WAITING                                    │
  └──────────────────────────────────────────────────────────────────┘


╔══════════════════════════════════════════════════════════════════════╗
║  STEP 2: Wait Point Analysis (Flow Diagram)                          ║
╚══════════════════════════════════════════════════════════════════════╝

                            :checkout

               ┌────────────────┴────────────────┐
               ▼                                 ▼
      ╔════════════════╗                ╔════════════════╗
      ║ cart is valid? ║                ║  cart invalid  ║
      ╚═══════╤════════╝                ╚═══════╤════════╝
              │                                 │
              ▼                                 ▼
      calculate price                     log error
              │                                 │
              ▼                                 ▼
 ╔════════════════════════╗         ┌───────────────────┐
 ║      WAIT POINT        ║         │     TERMINAL      │
 ║   #awaiting_payment    ║         │ #checkout_failed  │
 ║                        ║         │   (failure) ✗     │
 ╚═══════════╤════════════╝         └───────────────────┘

    ┌────────┴────────┐
    ▼                 ▼
:payment_received  :cancel_order
    │                 │
┌───┴───┐        ┌────┴────┐
▼       ▼        ▼         ▼
[match] [no]  [cancel-  [not
  │      │    lable]   cancel]
  │      │       │        │
  │      │       ▼        │
  │      │    refund      │
  │      │       │        │
  │      │       ▼        ▼
  │      │  ┌────────┐    │
  │      │  │TERMINAL│    │
  │      │  │#cancel-│    │
  │      │  │  led   │    │
  │      │  │(neut)○ │    │
  │      │  └────────┘    │
  │      │                │
  │      └───────┬────────┘
  │              │
  │              ▼
  │    (back to #awaiting_payment)


process payment
generate confirmation


┌───────────────────┐
│     TERMINAL      │
│    #confirmed     │
│   (success) ✓     │
└───────────────────┘


╔══════════════════════════════════════════════════════════════════════╗
║  STEP 3: Identified States                                           ║
╚══════════════════════════════════════════════════════════════════════╝

  ┌─ STATE 1 ──────────────────────────────────────────────────────┐
  │  Name: #idle                                                   │
  │  Type: INITIAL                                                 │
  │  Waiting for: :checkout                                        │
  └────────────────────────────────────────────────────────────────┘

  ┌─ STATE 2 ──────────────────────────────────────────────────────┐
  │  Name: #checkout_failed                                        │
  │  Type: TERMINAL                                                │
  │  Outcome: failure ✗                                            │
  │  After: :checkout (when cart invalid)                          │
  └────────────────────────────────────────────────────────────────┘

  ┌─ STATE 3 ──────────────────────────────────────────────────────┐
  │  Name: #awaiting_payment                                       │
  │  Type: WAITING                                                 │
  │  After: :checkout (when cart valid)                            │
  │  Waiting for: :payment_received, :cancel_order                 │
  │  Note: Loop target for PATH D and PATH F                       │
  └────────────────────────────────────────────────────────────────┘

  ┌─ STATE 4 ──────────────────────────────────────────────────────┐
  │  Name: #confirmed                                              │
  │  Type: TERMINAL                                                │
  │  Outcome: success ✓                                            │
  │  After: :payment_received (when amount matches)                │
  └────────────────────────────────────────────────────────────────┘

  ┌─ STATE 5 ──────────────────────────────────────────────────────┐
  │  Name: #cancelled                                              │
  │  Type: TERMINAL                                                │
  │  Outcome: neutral ○                                            │
  │  After: :cancel_order (when cancellable)                       │
  └────────────────────────────────────────────────────────────────┘


╔══════════════════════════════════════════════════════════════════════╗
║  STEP 4: Generated State Diagram                                     ║
╚══════════════════════════════════════════════════════════════════════╝

                            ┌────────────┐
                            │   #idle    │
                            │ (initial)  │
                            └─────┬──────┘
                                  │ :checkout
                     ┌────────────┴────────────┐
                     ▼                         ▼
            ┌─────────────────┐      ┌───────────────────┐
            │#checkout_failed │      │ #awaiting_payment │
            │ (failure) ✗     │      │    (waiting)      │◄────┐
            └─────────────────┘      └─────────┬─────────┘     │
                                               │               │
                                  ┌────────────┼────────────┐  │
                                  │            │            │  │
                                  ▼            │            ▼  │
                         ┌────────────┐        │   ┌──────────┐│
                         │ #confirmed │        │   │#cancelled││
                         │(success) ✓ │        │   │(neutral)○││
                         └────────────┘        │   └──────────┘│
                         :payment_received     │   :cancel     │
                         [match]               │   [ok]        │
                                               │               │
                                               └───────────────┘
                                               [mismatch] / [not ok]

Generated States File

flow
// order.states.flow

machine: @order

#idle
  type: initial
  waiting_for: :checkout

#checkout_failed
  type: terminal
  outcome: failure
  after: :checkout
  when: cart is invalid

#awaiting_payment
  type: waiting
  after: :checkout
  when: cart is valid
  waiting_for: :payment_received, :cancel_order

#confirmed
  type: terminal
  outcome: success
  after: :payment_received
  when: payment amount matches total

#cancelled
  type: terminal
  outcome: neutral
  after: :cancel_order
  when: order is cancellable

Example 3: Single Machine - Sync/Async Events and Queued API

This example demonstrates the impact of sync vs async emit and queued API on state derivation.

Flow File

flow
machine: @report

// ─── Scenario A: Queued API ───
// Event is queued, API returns immediately, processing in background

on :generate_report (queued api)
  $report_id becomes uuid()
  start report generation
  // Handler ends, report generates in background

  reply 202 with:
    | report_id | $report_id |
    | status    | "queued"   |

on :generation_complete
  $report_url becomes $result.url
  send callback for :generate_report:
    | report_id | $report_id  |
    | status    | "completed" |
    | url       | $report_url |


// ─── Scenario B: Sync Emit ───
// Emit without 'async' waits for result inline (same request)

on :quick_lookup (api)
  $data becomes emit :fetch_cache  // SYNC - waits inline

  ? $data exists
    reply 200 with:
      | data | $data |
  otherwise
    reply 404 with:
      | error | "NOT_FOUND" |

on :fetch_cache
  lookup in cache
  return $cached_data


// ─── Scenario C: Async Emit ───
// Emit with 'async' doesn't wait - fire and forget

on :process_order (api)
  validate order
  emit async :send_notification  // Fire-and-forget, no wait
  emit async :update_analytics   // Fire-and-forget, no wait

  reply 201 with:
    | order_id | $order_id |

on :send_notification
  send email

on :update_analytics
  log order metrics

State Derivation Analysis

╔══════════════════════════════════════════════════════════════════════╗
║  SCENARIO A: Queued API                                              ║
║  Event is queued, API returns immediately, background processing     ║
╚══════════════════════════════════════════════════════════════════════╝

  :generate_report (queued api)


  ╔════════════════════════════════════════════════════════════════════╗
  ║  (queued api) BEHAVIOR:                                            ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │  1. Event validation runs synchronously                      │  ║
  ║  │  2. Event is placed in queue                                 │  ║
  ║  │  3. API returns 202 immediately                              │  ║
  ║  │  4. Handler runs in background worker                        │  ║
  ║  └──────────────────────────────────────────────────────────────┘  ║
  ║                                                                    ║
  ║  ⚠ THIS CREATES A WAIT POINT - background processing starts        ║
  ╚════════════════════════════════════════════════════════════════════╝


  start report generation


  ╔════════════════════════╗
  ║      WAIT POINT        ║
  ║     #generating        ║
  ║  waiting_for:          ║
  ║  :generation_complete  ║
  ╚═══════════╤════════════╝


  :generation_complete


  send callback for :generate_report


  ┌───────────────────┐
  │     TERMINAL      │
  │  #report_ready    │
  │   (success) ✓     │
  └───────────────────┘


╔══════════════════════════════════════════════════════════════════════╗
║  SCENARIO B: Sync Emit                                               ║
║  Emit without 'async' waits for result inline (same request)         ║
╚══════════════════════════════════════════════════════════════════════╝

  :quick_lookup (api)


  $data becomes emit :fetch_cache


  ╔════════════════════════════════════════════════════════════════════╗
  ║  SYNC EMIT BEHAVIOR:                                               ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │  1. :fetch_cache handler is called inline                    │  ║
  ║  │  2. Result is returned immediately                           │  ║
  ║  │  3. Execution continues with $data                           │  ║
  ║  │  4. All within the SAME REQUEST                              │  ║
  ║  └──────────────────────────────────────────────────────────────┘  ║
  ║                                                                    ║
  ║  ✓ THIS IS NOT A WAIT POINT - everything happens synchronously     ║
  ╚════════════════════════════════════════════════════════════════════╝

        ├─────────────────────┬─────────────────────┐
        ▼                     ▼
  ╔═════════════╗       ╔═════════════╗
  ║ $data exists║       ║$data = null ║
  ╚══════╤══════╝       ╚══════╤══════╝
         │                     │
         ▼                     ▼
  reply 200 with data    reply 404
         │                     │
         ▼                     ▼
  ┌─────────────┐       ┌─────────────┐
  │  TERMINAL   │       │  TERMINAL   │
  │   #found    │       │ #not_found  │
  │ (success) ✓ │       │ (failure) ✗ │
  └─────────────┘       └─────────────┘


╔══════════════════════════════════════════════════════════════════════╗
║  SCENARIO C: Async Emit (Fire-and-Forget)                            ║
║  Emit with 'async' doesn't wait - background handlers triggered      ║
╚══════════════════════════════════════════════════════════════════════╝

  :process_order (api)


  validate order


  emit async :send_notification
  emit async :update_analytics

  ╔════════════════════════════════════════════════════════════════════╗
  ║  ASYNC EMIT (Fire-and-Forget) BEHAVIOR:                            ║
  ║  ┌──────────────────────────────────────────────────────────────┐  ║
  ║  │  1. :send_notification is queued for background processing   │  ║
  ║  │  2. :update_analytics is queued for background processing    │  ║
  ║  │  3. Main handler continues IMMEDIATELY                       │  ║
  ║  │  4. No response is expected from these handlers              │  ║
  ║  └──────────────────────────────────────────────────────────────┘  ║
  ║                                                                    ║
  ║  ✓ THIS IS NOT A WAIT POINT - fire and forget, no response         ║
  ╚════════════════════════════════════════════════════════════════════╝


  reply 201 with order_id


  ┌───────────────────┐
  │     TERMINAL      │
  │ #order_processed  │
  │   (success) ✓     │
  └───────────────────┘


╔══════════════════════════════════════════════════════════════════════╗
║  SUMMARY: Sync vs Async State Derivation                             ║
╚══════════════════════════════════════════════════════════════════════╝

  ┌────────────────────────────────────────────────────────────────────┐
  │  EMIT TYPE            │ WAIT? │ REASON                             │
  ├───────────────────────┼───────┼────────────────────────────────────┤
  │  emit :event          │  NO   │ Sync - runs inline, returns result │
  │  (no async keyword)   │       │                                    │
  ├───────────────────────┼───────┼────────────────────────────────────┤
  │  emit async :event    │  NO   │ Fire-and-forget, no response need  │
  │  (no response handler)│       │                                    │
  ├───────────────────────┼───────┼────────────────────────────────────┤
  │  emit async :event    │  YES  │ Response handler exists, must wait │
  │  (response handler)   │       │                                    │
  ├───────────────────────┼───────┼────────────────────────────────────┤
  │  (queued api)         │  YES  │ Event queued, background process   │
  ├───────────────────────┼───────┼────────────────────────────────────┤
  │  reply 202 async for  │  YES  │ Callback will be sent later        │
  └───────────────────────┴───────┴────────────────────────────────────┘

Sync vs Async State Derivation Rules

Emit TypeCreates Wait Point?Description
emit :event (sync)NOExecutes inline, result returned in same request
emit async :eventNO (fire-forget)Background execution, no response expected
emit async :event + handlerYESIf there's a handler waiting for response
(queued api) modifierYESEvent queued, handler runs in background
reply 202 async forYESCallback will be sent later

Generated States File

flow
// report.states.flow

machine: @report

// ─── Scenario A States ───

#idle_report
  type: initial
  waiting_for: :generate_report

#generating
  type: waiting
  after: :generate_report
  api_mode: queued
  waiting_for: :generation_complete

#report_ready
  type: terminal
  outcome: success
  after: :generation_complete

// ─── Scenario B States ───

#idle_lookup
  type: initial
  waiting_for: :quick_lookup

#found
  type: terminal
  outcome: success
  after: :quick_lookup
  sync_call: emit :fetch_cache
  when: $data exists

#not_found
  type: terminal
  outcome: failure
  after: :quick_lookup
  sync_call: emit :fetch_cache
  when: $data not found

// ─── Scenario C States ───

#idle_order
  type: initial
  waiting_for: :process_order

#order_processed
  type: terminal
  outcome: success
  after: :process_order
  fire_and_forget: :send_notification, :update_analytics

Key Insight: Sync emit does NOT create a wait point because the result is received within the same request. Async fire-and-forget also doesn't create a wait point. Only queued API and async emits that expect a response event create wait points.


Example 4: Multi-Machine Systems

Now we introduce multiple machines communicating with each other.

Example 4a: Two Machines - Simple Request/Response

flow
// ─── order.flow ───
machine: @order

on :checkout from @customer (api)
  validate cart
  emit :request_payment to @payment
    with $order_id, $total

on :payment_success from @payment
  emit :confirmation to @customer

on :payment_failed from @payment
  emit :checkout_failed to @customer
flow
// ─── payment.flow ───
machine: @payment

on :request_payment from @order
  process payment with gateway
  ? payment successful
    emit :payment_success to @order
      with $transaction_id
  otherwise
    emit :payment_failed to @order
      with $error_code

Cross-Machine Communication Pattern:

  1. @order sends :request_payment to @payment
  2. @payment processes and sends :payment_success or :payment_failed back to @order
  3. @order handles the response in separate handlers

State Derivation (for @order)

╔══════════════════════════════════════════════════════════════════════╗
║  CROSS-MACHINE COMMUNICATION PATTERN                                 ║
╚══════════════════════════════════════════════════════════════════════╝

  ┌─ @order machine ─────────────────────────────────────────────────┐
  │                                                                  │
  │  ┌────────────┐  :checkout   ┌───────────────────┐               │
  │  │   #idle    │ ───────────► │ #awaiting_payment │               │
  │  │ (initial)  │              │    (waiting)      │               │
  │  └────────────┘              └─────────┬─────────┘               │
  │                                        │                         │
  │                     emit :request_payment to @payment            │
  │                                        │                         │
  │  ╔════════════════════════════════════════════════════════════╗  │
  │  ║  CROSS-MACHINE WAIT:                                       ║  │
  │  ║  @order waits for @payment to respond                      ║  │
  │  ║  Responses: :payment_success OR :payment_failed            ║  │
  │  ╚════════════════════════════════════════════════════════════╝  │
  │                                        │                         │
  │                   ┌────────────────────┴────────────────────┐    │
  │                   ▼                                         ▼    │
  │          :payment_success                        :payment_failed │
  │          (from @payment)                         (from @payment) │
  │                   │                                         │    │
  │                   ▼                                         ▼    │
  │         ┌─────────────────┐                    ┌──────────────┐  │
  │         │   #confirmed    │                    │   #failed    │  │
  │         │  (success) ✓    │                    │ (failure) ✗  │  │
  │         └─────────────────┘                    └──────────────┘  │
  │                                                                  │
  └──────────────────────────────────────────────────────────────────┘

Generated States (order.states.flow)

flow
// order.states.flow

machine: @order

#idle
  type: initial
  waiting_for: :checkout from @customer

#awaiting_payment
  type: waiting
  after: :checkout
  emits: :request_payment to @payment
  waiting_for: :payment_success, :payment_failed from @payment

#confirmed
  type: terminal
  outcome: success
  after: :payment_success from @payment

#failed
  type: terminal
  outcome: failure
  after: :payment_failed from @payment

Example 4b: Three Machines - Chained Communication

flow
// ─── order.flow ───
machine: @order

on :checkout from @customer (api)
  emit :request_payment to @payment
    with $order_id, $total

on :payment_success from @payment
  emit :reserve_stock to @warehouse
    with $order_id, $items

on :stock_reserved from @warehouse
  emit :confirmation to @customer

on :payment_failed from @payment
  emit :checkout_failed to @customer

on :stock_unavailable from @warehouse
  emit :request_refund to @payment
  emit :out_of_stock to @customer

on :refund_complete from @payment
  // Order fully rolled back
flow
// ─── payment.flow ───
machine: @payment

on :request_payment from @order
  process payment
  ? successful
    emit :payment_success to @order
  otherwise
    emit :payment_failed to @order

on :request_refund from @order
  process refund
  emit :refund_complete to @order
flow
// ─── warehouse.flow ───
machine: @warehouse

on :reserve_stock from @order
  check inventory
  ? items available
    reserve items
    emit :stock_reserved to @order
  otherwise
    emit :stock_unavailable to @order

State Derivation (for @order)

╔══════════════════════════════════════════════════════════════════════╗
║  THREE-MACHINE CHAINED COMMUNICATION                                 ║
║  @order → @payment → @order → @warehouse → @order                    ║
╚══════════════════════════════════════════════════════════════════════╝

┌─ @order machine ─────────────────────────────────────────────────────┐
│                                                                      │
│  ┌────────────┐                                                      │
│  │   #idle    │                                                      │
│  │ (initial)  │                                                      │
│  └─────┬──────┘                                                      │
│        │ :checkout                                                   │
│        ▼                                                             │
│  emit :request_payment ─────────────────────────────► @payment       │
│        │                                                             │
│        ▼                                                             │
│  ╔═══════════════════════╗                                           │
│  ║     WAIT POINT 1      ║                                           │
│  ║  #awaiting_payment    ║                                           │
│  ╚═══════════╤═══════════╝                                           │
│              │                                                       │
│  ┌───────────┴────────────────────┐                                  │
│  ▼                                ▼                                  │
│  :payment_success           :payment_failed                          │
│  (from @payment)            (from @payment)                          │
│  │                                │                                  │
│  │                                ▼                                  │
│  │                      ┌───────────────────┐                        │
│  │                      │     TERMINAL      │                        │
│  │                      │ #payment_failed   │                        │
│  │                      │   (failure) ✗     │                        │
│  │                      └───────────────────┘                        │
│  │                                                                   │
│  ▼                                                                   │
│  emit :reserve_stock ───────────────────────────────► @warehouse     │
│  │                                                                   │
│  ▼                                                                   │
│  ╔═══════════════════════╗                                           │
│  ║     WAIT POINT 2      ║                                           │
│  ║   #awaiting_stock     ║                                           │
│  ╚═══════════╤═══════════╝                                           │
│              │                                                       │
│  ┌───────────┴────────────────────┐                                  │
│  ▼                                ▼                                  │
│  :stock_reserved           :stock_unavailable                        │
│  (from @warehouse)         (from @warehouse)                         │
│  │                                │                                  │
│  ▼                                ▼                                  │
│  ┌───────────────────┐     emit :request_refund ────► @payment       │
│  │     TERMINAL      │            │                                  │
│  │   #completed      │            ▼                                  │
│  │   (success) ✓     │     ╔═══════════════════════╗                 │
│  └───────────────────┘     ║     WAIT POINT 3      ║                 │
│                            ║  #awaiting_refund     ║                 │
│                            ╚═══════════╤═══════════╝                 │
│                                        │                             │
│                                        ▼                             │
│                                 :refund_complete                     │
│                                 (from @payment)                      │
│                                        │                             │
│                                        ▼                             │
│                            ┌───────────────────┐                     │
│                            │     TERMINAL      │                     │
│                            │    #refunded      │                     │
│                            │   (failure) ✗     │                     │
│                            └───────────────────┘                     │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘


╔══════════════════════════════════════════════════════════════════════╗
║  IDENTIFIED STATES FOR @order                                        ║
╚══════════════════════════════════════════════════════════════════════╝

  ┌─ STATE 1 ──────────────────────────────────────────────────────────┐
  │  #idle (initial)                                                   │
  │  Waiting for: :checkout from @customer                             │
  └────────────────────────────────────────────────────────────────────┘

  ┌─ STATE 2 ──────────────────────────────────────────────────────────┐
  │  #awaiting_payment (waiting)                                       │
  │  After: :checkout                                                  │
  │  Emits: :request_payment to @payment                               │
  │  Waiting for: :payment_success, :payment_failed from @payment      │
  └────────────────────────────────────────────────────────────────────┘

  ┌─ STATE 3 ──────────────────────────────────────────────────────────┐
  │  #payment_failed (terminal)                                        │
  │  Outcome: failure ✗                                                │
  │  After: :payment_failed from @payment                              │
  └────────────────────────────────────────────────────────────────────┘

  ┌─ STATE 4 ──────────────────────────────────────────────────────────┐
  │  #awaiting_stock (waiting)                                         │
  │  After: :payment_success from @payment                             │
  │  Emits: :reserve_stock to @warehouse                               │
  │  Waiting for: :stock_reserved, :stock_unavailable                  │
  └────────────────────────────────────────────────────────────────────┘

  ┌─ STATE 5 ──────────────────────────────────────────────────────────┐
  │  #completed (terminal)                                             │
  │  Outcome: success ✓                                                │
  │  After: :stock_reserved from @warehouse                            │
  └────────────────────────────────────────────────────────────────────┘

  ┌─ STATE 6 ──────────────────────────────────────────────────────────┐
  │  #awaiting_refund (waiting)                                        │
  │  After: :stock_unavailable from @warehouse                         │
  │  Emits: :request_refund to @payment                                │
  │  Waiting for: :refund_complete from @payment                       │
  └────────────────────────────────────────────────────────────────────┘

  ┌─ STATE 7 ──────────────────────────────────────────────────────────┐
  │  #refunded (terminal)                                              │
  │  Outcome: failure ✗                                                │
  │  After: :refund_complete from @payment                             │
  └────────────────────────────────────────────────────────────────────┘

Generated States (order.states.flow)

flow
// order.states.flow

machine: @order

#idle
  type: initial
  waiting_for: :checkout from @customer

#awaiting_payment
  type: waiting
  after: :checkout
  emits: :request_payment to @payment
  waiting_for: :payment_success, :payment_failed from @payment

#payment_failed
  type: terminal
  outcome: failure
  after: :payment_failed from @payment

#awaiting_stock
  type: waiting
  after: :payment_success from @payment
  emits: :reserve_stock to @warehouse
  waiting_for: :stock_reserved, :stock_unavailable from @warehouse

#completed
  type: terminal
  outcome: success
  after: :stock_reserved from @warehouse

#awaiting_refund
  type: waiting
  after: :stock_unavailable from @warehouse
  emits: :request_refund to @payment
  waiting_for: :refund_complete from @payment

#refunded
  type: terminal
  outcome: failure
  after: :refund_complete from @payment

Example 4c: Parallel Multi-Actor Communication

flow
// ─── loan_application.flow ───
machine: @loan

on :submit from @customer (api)
  $app_id becomes uuid()
  emit async :check_credit to @credit_bureau
  emit async :verify_identity to @id_service
  emit async :check_fraud to @fraud_service

on :credit_result from @credit_bureau
  $credit_score becomes $result.score
  check_if_all_complete

on :identity_verified from @id_service
  $identity_ok becomes true
  check_if_all_complete

on :fraud_result from @fraud_service
  $fraud_ok becomes $result.passed
  check_if_all_complete

on :all_checks_complete
  ? $credit_score > 700 and $identity_ok and $fraud_ok
    emit :approved to @customer
  otherwise
    emit :declined to @customer

Parallel Async State Derivation

╔══════════════════════════════════════════════════════════════════════╗
║  PARALLEL MULTI-ACTOR COMMUNICATION                                  ║
║  One event triggers multiple parallel async calls                    ║
╚══════════════════════════════════════════════════════════════════════╝

┌─ @loan machine ──────────────────────────────────────────────────────┐
│                                                                      │
│  ┌────────────┐                                                      │
│  │   #idle    │                                                      │
│  │ (initial)  │                                                      │
│  └─────┬──────┘                                                      │
│        │ :submit                                                     │
│        ▼                                                             │
│  ╔════════════════════════════════════════════════════════════════╗  │
│  ║  PARALLEL ASYNC EMIT:                                          ║  │
│  ║  ┌──────────────────────────────────────────────────────────┐  ║  │
│  ║  │  emit async :check_credit ────────► @credit_bureau       │  ║  │
│  ║  │  emit async :verify_identity ─────► @id_service          │  ║  │
│  ║  │  emit async :check_fraud ─────────► @fraud_service       │  ║  │
│  ║  └──────────────────────────────────────────────────────────┘  ║  │
│  ║                                                                ║  │
│  ║  ⚠ ALL THREE run in parallel, responses arrive ANY ORDER       ║  │
│  ╚════════════════════════════════════════════════════════════════╝  │
│        │                                                             │
│        ▼                                                             │
│  ╔════════════════════════════════════╗                              │
│  ║         WAIT POINT                 ║                              │
│  ║    #collecting_results             ║                              │
│  ║                                    ║                              │
│  ║  waiting_for_all:                  ║                              │
│  ║   • :credit_result                 ║                              │
│  ║   • :identity_verified             ║                              │
│  ║   • :fraud_result                  ║                              │
│  ╚════════════════╤═══════════════════╝                              │
│                   │                                                  │
│     ╔═════════════╧═════════════╗                                    │
│     ║  RESPONSES ARRIVE IN      ║                                    │
│     ║  ANY ORDER:               ║                                    │
│     ║                           ║                                    │
│     ║  Scenario 1: C → I → F    ║                                    │
│     ║  Scenario 2: F → C → I    ║                                    │
│     ║  Scenario 3: I → F → C    ║                                    │
│     ║  ... (any permutation)    ║                                    │
│     ╚═══════════════════════════╝                                    │
│                   │                                                  │
│  ┌────────────────┼────────────────┐                                 │
│  ▼                ▼                ▼                                 │
│  :credit_result  :identity_   :fraud_result                          │
│  (from @credit)   verified    (from @fraud)                          │
│       │          (from @id)        │                                 │
│       │               │            │                                 │
│       ▼               ▼            ▼                                 │
│  ┌─────────────────────────────────────────┐                         │
│  │  Each handler:                          │                         │
│  │   1. Updates context ($credit_score,    │                         │
│  │      $identity_ok, $fraud_ok)           │                         │
│  │   2. Calls check_if_all_complete        │                         │
│  │   3. If all 3 received → emit           │                         │
│  │      :all_checks_complete               │                         │
│  └────────────────────┬────────────────────┘                         │
│                       │                                              │
│                       ▼                                              │
│              :all_checks_complete                                    │
│                       │                                              │
│          ┌────────────┴────────────┐                                 │
│          ▼                         ▼                                 │
│  ╔═══════════════╗         ╔═══════════════╗                         │
│  ║ $credit > 700 ║         ║ any check     ║                         │
│  ║ AND           ║         ║ failed        ║                         │
│  ║ $identity_ok  ║         ╚═══════╤═══════╝                         │
│  ║ AND $fraud_ok ║                 │                                 │
│  ╚═══════╤═══════╝                 │                                 │
│          │                         │                                 │
│          ▼                         ▼                                 │
│  ┌─────────────────┐       ┌─────────────────┐                       │
│  │    TERMINAL     │       │    TERMINAL     │                       │
│  │   #approved     │       │   #declined     │                       │
│  │  (success) ✓    │       │  (failure) ✗    │                       │
│  └─────────────────┘       └─────────────────┘                       │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘


╔══════════════════════════════════════════════════════════════════════╗
║  KEY INSIGHT: Parallel Async Collection                              ║
╚══════════════════════════════════════════════════════════════════════╝

  ┌────────────────────────────────────────────────────────────────────┐
  │  When multiple async emits are fired simultaneously:               │
  │                                                                    │
  │  1. All events are sent in parallel                                │
  │  2. Responses can arrive in ANY order                              │
  │  3. Each response handler updates shared context                   │
  │  4. A "collector" pattern checks if all responses received         │
  │  5. Only ONE state exists for the "collecting" phase               │
  │                                                                    │
  │  The state #collecting_results represents the ENTIRE collection    │
  │  phase, not separate states for each pending response.             │
  └────────────────────────────────────────────────────────────────────┘

Generated States (loan.states.flow)

flow
// loan.states.flow

machine: @loan

#idle
  type: initial
  waiting_for: :submit from @customer

#collecting_results
  type: waiting
  after: :submit
  emits_async:
    - :check_credit to @credit_bureau
    - :verify_identity to @id_service
    - :check_fraud to @fraud_service
  waiting_for_all:
    - :credit_result from @credit_bureau
    - :identity_verified from @id_service
    - :fraud_result from @fraud_service
  then: :all_checks_complete

#approved
  type: terminal
  outcome: success
  after: :all_checks_complete
  when: all checks passed

#declined
  type: terminal
  outcome: failure
  after: :all_checks_complete
  when: any check failed

State Naming: Multi-Event Paths

Problem

In paths with 3-5 events, generated names become too long:

#after_checkout_payment_success_stock_reserved_shipping_scheduled

Solution: Semantic Naming

Instead of event lists, use names that describe the current situation:

Generated (Bad)Semantic (Good)Why?
#after_checkout#awaiting_paymentDescribes what we're waiting for
#after_payment_success#preparing_shipmentDescribes what's happening
#after_payment_and_stock#ready_to_shipDescribes current status
#after_all_events#completedDescribes the outcome

Naming Strategy

Generated name = #after_{last_event}

Developer choices:
  1. #awaiting_{expected_event}  → "What are we waiting for?"
  2. #processing_{what}          → "What's being processed?"
  3. #{status}                   → "What's the current status?"
  4. #{outcome}                  → "How did the process end?"

CLI Commands

Generate States (Default: Interactive with Diagram)

Default mode is interactive with diagram URL:

bash
$ eventflow derive-states order.flow

Analyzing wait points in order.flow...

Found 5 states. Opening interactive diagram...

╔════════════════════════════════════════════════════════════════════╗
  📊 State Diagram URL:
  http://localhost:4173/diagram/order?session=abc123

  Open this URL in your browser to see the diagram.
  States will be highlighted as you name them.
╚════════════════════════════════════════════════════════════════════╝

───────────────────────────────────────────────────────────────────────
STATE 1 of 5: INITIAL STATE                      [Highlighted in diagram]
───────────────────────────────────────────────────────────────────────
  Type: initial
  Description: Before any event is received
  Waiting for: :checkout from @customer

  Generated name: #idle

? Keep this name or enter a new one [idle]: █

───────────────────────────────────────────────────────────────────────
STATE 2 of 5: WAITING STATE                      [Highlighted in diagram]
───────────────────────────────────────────────────────────────────────
  Type: waiting
  After: :checkout from @customer
  Emits: :request_payment to @payment
  Waiting for: :payment_success, :payment_failed from @payment

  Generated name: #after_checkout

? Keep this name or enter a new one [after_checkout]: awaiting_payment

 Renamed to #awaiting_payment

───────────────────────────────────────────────────────────────────────
STATE 3 of 5: TERMINAL STATE                     [Highlighted in diagram]
───────────────────────────────────────────────────────────────────────
  Type: terminal
  After: :payment_success from @payment

  Generated name: #after_payment_success

? Keep this name or enter a new one [after_payment_success]: confirmed

 Renamed to #confirmed

? What is the outcome of this state? (Use arrow keys)
 success  - Happy path completion (shown in green)
    failure  - Error or rejection (shown in red)
    neutral  - Neither success nor failure (shown in gray)

 Outcome: success

═══════════════════════════════════════════════════════════════════════

 Generated order.states.flow

Summary:
 5 states defined
 2 terminal states (1 success, 1 failure)
 Diagram updated at: http://localhost:4173/diagram/order

Diagram URL and Highlighting Mechanism

Diagram shown in browser:

┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│                         order.flow                                  │
│                                                                     │
│    ┌─────────┐                                                      │
│    │         │  :checkout                                           │
│    │ ██████  │ ─────────────────►  ┌─────────────────┐              │
│    │ ██████  │  (NAMING THIS)      │                 │              │
│    │         │                     │ #after_checkout │              │
│    └─────────┘                     │                 │              │
│      #idle                         └────────┬────────┘              │
│                                             │                       │
│                              :payment_success│:payment_failed       │
│                           ┌─────────────────┴──────────────┐        │
│                           ▼                                ▼        │
│                    ┌────────────┐                   ┌────────────┐  │
│                    │            │                   │            │  │
│                    │ #confirmed │                   │  #failed   │  │
│                    │            │                   │            │  │
│                    └────────────┘                   └────────────┘  │
│                                                                     │
│    ████ = Currently being named (highlighted)                       │
│    ─── = Transition                                                 │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Diagram features:
  • WebSocket real-time updates from CLI
  • Currently naming state highlighted with pulse animation
  • State colors update as naming completes
  • Terminal states show outcome color (green/red/gray)

Generate States (Non-Interactive / CI Mode)

For CI/CD pipelines or quick generation:

bash
$ eventflow derive-states order.flow --no-interactive

Analyzing wait points in order.flow...

Found 5 states with generated names:
  #idle                    (initial)
  #after_checkout          (waiting)
  #after_payment_failed    (waiting)
  #after_payment_success   (terminal, outcome: ?)
  #after_max_retries       (terminal, outcome: ?)

 Generated order.states.flow

 Warning: 2 terminal states have undefined outcomes.
  Please edit order.states.flow to set outcomes, or run:
  eventflow derive-states order.flow  (interactive mode)

Update Existing States

Update existing states when flow file changes:

bash
$ eventflow derive-states order.flow --update

Analyzing order.flow against existing order.states.flow...

╔════════════════════════════════════════════════════════════════════╗
  📊 Diagram URL: http://localhost:4173/diagram/order?session=xyz
╚════════════════════════════════════════════════════════════════════╝

Changes detected:

  ┌─ NEW STATES ─────────────────────────────────────────────────────┐

  + Wait point after :refund_requested
    Generated name: #after_refund_requested                       │

  ? Keep this name or enter a new one: awaiting_refund
  └──────────────────────────────────────────────────────────────────┘

  ┌─ REMOVED STATES ─────────────────────────────────────────────────┐

  - #after_legacy_event (no longer reachable in flow)             │

  ? Remove this state? (Y/n): Y                                   │
  └──────────────────────────────────────────────────────────────────┘

  ┌─ PRESERVED STATES ───────────────────────────────────────────────┐

 #idle               (initial)                                 │
 #awaiting_payment   (waiting)                                 │
 #confirmed          (terminal, success)                       │
  └──────────────────────────────────────────────────────────────────┘

? Apply changes? (Y/n): Y

 Updated order.states.flow

Validate States

Check flow and states file consistency:

bash
$ eventflow validate order.flow --states

Validating order.flow against order.states.flow...

 Validation failed:

  ERRORS:
 State #pending_shipment in .states.flow but not reachable in flow
 Wait point after :ship_order has no corresponding state

  WARNINGS:
 Terminal state #cancelled has no handler that leads to it

Fix with:
  $ eventflow derive-states order.flow --update

State File Syntax

State Types

TypeMeaningRequired Fields
initialFirst state before any event-
waitingWaiting for external responseafter, waiting_for
terminalFinal state, no more eventsoutcome

Outcome Values

OutcomeMeaningDiagram Color
successHappy path completionGreen
failureError/rejectionRed
neutralNeither success nor failureGray

Full State Syntax

flow
#state_name
  type: initial | waiting | terminal
  outcome: success | failure | neutral    // Only for terminal
  after: :event from @actor               // What triggers this state
  emits: :event to @actor                 // What this state emits
  waiting_for: :event1, :event2           // What ends this state
  description: Human readable text        // Optional

Migration Path

Phase 1: Introduce .states.flow

  1. Add eventflow derive-states command
  2. Generate .states.flow from existing flows
  3. Both moves to and .states.flow work together

Phase 2: Deprecate moves to

  1. Warn when moves to used but .states.flow exists
  2. Update documentation to use implicit style
  3. moves to still works but shows deprecation warning

Phase 3: Remove moves to

  1. moves to syntax removed
  2. All flows must use .states.flow
  3. Migration tool converts old flows

Benefits

BenefitDescription
DRYStates defined in one place, no repetition
Separation of ConcernsEvent flow and state structure separate
Explicit OutcomesTerminal states marked as success/failure
Tool SupportCLI derivation, validation, diagram generation
Lane Diagram AlignedFlow files contain only event communication

Potential Concerns

Readability

Concern: Hard to understand flow without seeing state transitions.

Solutions:

  • eventflow diagram command always available
  • IDE extension can show states as annotations
  • .states.flow file always alongside

Two File Management

Concern: Hard to keep main flow and states file in sync.

Solutions:

  • eventflow validate detects inconsistencies
  • eventflow derive-states --update auto-updates
  • CI/CD pipeline requires validation

New Syntax to Learn

Concern: .states.flow format is a new syntax.

Solutions:

  • EventFlow native syntax, not YAML
  • CLI generates it, usually not written manually
  • Only editing state names and outcomes

Documentation Updates Required

Priority 1: Core Changes

FileChange
guide/derive-states.mdComplete rewrite with new workflow
guide/actions.mdRemove moves to, add derive reference
reference/syntax/events-actions.mdUpdate state transition syntax
NEW: reference/syntax/states-file.mdDocument .states.flow format

Priority 2: Workflow Updates

FileChange
guide/workflow/session-4-implementation.mdDetailed derive step
guide/workflow/overview.mdAdd state derivation phase
guide/workflow/session-5-review.mdState review section

Priority 3: CLI Reference

FileChange
reference/cli/utility-commands.mdderive-states command
reference/cli/overview.mdUpdate command list

Priority 4: Examples

FileChange
guide/getting-started.mdRemove moves to, add derive
examples/e-commerce.mdAdd .states.flow example
examples/job-application.mdAdd .states.flow example
All files with moves toRemove inline transitions

Open Questions

  1. Backward Compatibility: How will existing moves to flows be migrated?
  2. IDE Support: How will VSCode extension display state derivation?
  3. Testing: How will state references work in test assertions?
  4. Diagram Integration: Which file will state diagrams be generated from?

Released under the MIT License.