Conversation Context
When machines communicate, the system automatically maintains a conversation context that enables response routing without explicit correlation keys.
The Problem
When @order sends an event to @payment, and @payment responds, how does the response know which specific order instance to return to?
// @order emits to @payment
emit :payment_request to @payment
// Later, @payment responds
emit :payment_success to @order // But WHICH order?The Solution: Automatic Correlation
EventFlow automatically maintains conversation context. You don't need to manage correlation IDs.
How It Works
┌─────────────────────────────────────────────────────────────┐
│ Conversation Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ @order:abc123 @payment:xyz789 │
│ │ │ │
│ │ ─── :payment_request ──────────> │ │
│ │ conversation_id: conv-001 │ │
│ │ from: @order:abc123 │ │
│ │ │ │
│ │ <── :payment_success ─────────── │ │
│ │ conversation_id: conv-001 │ │
│ │ (auto-routed to @order:abc123) │
│ │ │ │
└─────────────────────────────────────────────────────────────┘Event Metadata
When you write:
emit :payment_request to @paymentThe runtime automatically adds metadata:
{
"event": ":payment_request",
"conversation_id": "conv-001",
"from": {
"machine": "@order",
"aggregate_id": "order-abc123"
},
"to": {
"machine": "@payment"
},
"data": { ... }
}When @payment responds:
emit :payment_success to @orderThe runtime uses the conversation context:
{
"event": ":payment_success",
"conversation_id": "conv-001",
"from": {
"machine": "@payment",
"aggregate_id": "payment-xyz789"
},
"to": {
"machine": "@order",
"aggregate_id": "order-abc123"
}
}Key Point: EventFlow authors don't need to manage correlation keys - the runtime handles it automatically.
Complete Example
machine: @order
scenario: checkout flow
on> :checkout from @customer
// New aggregate created: order-abc123
order moves to #awaiting_payment
emit :payment_request to @payment // Starts conversation
on :payment_success from @payment // Same conversation
// Automatically routed to order-abc123
order moves to #paid
emit :reserve_stock to @inventory // New conversation branch
on :stock_reserved from @inventory // Returns via conversation
order moves to #fulfilled
machine: @payment
scenario: payment processing
on :payment_request from @order
// First event for @payment → new aggregate: payment-xyz789
// Conversation context links back to order-abc123
process card
? card is valid
emit :payment_success to @order // Returns to originator
?
emit :payment_failed to @order
machine: @inventory
scenario: stock management
on :reserve_stock from @order
// First event for @inventory → new aggregate: inventory-456
// Conversation context links back to order-abc123
? stock is available
reserve items
emit :stock_reserved to @order // Returns to originator
?
emit :out_of_stock to @orderMultiple Conversations
A single aggregate can participate in multiple conversations simultaneously:
on> :checkout from @customer
order moves to #processing
emit :payment_request to @payment // Conversation A
emit :fraud_check to @fraud // Conversation B
emit :reserve_stock to @inventory // Conversation C
on :payment_success from @payment // Response to A
$payment_ok becomes true
? $payment_ok and $fraud_ok and $stock_ok
order moves to #confirmed
on :fraud_cleared from @fraud // Response to B
$fraud_ok becomes true
? $payment_ok and $fraud_ok and $stock_ok
order moves to #confirmed
on :stock_reserved from @inventory // Response to C
$stock_ok becomes true
? $payment_ok and $fraud_ok and $stock_ok
order moves to #confirmedEach emit starts a separate conversation, and responses are correctly routed back to the originating aggregate.
Conversation Branching
Conversations can branch when one machine emits to multiple others:
@order:abc123
│
├── emit :payment_request ──────> @payment:xyz789
│ │
│ <── :payment_success ──────────────┘
│
├── emit :fraud_check ──────────> @fraud:check-001
│ │
│ <── :fraud_cleared ────────────────┘
│
└── emit :reserve_stock ────────> @inventory:res-001
│
<── :stock_reserved ───────────────┘All three responses are routed back to @order:abc123.
Nested Conversations
A machine handling a request can start new conversations:
machine: @order
on> :checkout from @customer
emit :payment_request to @payment
order moves to #awaiting_payment
machine: @payment
on :payment_request from @order
// @payment starts its own conversation with @bank
emit :authorize_card to @bank
payment moves to #authorizing
on :card_authorized from @bank
// Can still respond to original @order
emit :payment_success to @orderThe conversation chain is preserved through all levels.
Manual Correlation (When Needed)
In rare cases where you need explicit correlation (e.g., calling external APIs), you can include IDs in event data:
emit :external_api_call to @gateway
with:
| order_id | $order_id |
| callback_id | $callback_id |But for internal machine-to-machine communication, automatic correlation is preferred.
Conversation Lifecycle
- Start - First
emitfrom an aggregate creates conversation context - Propagate - Context is passed along with each event
- Branch - Multiple emits create parallel conversation branches
- End - Conversations end when processing completes (no explicit close)
Benefits
- No boilerplate - No correlation ID passing in EventFlow code
- Automatic routing - Responses find their way back automatically
- Multiple conversations - Handle parallel flows naturally
- Nested support - Works across multiple levels of machine interaction
- Audit trail - Conversation IDs provide traceability