Skip to content

Projections

Projections declare how events are projected into a queryable read model. The body of a projection block is written in the Projection Declaration Language (PDL) — an embedded sub-grammar. The Screenplay parser delegates the indented body to the PDL parser (see Sub-language Pluggability).

The pages in this section document the full projection sub-language — every directive, operation, and expression the PDL supports. The same language is also used standalone by Cratis Chronicle to define projections without writing code; see Chronicle projections for how Chronicle hosts and executes it.

projection <Name> => <ReadModel>
<PDL grammar>
projection InvoiceDetails => InvoiceDetailsReadModel
key invoiceId
from InvoiceRegistered key invoiceId
customerId = customerId
invoiceNumber = invoiceNumber
currency = currency
status = "draft"
registeredAt = $eventContext.occurred
from InvoiceSent
status = "sent"
sentAt = sentAt
from InvoicePaid
status = "paid"
paidAt = paidAt
join customer on customerId
with CustomerRegistered
customerName = name
children lineItems identified by lineNumber
from InvoiceLineItemAdded key lineNumber
parent invoiceId
quantity = quantity
unitPrice = unitPrice
remove with InvoiceLineItemRemoved key lineNumber
parent invoiceId
ConstructMeaning
key <property>Which property identifies the read model instance.
from <EventType>Maps event properties onto the read model. Same-named properties map automatically.
join <property> on <key>Joins related state by a key property.
children <collection> identified by <key>Projects events into a child collection.
parent <property>Identifies the owning parent of a child.
remove with <EventType>Removes the instance (or child) when the event occurs.
increment / decrement <property>Counters maintained per event.
$eventContext.occurredThe timestamp of the event being projected.

An aggregating projection using counters:

projection InvoiceSummary => InvoiceSummaryReadModel
from InvoiceRegistered
key "global"
increment totalCount
increment draftCount
from InvoiceSent
decrement draftCount
increment sentCount
from InvoiceCancelled
decrement totalCount

A slice is one behavior, and a behavior often needs more than one read model. The list a screen binds to is shaped for a reader; the state a command consults before it decides is shaped for a decision. Both are built from the same events by the same slice, so both projection blocks live in it:

slice StateView CustomerPortalReport
query GetPortalReport => PortalReportReadModel
by customerId CustomerId
projection PortalReport => PortalReportReadModel
from PortalInvitationSent
invitedAt = $eventContext.occurred
projection RevokedPortalToken => RevokedPortalTokenReadModel
from PortalTokenRevoked
revokedAt = $eventContext.occurred

Declare as many as the behavior needs. Each names its own read model, and printing writes them back out in the order they were declared. Splitting them across two slices would say the system has two behaviors where it has one.

  • From Event - Define rules that trigger when events occur
  • Property Mapping - Map event data to read model properties
  • Auto-Map - Automatically map matching properties
  • Keys - Explicit and composite keys for projection instances
  • Event Context - Access event metadata like timestamps and correlation IDs
  • From Every - Apply rules to all events the projection subscribes to
  • From All - Subscribe to all event types without filtering
  • Counters - Increment, decrement, and count operations
  • Arithmetic - Add and subtract operations
  • Joins - Combine data from related events
  • Children - Define nested collections
  • Nested Objects - Single nullable child objects
  • Removal - Remove projection instances based on events
  • Expressions - Understanding expression syntax
  • Grammar (EBNF) - Complete formal grammar specification