Defining Listeners
A listener is a deterministic in-process reaction to an emitted event. When an event matching a listener’s on: condition is emitted, the listener runs synchronously — in the same commit envelope, before the ledger append completes.
Listeners let you maintain consistency across related entities within a single intent’s execution, without introducing asynchronous coupling.
How listeners differ from projections and side_effects
| Listener | Projection | side_effects | |
|---|---|---|---|
| Timing | Synchronous, same commit | Async, via Kafka | Async, fan-out |
| I/O | None | PostgreSQL UPSERT | emit, submit, schedule |
| Purpose | Cross-entity consistency in-process | Read models | Fan-out to other streams |
| Replay | Part of rule evaluation | Via Kafka replay | Re-submitted intents |
Use listeners when you need two entity states to update atomically within the same intent. Use projections for query-side read models. Use side_effects for fan-out that can be eventually consistent.
Syntax
listeners:
- on: POINTS_AWARDED
where: event.payload.apply_to_match
resolve:
match: lookup(match, event.payload.match_id)
mutate:
- op: add
path: /stats/home_score
value: event.payload.delta
on: matchFields
| Field | Required | Description |
|---|---|---|
on | Yes | The event type that triggers this listener. |
where | No | Condition expression. Listener runs only when truthy. |
resolve | No | Named lookups to load related entities. |
mutate | Yes | List of mutation operations to apply. |
on:
The event type name that triggers the listener. Must match a declared event.
- on: TICKET_PURCHASEDOne listener per on: event. If you need multiple reactions to the same event, declare multiple listener blocks.
where:
An optional condition evaluated against the triggering event. If the condition is falsy, the listener does not run.
- on: POINTS_AWARDED
where: event.payload.apply_to_matchExpression context: event.payload.<field> for the triggering event’s payload, event.ts, event.entity_id.
resolve:
Named lookups that load related entities for mutation. The resolved entities are available by name in mutate: via the on: field.
resolve:
match: lookup(match, event.payload.match_id)
venue: lookup(venue, event.payload.venue_id)The lookup(entityType, entityId) function loads the entity snapshot for the given type and ID. The result is available as the named variable in mutate:.
mutate:
A list of mutation operations, each with an on: specifying which resolved entity to mutate.
mutate:
- op: add
path: /stats/home_score
value: event.payload.delta
on: match
- op: set
path: /last_scored_at
value: event.ts
on: matchSupported ops in listeners: set, add, sub, push, filter, remove, unset, merge.
The on: field refers to a variable from resolve:. The primary entity (the one the triggering action was running on) is accessible as entity.
Ordering
Listeners are sorted and executed in file path order within the application. If you have multiple listener files and execution order matters, name your files with a numeric prefix:
listeners/
01_score.listeners.causet
02_rank.listeners.causetWithin a single file, listeners execute in declaration order.
Next steps
- Examples — scoring and follow-counter patterns
- Best Practices — common mistakes
- Intents — where events that trigger listeners are emitted