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.