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
:checkoutand: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,:subtractoperations).The
moves to #statesyntax remains the explicit way to define process flow.
Problem Statement
EventFlow's current state definition approach has inconsistencies:
moves torepetition: Every event handler writesorder moves to #state, duplicating states- Conflicts with lane diagram philosophy: Sessions 1-3 decided not to mention states, but flow files have inline state transitions
- Outcome ambiguity: Terminal states don't explicitly mark success/failure/neutral status
- Mixed event flow and state structure: Same file contains both event communication and state transitions
Current Approach
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 handlerProblems:
#awaiting_paymentcan 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)
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.flowGenerated States File
// 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_receivedState Derivation Algorithm
The core principle of state derivation: Every wait point is a state.
Derivation Rules
| Situation | State Type | Description |
|---|---|---|
| Before event handler | initial | No event received yet |
| After handler, expecting another event | waiting | Waiting for next event |
After emit async, expecting response | waiting | Background process started |
| Final action, no more handlers expected | terminal | Process ended |
State Naming Rules
State names should reflect what is being waited for, not how we got there:
| Pattern | Example | Description |
|---|---|---|
#awaiting_{expected} | #awaiting_payment | Waiting for a response/event |
#processing_{what} | #processing_order | Async operation in progress |
#{outcome} | #confirmed, #cancelled | Terminal state, indicates outcome |
#{what}_failed | #payment_failed | Waiting after error |
Example 1: Single Machine - Simple Linear Flow
The simplest case: one machine with sequential events, no branching, no external actors.
Flow File
machine: @order
on :place_order (api)
validate order details
calculate total
on :confirm_order (api)
finalize order
generate receiptStep-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
// 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_orderExample 2: Single Machine - Guarded Multi-Path Transitions
One machine with conditional branching based on guards.
Flow File
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 rejectedStep-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
// 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 cancellableExample 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
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 metricsState 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 Type | Creates Wait Point? | Description |
|---|---|---|
emit :event (sync) | NO | Executes inline, result returned in same request |
emit async :event | NO (fire-forget) | Background execution, no response expected |
emit async :event + handler | YES | If there's a handler waiting for response |
(queued api) modifier | YES | Event queued, handler runs in background |
reply 202 async for | YES | Callback will be sent later |
Generated States File
// 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_analyticsKey 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
// ─── 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// ─── 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_codeCross-Machine Communication Pattern:
@ordersends:request_paymentto@payment@paymentprocesses and sends:payment_successor:payment_failedback to@order@orderhandles 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)
// 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 @paymentExample 4b: Three Machines - Chained Communication
// ─── 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// ─── 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// ─── 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 @orderState 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)
// 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 @paymentExample 4c: Parallel Multi-Actor Communication
// ─── 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 @customerParallel 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)
// 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 failedState Naming: Multi-Event Paths
Problem
In paths with 3-5 events, generated names become too long:
#after_checkout_payment_success_stock_reserved_shipping_scheduledSolution: Semantic Naming
Instead of event lists, use names that describe the current situation:
| Generated (Bad) | Semantic (Good) | Why? |
|---|---|---|
#after_checkout | #awaiting_payment | Describes what we're waiting for |
#after_payment_success | #preparing_shipment | Describes what's happening |
#after_payment_and_stock | #ready_to_ship | Describes current status |
#after_all_events | #completed | Describes 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:
$ 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/orderDiagram 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:
$ 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:
$ 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.flowValidate States
Check flow and states file consistency:
$ 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 --updateState File Syntax
State Types
| Type | Meaning | Required Fields |
|---|---|---|
initial | First state before any event | - |
waiting | Waiting for external response | after, waiting_for |
terminal | Final state, no more events | outcome |
Outcome Values
| Outcome | Meaning | Diagram Color |
|---|---|---|
success | Happy path completion | Green |
failure | Error/rejection | Red |
neutral | Neither success nor failure | Gray |
Full State Syntax
#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 // OptionalMigration Path
Phase 1: Introduce .states.flow
- Add
eventflow derive-statescommand - Generate
.states.flowfrom existing flows - Both
moves toand.states.flowwork together
Phase 2: Deprecate moves to
- Warn when
moves toused but.states.flowexists - Update documentation to use implicit style
moves tostill works but shows deprecation warning
Phase 3: Remove moves to
moves tosyntax removed- All flows must use
.states.flow - Migration tool converts old flows
Benefits
| Benefit | Description |
|---|---|
| DRY | States defined in one place, no repetition |
| Separation of Concerns | Event flow and state structure separate |
| Explicit Outcomes | Terminal states marked as success/failure |
| Tool Support | CLI derivation, validation, diagram generation |
| Lane Diagram Aligned | Flow files contain only event communication |
Potential Concerns
Readability
Concern: Hard to understand flow without seeing state transitions.
Solutions:
eventflow diagramcommand always available- IDE extension can show states as annotations
.states.flowfile always alongside
Two File Management
Concern: Hard to keep main flow and states file in sync.
Solutions:
eventflow validatedetects inconsistencieseventflow derive-states --updateauto-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
| File | Change |
|---|---|
guide/derive-states.md | Complete rewrite with new workflow |
guide/actions.md | Remove moves to, add derive reference |
reference/syntax/events-actions.md | Update state transition syntax |
NEW: reference/syntax/states-file.md | Document .states.flow format |
Priority 2: Workflow Updates
| File | Change |
|---|---|
guide/workflow/session-4-implementation.md | Detailed derive step |
guide/workflow/overview.md | Add state derivation phase |
guide/workflow/session-5-review.md | State review section |
Priority 3: CLI Reference
| File | Change |
|---|---|
reference/cli/utility-commands.md | derive-states command |
reference/cli/overview.md | Update command list |
Priority 4: Examples
| File | Change |
|---|---|
guide/getting-started.md | Remove moves to, add derive |
examples/e-commerce.md | Add .states.flow example |
examples/job-application.md | Add .states.flow example |
All files with moves to | Remove inline transitions |
Open Questions
- Backward Compatibility: How will existing
moves toflows be migrated? - IDE Support: How will VSCode extension display state derivation?
- Testing: How will state references work in test assertions?
- Diagram Integration: Which file will state diagrams be generated from?
Related Proposals
- Analytics Proposal - Uses
success:andfailure:for funnel analysis - Static Analysis Proposal - State reachability analysis