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).

For documentation examples, use pdl fences for projection-only snippets in projections/* (compiled with CompileProjection); use screenplay fences for whole-document examples, including ones on this page. A projection-level key is parsed but does not route events; declare keys on each from (or its events).

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. Chronicle is also the semantic reference for what a projection means: the executable semantic model mirrors how Chronicle lowers and runs it — see Semantic Model.

One quoted or fenced-text description on the projection header records report-only authoring intent (PLAY0270), printed before file and every builder directive. Variants and nested blocks do not accept it. The description does not satisfy a projection’s directive requirement and adds no executable bytes. Markdown documentation is not supported here.

projection <Name> => <ReadModel>
<PDL grammar>
projection InvoiceDetails => InvoiceDetailsReadModel
from InvoiceRegistered key invoiceId
customerId = customerId
invoiceNumber = invoiceNumber
currency = currency
status = "draft"
registeredAt = $eventContext.occurred
from InvoiceSent key invoiceId
status = "sent"
sentAt = sentAt
from InvoicePaid key invoiceId
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
from <EventType> key <expression>Which value identifies the read model instance for this event.
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.
clear <property>Removes the value the property holds.
$eventContext.occurredThe timestamp of the event being projected.
variant <Name> / enters on <EventType>One of several mutually exclusive named read models sharing the projection’s identity.

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.

In an explicitly outbound Translate slice, a projection (or reducer) may name the slice’s public event after => instead of a read model. It builds the public event from private events:

slice Translate PublishOrderShipped
direction outbound
public event OrderShipped
orderId String
status String
projection OrderShippedPublisher => OrderShipped
from OrderPacked
status = "packed"
from ShipmentDispatched
status = "shipped"

The target is an event when => names a declared event and no readmodel of that name. Its from and on events must be private local events (PLAY0611), the target must be the slice’s one local public event (PLAY0609, PLAY0613, PLAY0617), and the form is an error anywhere but an explicitly outbound translation (PLAY0618). It selects ESM v9. The reference evaluator folds the private events deterministically and publishes one public instance per target key. A reducer that targets an event has an opaque transition body, so the reference evaluator reports Unsupported for specifications that depend on it. Renaming the event refuses while a => target names it.

  • 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
  • Variants - Mutually exclusive named read models sharing one projection identity
  • Expressions - Understanding expression syntax
  • Semantic Model - How projections bind to the executable semantic model, with Chronicle as the semantic reference
  • Grammar (EBNF) - Complete formal grammar specification