Skip to content

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:

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 ​

  1. reply keyword - Natural language response mechanism with HTTP status
  2. Inline responses - Define response structure where you use it
  3. Reusable responses - Optional named responses for repeated patterns
  4. Field mapping - Transform context to API-friendly output
  5. 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:

flow
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:

flow
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:

StatusMeaningWhen to Use
200OKSuccessful read/update
201CreatedNew resource created
202AcceptedAsync operation started
400Bad RequestValidation error
404Not FoundResource doesn't exist
409ConflictState conflict
500Internal ErrorUnexpected failure

3.4 Human-Readable Status Codes ​

Status codes can optionally include their meaning for readability:

flow
// 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:

flow
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 ​

flow
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 ​

flow
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 $total variable's type is understood from how it's used in the flow (e.g., $total: number is 0 in given block, or $total increases by $amount).

4.2 Using a Named Response ​

flow
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_error

4.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:

flow
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):

flow
reply 200 with:
  | orderId     | $order_id     |  // camelCase for JavaScript clients
  | orderTotal  | $total        |
  | orderStatus | current_state |

5.3 Computed Fields ​

Use expressions to compute values:

flow
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:

flow
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:

flow
on :get_order_report from @admin (api)
  reply 200 with ^order_report

The ^ prefix indicates this is a binding transformer, not a named response. The transformer is a class in your binding language (PHP, JS, etc.):

php
#[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:

flow
reply 200 with:
  | id               | $order_id       |
  | customer.name    | $customer_name  |
  | customer.email   | $customer_email |
  | shipping.address | $ship_address   |
  | shipping.method  | $ship_method    |

Output:

json
{
  "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:

flow
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:

json
{
  "id": "order-123",
  "items": [
    { "name": "Widget", "price": 29.99 },
    { "name": "Gadget", "price": 49.99 }
  ]
}

For dynamic arrays, use the context variable directly:

flow
reply 200 with:
  | id    | $order_id |
  | items | $items    |

5.8 JSON:API Style Responses ​

EventFlow supports JSON:API specification style responses:

flow
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:

json
{
  "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:

flow
on :update_notes from @customer (api)
  $notes becomes $new_notes
  // No explicit reply - returns default

Default Response:

json
{
  "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:

json
{
  "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:

flow
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:

flow
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:

  1. API request comes in → immediate 202 Accepted response
  2. Work is queued via emit :process_report to @queue
  3. Queue processes the work asynchronously
  4. On completion, send callback for :generate_report notifies the original caller

7.4 Async Options ​

OptionDescriptionDefault
callback:Callback type: websocket, webhook, pollingwebsocket
channel:WebSocket channel name"{event_name}." + id
url:Webhook URLRequired when callback: webhook

7.5 Callback Types ​

WebSocket (Default) - Server pushes to channel when complete:

flow
// 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:

flow
reply 202 async for :process_order:
  callback: webhook
  url: $callback_url
  | job_id | $job_id  |
  | status | "queued" |

Polling - Client polls status endpoint:

flow
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:

flow
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:

  1. User A calls :generate_report → conversation conv-001
  2. User B calls :generate_report → conversation conv-002
  3. User B's report finishes first
  4. Which client receives the callback?

The Solution - Conversation + Event Scoped Callbacks:

The runtime automatically scopes callback configurations:

  1. Registration: When reply 202 async for :event executes, the runtime stores (conversation_id, event_name) → callback_config
  2. Delivery: When send callback for :event executes, the runtime finds the matching config using current conversation + event name
  3. Cleanup: After successful delivery, the config is removed
flow
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 ​

flow
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:

flow
reply 400 bad request with:
  | error   | $error_code    |
  | message | $error_message |

Output:

json
{
  "error": "VALIDATION_FAILED",
  "message": "Cart validation failed"
}

JSON:API style (using dot notation for nesting):

flow
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:

json
{
  "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 ​

flow
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:

flow
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 path

This 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:

flow
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:

flow
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 ​

flow
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 ​

flow
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 empty

9.5 Testing Async Responses ​

flow
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 conversation

9.6 Testing Binding Transformers ​

flow
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 empty

9.7 Reply Assertion Syntax ​

AssertionDescription
= reply status is NHTTP status code
= reply.field equals valueField has exact value
= reply.field is not emptyField exists and not null/empty
= reply.nested.field equals valueNested field check (dot notation)
= reply contains fieldField exists in response
= reply does not contain fieldField absent from response
= reply.field is greater than NNumeric comparison
= callback for :event was sentAsync callback delivered for event
= callback for :event was sent to current conversationCallback targeted correctly
= callback.field equals valueCallback payload check

10. Complete Example ​

10.1 Order Machine with Responses ​

flow
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_notification

10.2 Test File ​

flow
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 #paid

10.3 Async Report Example ​

flow
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 ​

KeywordPurposeExample
reply STATUS with:Inline responsereply 201 with:
reply STATUS MEANING with:With human-readable statusreply 400 bad request with:
reply STATUS with nameNamed responsereply 200 with order_details
reply STATUS async for :event:Async responsereply 202 async for :generate_report:

11.2 Response Definition ​

KeywordPurposeExample
response:Define reusable responseresponse: 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 |
^nameBinding transformerreply 200 with ^order_report

11.3 Async Options ​

KeywordPurposeExample
for :eventTarget event for async replyreply 202 async for :generate_report:
callback:Callback type (default: websocket)callback: webhook
channel:WebSocket channel (default: auto)channel: "reports." + $id
url:Webhook URLurl: $callback_url
send callback for :event:Send async resultsend callback for :generate_report:

11.4 Test Assertions ​

AssertionPurpose
= reply status is NCheck HTTP status
= reply.field equals valueCheck field value
= reply.field is not emptyCheck field exists
= reply contains fieldCheck field present
= reply does not contain fieldCheck field absent
= callback was sentCheck async callback
= callback.field equals valueCheck 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:

  1. Build response object based on field mapping
  2. Evaluate all expressions
  3. Apply conditional field logic
  4. Serialize to JSON
  5. Set HTTP status code

12.2 Default Response ​

When no reply statement:

json
{
  "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 reply as 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:

  1. Identify API events - Which events need responses?
  2. Define success responses - What data does the caller need?
  3. Define error responses - What can go wrong?
  4. Add tests - Verify response structure
flow
// 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

Released under the MIT License.