EventFlow Machine Response Proposal
Structured Responses for API Event Handlers
Version: Draft 0.3 Date: December 2024 Status: ✅ Implemented
This proposal has been implemented and integrated into the main documentation.
See the following documentation pages:
- Machine Responses Guide - Complete guide
- Symbols Reference -
^binding transformer- PHP Binding - ResponseTransformer
- Assertions - Response Assertions
This proposal is kept as an archive for reference.
1. Executive Summary
EventFlow machines receive events and emit events to communicate - but they don't currently have a structured way to return responses to API callers. This proposal introduces Machine Response - a natural language approach to defining and returning structured responses from API event handlers.
Core Philosophy
Events tell the story. Replies answer the caller.
Machine does the work. Response shapes the answer.
Key Features
replykeyword - Natural language response mechanism with HTTP status- Inline responses - Define response structure where you use it
- Reusable responses - Optional named responses for repeated patterns
- Field mapping - Transform context to API-friendly output
- Async support - Webhook, WebSocket, and polling callbacks
Note: For event queue configuration (ordering, retry, dead letter queues), see EVENT_QUEUE_PROPOSAL.md.
2. Motivation & Problem Statement
2.1 Current Situation
EventFlow machines can:
- Receive events with data:
on :checkout from @customer (api) - Emit events with data:
emit :payment_request to @payment with $order_id - Store data in context:
$total becomes 100
But they cannot:
- Return structured responses to API callers
- Shape response data (field mapping, computed fields)
- Indicate success/failure with proper HTTP semantics
- Handle long-running operations asynchronously
2.2 Real-World Need
When exposing a state machine as an API:
on :checkout from @customer (api)
$order_id becomes uuid()
order moves to #awaiting_payment
emit :payment_request to @payment
// What does the API caller receive back?The caller needs to know:
- Was the request successful?
- What's the order ID?
- What's the current state?
- When will they hear back?
3. Reply Syntax
3.1 Basic Reply with Inline Response
Every API event handler should end with a reply statement. The HTTP status code is required:
on :checkout from @customer (api)
$order_id becomes uuid()
order moves to #awaiting_payment
reply 201 with:
| id | $order_id |
| status | current_state |
| total | $total |3.2 Syntax Format
reply <HTTP_STATUS> with:
| <field_name> | <source> |
| <field_name> | <source> | <type> |
| <field_name> | <source> | <type> | <condition> |3.3 HTTP Status Codes
Common status codes:
| Status | Meaning | When to Use |
|---|---|---|
200 | OK | Successful read/update |
201 | Created | New resource created |
202 | Accepted | Async operation started |
400 | Bad Request | Validation error |
404 | Not Found | Resource doesn't exist |
409 | Conflict | State conflict |
500 | Internal Error | Unexpected failure |
3.4 Human-Readable Status Codes
Status codes can optionally include their meaning for readability:
// Both are valid - choose based on audience
reply 400 with:
| error | "CART_EMPTY" |
reply 400 bad request with:
| error | "CART_EMPTY" |When using the descriptive form:
reply 201 created with:
| id | $order_id |
reply 202 accepted with:
| job_id | $job_id |
reply 404 not found with:
| error | "ORDER_NOT_FOUND" |Note: Lane diagram generators automatically display the human-readable form (e.g.,
400 Bad Request) regardless of how it's written in the flow file. This ensures diagrams are always accessible to non-technical stakeholders.
3.5 Reply with Error
on :checkout from @customer (api)
? cart is empty
reply 400 with:
| error | "CART_EMPTY" |
| message | "Cannot checkout empty cart" |
? cart is valid
$order_id becomes uuid()
order moves to #awaiting_payment
reply 201 with:
| id | $order_id |
| status | current_state |
otherwise
reply 400 with:
| error | "INVALID_CART" |
| message | "Cart validation failed" |4. Reusable Response Definitions (Optional)
For responses used in multiple places, define them at machine level:
4.1 Defining a Reusable Response
machine: @order
response: order_created
| field | from |
| id | $order_id |
| status | current_state |
| total | $total |
| created_at | $created_at |
response: order_error
| field | from |
| error | $error_code |
| message | $error_message |Note: Types are inferred from context usage. The
$totalvariable's type is understood from how it's used in the flow (e.g.,$total: number is 0in given block, or$total increases by $amount).
4.2 Using a Named Response
on :checkout from @customer (api)
? cart is valid
$order_id becomes uuid()
order moves to #awaiting_payment
reply 201 with order_created
otherwise
$error_code becomes "INVALID_CART"
$error_message becomes "Cart validation failed"
reply 400 with order_error
on :get_order from @customer (api)
? order exists
reply 200 with order_created // Reuse same response
otherwise
$error_code becomes "NOT_FOUND"
$error_message becomes "Order not found"
reply 404 with order_error4.3 When to Use Named Responses
- Inline: Default choice, simple one-off responses
- Named: When the same response structure is used 2+ times
5. Field Mapping Features
5.1 Simple Mapping
Direct context-to-field mapping:
reply 200 with:
| order_id | $order_id |
| total | $total |
| state | current_state|5.2 Field Renaming
Map to differently-named fields (useful for API conventions):
reply 200 with:
| orderId | $order_id | // camelCase for JavaScript clients
| orderTotal | $total |
| orderStatus | current_state |5.3 Computed Fields
Use expressions to compute values:
reply 200 with:
| id | $order_id |
| display_total | "$" + format($total) |
| item_count | count($items) |
| tax_amount | $total * 0.1 |
| is_large | $total > 1000 |5.4 Conditional Fields
Fields that appear only when a condition is met:
reply 200 with:
| id | $order_id | |
| total | $total | |
| discount | $discount | when $discount > 0 |
| tracking_no | $tracking_no | when order is in #shipped |5.5 Complex Transformations via Bindings
For complex response transformations that are hard to express in EventFlow syntax, use language bindings with the ^ (binding) prefix:
on :get_order_report from @admin (api)
reply 200 with ^order_reportThe ^ prefix indicates this is a binding transformer, not a named response. The transformer is a class in your binding language (PHP, JS, etc.):
#[ResponseTransformer('order_report')]
class OrderReportTransformer {
public function transform(Context $context): array {
return [
'summary' => $this->buildSummary($context),
'charts' => $this->generateChartData($context),
'export_formats' => ['pdf', 'csv', 'xlsx'],
];
}
}This allows complex business logic while keeping flow files clean and readable.
5.6 Nested Objects
Use dot notation for nested structures:
reply 200 with:
| id | $order_id |
| customer.name | $customer_name |
| customer.email | $customer_email |
| shipping.address | $ship_address |
| shipping.method | $ship_method |Output:
{
"id": "order-123",
"customer": {
"name": "John Doe",
"email": "john@example.com"
},
"shipping": {
"address": "123 Main St",
"method": "express"
}
}Dot notation keeps the table structure clean and symmetric while supporting arbitrary nesting depth (e.g., order.customer.address.city).
5.7 Array Notation
Use bracket notation for arrays:
reply 200 with:
| id | $order_id |
| items[0].name | $first_item_name |
| items[0].price | $first_item_price |
| items[1].name | $second_item_name |
| items[1].price | $second_item_price |Output:
{
"id": "order-123",
"items": [
{ "name": "Widget", "price": 29.99 },
{ "name": "Gadget", "price": 49.99 }
]
}For dynamic arrays, use the context variable directly:
reply 200 with:
| id | $order_id |
| items | $items |5.8 JSON:API Style Responses
EventFlow supports JSON:API specification style responses:
reply 200 with:
| data.type | "orders" |
| data.id | $order_id |
| data.attributes.total | $total |
| data.attributes.state | current_state |
| data.relationships.customer.data.type | "customers" |
| data.relationships.customer.data.id | $customer_id |Output:
{
"data": {
"type": "orders",
"id": "order-123",
"attributes": {
"total": 150.00,
"state": "awaiting_payment"
},
"relationships": {
"customer": {
"data": { "type": "customers", "id": "cust-456" }
}
}
}
}6. Default Reply Behavior
6.1 Single Machine
API events without explicit reply return an acknowledgment:
on :update_notes from @customer (api)
$notes becomes $new_notes
// No explicit reply - returns defaultDefault Response:
{
"success": true,
"aggregate_id": "order-abc123",
"state": "processing"
}6.2 Machine Systems
In a machine system, the default reply includes the conversation ID and all machine states:
{
"success": true,
"conversation_id": "conv-abc123",
"machines": {
"@order": "awaiting_payment",
"@payment": "pending"
}
}The conversation_id is the primary identifier for tracking the entire flow. Individual machine states are included for detailed status. To interact with a specific machine instance, use the conversation_id - the runtime resolves the correct aggregate internally.
6.3 Best Practice
Always use explicit reply statements for clarity. Default replies are a fallback, not a feature.
7. Async Responses
7.1 When to Use Async
Use async responses when:
- Processing takes longer than acceptable API timeout
- Client needs to be notified when complete
- Work happens in background
7.2 Async Reply Syntax
Use reply 202 async for :event_name to indicate which event this async response is for:
on :generate_report from @admin (api)
$report_id becomes uuid()
report moves to #queued
reply 202 async for :generate_report:
| report_id | $report_id |
| status | "queued" |This automatically creates a WebSocket channel named {event_name}.{id} (e.g., generate_report.rpt-123).
7.3 Queued Event Processing
For operations that go through a queue before processing:
on :generate_report from @admin (api)
$report_id becomes uuid()
report moves to #queued
reply 202 async for :generate_report:
| report_id | $report_id |
| status | "queued" |
emit :process_report to @queue
with:
| report_id | $report_id |
on :process_report from @queue
report moves to #generating
// ... do the work
on :report_compiled
report moves to #ready
send callback for :generate_report:
| report_id | $report_id |
| status | "completed" |The flow:
- API request comes in → immediate
202 Acceptedresponse - Work is queued via
emit :process_report to @queue - Queue processes the work asynchronously
- On completion,
send callback for :generate_reportnotifies the original caller
7.4 Async Options
| Option | Description | Default |
|---|---|---|
callback: | Callback type: websocket, webhook, polling | websocket |
channel: | WebSocket channel name | "{event_name}." + id |
url: | Webhook URL | Required when callback: webhook |
7.5 Callback Types
WebSocket (Default) - Server pushes to channel when complete:
// Minimal - uses defaults (websocket, auto-channel)
reply 202 async for :generate_report:
| report_id | $report_id |
// Explicit channel override
reply 202 async for :generate_report:
channel: "reports." + $report_id
| report_id | $report_id |Webhook - Server POSTs to client URL when complete:
reply 202 async for :process_order:
callback: webhook
url: $callback_url
| job_id | $job_id |
| status | "queued" |Polling - Client polls status endpoint:
reply 202 async for :export_data:
callback: polling
| operation_id | $operation_id |
| status | "queued" |
| poll_url | "/status/" + $operation_id |7.6 Async Completion
When the async operation completes, use send callback for :event_name to deliver the result:
on :report_compiled
$download_url becomes generate_url($report_id)
report moves to #ready
send callback for :generate_report:
| report_id | $report_id |
| status | "completed" |
| url | $download_url |7.7 How Callback Tracking Works
The runtime tracks async callbacks using conversation ID + event name. This ensures correct delivery even with concurrent requests.
The Problem:
Consider this scenario:
- User A calls
:generate_report→ conversationconv-001 - User B calls
:generate_report→ conversationconv-002 - User B's report finishes first
- Which client receives the callback?
The Solution - Conversation + Event Scoped Callbacks:
The runtime automatically scopes callback configurations:
- Registration: When
reply 202 async for :eventexecutes, the runtime stores(conversation_id, event_name) → callback_config - Delivery: When
send callback for :eventexecutes, the runtime finds the matching config using current conversation + event name - Cleanup: After successful delivery, the config is removed
on :generate_report from @admin (api)
$report_id becomes uuid()
report moves to #queued
// Runtime stores: (conv-001, :generate_report) → {websocket, channel}
reply 202 async for :generate_report:
| report_id | $report_id |
| status | "queued" |
on :report_compiled
report moves to #ready
// Runtime looks up: (current_conversation, :generate_report)
send callback for :generate_report:
| report_id | $report_id |
| status | "completed" |Concurrent Requests Example:
Timeline:
─────────────────────────────────────────────────────────────
User A: :generate_report → conv-001 → 202 accepted
User B: :generate_report → conv-002 → 202 accepted
↓
User B's report completes first (conv-002)
↓
send callback for :generate_report → delivered to User B ✓
↓
User A's report completes (conv-001)
↓
send callback for :generate_report → delivered to User A ✓Each conversation maintains its own callback state per event, so responses always go to the correct client.
8. Error Handling
8.1 Structured Error Responses
on :checkout from @customer (api)
? cart is empty
reply 400 with:
| error | "CART_EMPTY" |
| message | "Cannot checkout empty cart" |
? cart is valid
// ... success path
otherwise
reply 400 with:
| error | "VALIDATION_FAILED" |
| message | "Cart validation failed" |
| details | $validation_errors |8.2 Error Response Patterns
You can structure error responses to match your API standards.
Simple flat structure:
reply 400 bad request with:
| error | $error_code |
| message | $error_message |Output:
{
"error": "VALIDATION_FAILED",
"message": "Cart validation failed"
}JSON:API style (using dot notation for nesting):
reply 400 bad request with:
| errors[0].code | $error_code |
| errors[0].title | $error_title |
| errors[0].detail | $error_detail |
| errors[0].source.pointer | $error_field |Output:
{
"errors": [
{
"code": "CART_EMPTY",
"title": "Validation Error",
"detail": "Cannot checkout with an empty cart",
"source": {
"pointer": "/data/attributes/cart"
}
}
]
}The choice of error format is yours - EventFlow provides the building blocks.
8.3 Guard-Based Error Handling
on :checkout from @customer (api)
? cart is empty
reply 400 with:
| error | "CART_EMPTY" |
? not customer is logged in
reply 401 with:
| error | "UNAUTHORIZED" |
? cart total exceeds limit
reply 400 with:
| error | "LIMIT_EXCEEDED" |
| limit | $max_order_total |
// All validations passed
$order_id becomes uuid()
order moves to #awaiting_payment
reply 201 with:
| id | $order_id |8.4 Named Responses for Repeated Errors
When the same error structure is used across multiple handlers, define a named response:
machine: @order
response: validation_error
| field | from |
| error | $error_code |
| message | $error_message |
on :checkout from @customer (api)
? cart is empty
$error_code becomes "CART_EMPTY"
$error_message becomes "Cannot checkout empty cart"
reply 400 with validation_error
// ... success path
on :add_item from @customer (api)
? product is unavailable
$error_code becomes "UNAVAILABLE"
$error_message becomes "Product is not available"
reply 400 with validation_error // Reuse same response
// ... success pathThis keeps error responses consistent across the machine and reduces duplication.
9. Testing Responses
9.1 Response Assertions in Flow Files
Test response in happy path scenarios:
scenario: checkout flow
on :checkout from @customer (api)
$order_id becomes uuid()
order moves to #awaiting_payment
reply 201 with:
| id | $order_id |
| status | current_state |
expect:
= order is in #awaiting_payment
= reply status is 201
= reply.id is not empty
= reply.status equals "awaiting_payment"9.2 Response Assertions in Test Files
Test variations and error cases:
test: @order
for scenario: checkout flow
for :checkout:
// Success case
returns order on success:
= reply status is 201
= reply.id is not empty
= reply.status equals "awaiting_payment"
// Error cases
returns 400 for empty cart:
with scenario:
cart is empty
= reply status is 400
= reply.error equals "CART_EMPTY"
returns 400 for invalid cart:
assume:
? cart is valid = false
= reply status is 400
= reply.error equals "VALIDATION_FAILED"
returns 401 for guest user:
with scenario:
customer is not logged in
= reply status is 401
= reply.error equals "UNAUTHORIZED"9.3 Testing Nested and Conditional Fields
test: @order
for scenario: checkout flow
for :get_order:
returns nested customer data:
= reply status is 200
= reply.customer.name is not empty
= reply.customer.email is not empty
includes tracking only when shipped:
with context:
order is in #processing
= reply status is 200
= reply does not contain tracking
includes tracking when shipped:
with context:
order is in #shipped
$tracking_no is "TRACK123"
= reply status is 200
= reply.tracking equals "TRACK123"9.4 Testing Named Responses
test: @order
for scenario: checkout flow
for :checkout:
uses named error response:
with scenario:
cart is empty
= reply status is 400
= reply.error equals "CART_EMPTY"
= reply.message is not empty9.5 Testing Async Responses
test: @report_generator
for scenario: generate report
for :generate_report:
returns 202 accepted with tracking:
= reply status is 202
= reply.report_id is not empty
= reply.status equals "queued"
sends callback on completion:
after :generate_report
receive :report_compiled
= callback for :generate_report was sent
= callback.status equals "completed"
= callback.url is not empty
callback targets correct client:
// Verifies conversation-scoped delivery
after :generate_report
receive :report_compiled
= callback for :generate_report was sent to current conversation9.6 Testing Binding Transformers
test: @report_generator
for scenario: generate report
for :get_report_summary:
returns transformed response:
= reply status is 200
= reply.summary is not empty
= reply.charts is not empty9.7 Reply Assertion Syntax
| Assertion | Description |
|---|---|
= reply status is N | HTTP status code |
= reply.field equals value | Field has exact value |
= reply.field is not empty | Field exists and not null/empty |
= reply.nested.field equals value | Nested field check (dot notation) |
= reply contains field | Field exists in response |
= reply does not contain field | Field absent from response |
= reply.field is greater than N | Numeric comparison |
= callback for :event was sent | Async callback delivered for event |
= callback for :event was sent to current conversation | Callback targeted correctly |
= callback.field equals value | Callback payload check |
10. Complete Example
10.1 Order Machine with Responses
machine: @order
// Reusable response for order errors
response: order_error
| field | from |
| error | $error_code |
| message | $error_message |
scenario: checkout flow
given:
@customer is logged in
cart has items
$total: number is 0
on :add_item from @customer (api)
$items adds $product
$total increases by $product.price
reply 200 with:
| item_count | count($items) |
| total | $total |
on :checkout from @customer (api)
? cart is empty
reply 400 with:
| error | "CART_EMPTY" |
| message | "Cannot checkout empty cart" |
? cart is valid
$order_id becomes uuid()
$created_at becomes now
order moves to #awaiting_payment
emit :payment_request to @payment
with:
| order_id | $order_id |
| amount | $total |
reply 201 with:
| id | $order_id |
| status | current_state |
| total | $total |
| item_count | count($items) |
| created_at | $created_at |
expect:
= order is in #awaiting_payment
= reply status is 201
= reply.id equals $order_id
otherwise
reply 400 with:
| error | "INVALID_CART" |
| message | "Cart validation failed" |
on :get_order from @customer (api)
? order exists
reply 200 with:
| id | $order_id | |
| status | current_state | |
| total | $total | |
| items | $items | |
| customer.name | $customer_name | |
| customer.email | $customer_email | |
| tracking | $tracking_no | when order is in #shipped |
otherwise
$error_code becomes "NOT_FOUND"
$error_message becomes "Order not found"
reply 404 with order_error
on :payment_success from @payment
order moves to #paid
emit :reserve_stock to @inventory
on :ship from @warehouse
$tracking_no becomes $tracking
order moves to #shipped
emit :shipment_notification to @customer
expect:
= order is in #shipped
= @customer received :shipment_notification10.2 Test File
test: @order
for scenario: checkout flow
// Test :add_item responses
for :add_item:
returns updated cart:
= reply status is 200
= reply.item_count is greater than 0
= reply.total is greater than 0
// Test :checkout responses
for :checkout:
successful checkout:
= reply status is 201
= reply.id is not empty
= reply.status equals "awaiting_payment"
= reply.total equals 1200
= reply.item_count equals 3
empty cart error:
with scenario:
cart is empty
= reply status is 400
= reply.error equals "CART_EMPTY"
invalid cart error:
assume:
? cart is valid = false
= reply status is 400
= reply.error equals "INVALID_CART"
// Test :get_order responses
for :get_order:
returns order details:
= reply status is 200
= reply.id equals $order_id
= reply contains customer
= reply does not contain tracking // Not shipped yet
includes tracking when shipped:
with context:
order is in #shipped
$tracking_no is "TRACK123"
= reply status is 200
= reply.tracking equals "TRACK123"
returns 404 for missing order:
assume:
? order exists = false
= reply status is 404
= reply.error equals "NOT_FOUND"
// Path divergence tests
payment fails after checkout:
after :checkout
receive :payment_failed from @payment
= order is in #payment_failed
payment retry then success:
after :checkout
receive :payment_failed from @payment
then :payment_success from @payment
= order is in #paid10.3 Async Report Example
machine: @report_generator
scenario: generate sales report
on :generate_report from @admin (api)
$report_id becomes uuid()
report moves to #queued
reply 202 async for :generate_report:
| report_id | $report_id |
| status | "queued" |
| message | "Report generation started" |
emit :process_report to @queue
with:
| report_id | $report_id |
on :process_report from @queue
report moves to #generating
generate_sales_data
compile_statistics
create_pdf
on :report_compiled
$download_url becomes generate_signed_url($report_id)
$expires_at becomes now + 24 hours
report moves to #ready
send callback for :generate_report:
| report_id | $report_id |
| status | "completed" |
| url | $download_url |
| expires_at | $expires_at |11. Keywords Reference
11.1 Reply Syntax
| Keyword | Purpose | Example |
|---|---|---|
reply STATUS with: | Inline response | reply 201 with: |
reply STATUS MEANING with: | With human-readable status | reply 400 bad request with: |
reply STATUS with name | Named response | reply 200 with order_details |
reply STATUS async for :event: | Async response | reply 202 async for :generate_report: |
11.2 Response Definition
| Keyword | Purpose | Example |
|---|---|---|
response: | Define reusable response | response: order_created |
| field | from | | Field mapping | | id | $order_id | |
| field.nested | from | | Nested field (dot notation) | | customer.name | $name | |
| field[n].prop | from | | Array element | | items[0].name | $name | |
| field | from | when | | Conditional field | | discount | $d | when $d > 0 | |
^name | Binding transformer | reply 200 with ^order_report |
11.3 Async Options
| Keyword | Purpose | Example |
|---|---|---|
for :event | Target event for async reply | reply 202 async for :generate_report: |
callback: | Callback type (default: websocket) | callback: webhook |
channel: | WebSocket channel (default: auto) | channel: "reports." + $id |
url: | Webhook URL | url: $callback_url |
send callback for :event: | Send async result | send callback for :generate_report: |
11.4 Test Assertions
| Assertion | Purpose |
|---|---|
= reply status is N | Check HTTP status |
= reply.field equals value | Check field value |
= reply.field is not empty | Check field exists |
= reply contains field | Check field present |
= reply does not contain field | Check field absent |
= callback was sent | Check async callback |
= callback.field equals value | Check callback payload |
Note: For queue-related keywords, see EVENT_QUEUE_PROPOSAL.md.
12. Implementation Notes
12.1 Response Serialization
Responses are serialized to JSON by default. The runtime should:
- Build response object based on field mapping
- Evaluate all expressions
- Apply conditional field logic
- Serialize to JSON
- Set HTTP status code
12.2 Default Response
When no reply statement:
{
"success": true,
"aggregate_id": "<instance-id>",
"state": "<current-state>"
}12.3 PHP Binding Considerations
The PHP binding should:
- Parse response definitions during machine registration
- Implement
replyas a return mechanism in event handlers - Integrate with Laravel/Symfony HTTP layer for status codes
- Support custom transformer classes for complex responses
Note: For queue implementation notes, see EVENT_QUEUE_PROPOSAL.md.
13. Summary
┌─────────────────────────────────────────────────────────────┐
│ │
│ Reply Syntax │
│ ──────────── │
│ - reply STATUS with: (inline response) │
│ - reply STATUS with name (named response) │
│ - reply STATUS async: (async with callback) │
│ - HTTP status always required │
│ │
│ Response Features │
│ ───────────────── │
│ - Inline responses for one-off use │
│ - Named responses for reuse │
│ - Field mapping, computed fields, conditionals │
│ - Nested objects via dot notation │
│ - Binding transformers (^prefix) for complex logic │
│ │
│ Async Support │
│ ───────────── │
│ - reply 202 async for :event_name │
│ - WebSocket (default), Webhook, Polling callbacks │
│ - Auto-generated channels ({event_name}.{id}) │
│ - Conversation + event scoped callback tracking │
│ - send callback for :event_name │
│ │
│ Testing │
│ ─────── │
│ - = reply status is N │
│ - = reply.field equals value │
│ - = callback was sent │
│ │
└─────────────────────────────────────────────────────────────┘Philosophy
Events tell the story. Replies answer the caller.
Machine does the work. Response shapes the answer.
14. Design Workflow
Response design is primarily a developer task during implementation. Here's a typical workflow:
14.1 Discovery Phase (Team)
During discovery sessions, the team focuses on:
- What events the machine receives
- What state transitions happen
- How machines communicate
Lane diagrams capture the flow, not the response details:
@customer @order @payment
| | |
|-- :checkout --> | |
| |-- :payment_req --> |
| |<-- :payment_ok --- |
|<-- :confirmed --| |14.2 Implementation Phase (Developer)
When implementing, the developer designs responses:
- Identify API events - Which events need responses?
- Define success responses - What data does the caller need?
- Define error responses - What can go wrong?
- Add tests - Verify response structure
// Developer adds response details during implementation
on :checkout from @customer (api)
? cart is empty
reply 400 with:
| error | "CART_EMPTY" |
$order_id becomes uuid()
order moves to #awaiting_payment
reply 201 with:
| id | $order_id |
| status | current_state |14.3 Why This Split?
- Discovery: Focuses on business flow, readable by non-developers
- Implementation: Adds technical details like response structure
- Responses are API contracts: Developers understand HTTP semantics and client needs
15. Related Proposals
- Event Queue Proposal - Reliable, ordered event processing
- Test Scenarios Proposal - Delta-based testing