An event is a fact an aggregate emits when something happens to it — an order was placed, an order was paid. Events are how one part of the system announces a change without knowing who reacts to it: the emitter states a fact; subscribers elsewhere decide what to do with it. Only aggregate roots emit events, and each event names the aggregate it belongs to.
Declaring an event
An event names its aggregate, how it is triggered, how it is delivered, and the payload it carries:
event OrderPlaced {
aggregate Order
trigger custom
delivery signal
topic "ordering.order.placed"
payload "this.id"
doc "A customer placed an order."
systems [OrderingSystem]
order_id uuid
customer_id uuid
}aggregate Orderbinds the event to its emitting aggregate root.topicis the channel subscribers listen on.payloadis an expression selecting what the event carries.- The fields (
order_id,customer_id) form the event's own message shape.
Two ways an event fires
The trigger clause decides when the event is emitted, and Chowk shows both styles on the same aggregate.
Explicit (custom) — the event is emitted deliberately by a command. OrderPlaced above is custom, and the PlaceOrder command announces it with emits OrderPlaced. Use custom when the fact is not a plain lifecycle transition.
Lifecycle (on_update) — the framework emits the event automatically on a lifecycle change, optionally filtered by a when guard. OrderPaid fires whenever an order update lands the row in PAID:
event OrderPaid {
aggregate Order
trigger on_update
when "this.status == 'PAID'"
delivery signal
topic "ordering.order.paid"
payload "this.id"
doc "An order was paid."
systems [OrderingSystem]
order_id uuid
customer_id uuid
}No command has to remember to emit it — marking an order paid is what triggers it, and the when guard ensures it only fires on the transition into PAID. The available triggers are on_create, on_update, on_delete, on_restore, and custom.
Events decouple the sub-domains
An event is only half a story — its value is that other sub-domains react to it without the emitter knowing. Chowk's fulfillment opens a shipment in response to OrderPlaced, through a subscription:
subscription StartFulfillmentOnOrderPlaced {
on ordering.v1.OrderPlaced
command StartFulfillment
systems [FulfillmentSystem]
doc "On OrderPlaced, open a shipment for the order."
input {
order_id: "event.order_id"
}
}The flow is fully decoupled: PlaceOrder (in ordering) emits OrderPlaced; the event travels through the platform's delivery pipeline; this subscription (in fulfillment) maps the payload into StartFulfillment and opens a shipment. Ordering never calls fulfillment — it announces a fact, and fulfillment listens. New reactions (a payments sub-domain, an analytics feed) subscribe to the same event without touching the ordering model at all.
flowchart LR A[PlaceOrder<br/>ordering] -->|emits| E((OrderPlaced)) E -->|subscription| B[StartFulfillment<br/>fulfillment] E -.->|future subscriber| C[payments · analytics]
This is the seam between the write side and everything that reacts to it: an aggregate records a fact as an event, and the reactions are declared separately as subscriptions — so the thing that changes and the things that respond to it stay independent.