Events
Events are immutable, past-tense facts — the record of something that happened. An event declaration defines the event type’s name and properties. Properties use concepts, composite types, or primitives.
Syntax
Section titled “Syntax”[public] event <Name> [generation <N>] [from "<origin>"] [file <path>] [description "<text>"] [documentation <fenced markdown>] [id "<old-name>"] // only after renaming a persisted event [tag <value>]* <property> <Type> [subject] ...Without generation, an event is generation 1 and prints exactly as before. To show an evolved event, declare each full shape with its own number, beginning at 1 and continuing without gaps or duplicates. Declarations with the same name in the same owning slice (module, feature path and slice) identify one event contract: its address and EventContractId do not include the generation number. A same-named event in another slice has a different address, not another generation of this contract. The current ESM binder treats same-named events across slices as ambiguous, and the identity catalog rejects colliding legacy IDs; do not use cross-slice declarations to express lineage. Generations use numbers from 1 through 4294967294; Chronicle reserves 4294967295 for an unspecified generation. The previous generation of N is always N−1. References to an event name resolve to its current (highest) generation in that slice; property checks use that shape, not the historical ones. The executable model stores one contract with current revision N and complete prior revisions 1..N−1, each with its predecessor (generation 1 has none). Each revision owns separate property identities, even for same-named properties. A lone generation 1 marker behaves like an unmarked event and retains its previous ESM version. A multi-generation contract selects ESM v4. Bare references use the current revision only; a produces mapping, then assertion, projection mapping, or constraint naming a removed property fails binding with PLAY0273, and given historical-shape consumption reports PLAY0449 with the event and revision. No historical fact is silently converted or replayed. A target without explicit migration support must reject an evolved contract; Chronicle registration needs migrations, which Screenplay does not yet express. Complete historical declarations alone do not prove schema immutability across edits to referenced concepts or types; use the release baseline check below. Transformations between generations remain tracked in #71.
event ProjectRegistered generation 1 projectId Uuid name String
event ProjectRegistered generation 2 name StringThe optional file line names the repository relative file this declaration is realized by, so a document can be navigated back to the code it describes. It is additive - it never stands in for any part of the declaration. See File references.
For an existing catalog, advance a contract only with SemanticIdentityCatalog.PlanEventRevisionAdvancement(previous, expectedRevision, documentKeys, semanticAddresses, eventAddresses, advancements). Supply the complete current addresses and an explicit (event address, new revision) advancement. The plan checks the exact previous catalog revision, retains the contract id, carries generation-1 property IDs to revision-qualified addresses, and refuses backward or stale advancement. A fresh compile can bootstrap all property identities for each declared revision and record revision N in its returned SemanticCompilation.Documents.IdentityCatalog; persist that catalog before subsequent edits. Workspace transactions can supply the same explicit advancement through WorkspaceTransactionRequest.EventRevisionAdvancements, checked against ExpectedCatalogRevision. Newly introduced events receive their declared revision without an advancement. Declaring fewer generations than a persisted catalog records fails binding.
Released event contracts
Section titled “Released event contracts”At release time, explicitly admit the shipped revisions
with screenplay baseline admit <root> --release <label> and commit
.screenplay/release-baseline.json. Run screenplay baseline check <root> in CI
before accepting later edits. The file keys on persisted event contract identities
and revision numbers, never on names or display addresses.
Each entry freezes the resolved stored shape: property names, optional and
collection flags, expanded primitives and composite members, concept compliance
markers, enum member names and numbers, and the effective stored event name
(id pin when present, otherwise the declaration name). Equality is exact in v1,
even for changes Chronicle might tolerate. A changed referenced concept, composite
or enum affects every admitted revision that uses it, including historical ones.
Declare a new type or concept and use it in a new generation instead of changing
the old shared type underneath stored events.
Concept/type/enum names, descriptions, documentation, tags, semantic property IDs, concept validation rules and generation strategies do not change a stored shape. The baseline itself is not part of the language or executable model and changes none of their revisions. Ordinary compilation and reading remain available when it is malformed; the baseline check and admission fail closed. No tool removes entries: unfreezing requires a deliberate reviewed edit to the sidecar.
Chronicle projection registration
Section titled “Chronicle projection registration”For projections that opt in to Chronicle’s generation support, bare PDL event names pin the latest registered generation when the projection is registered: one generation per event type per projection. This is provider registration behavior, not a change to Screenplay’s resolution to the highest declared generation described above and in decision 0015. There is no X+N syntax. Explicit generation-qualified projection references remain tracked in Screenplay#642.
Change an event property through MCP
Section titled “Change an event property through MCP”Use propose-property to add, rename or remove a current event property. Without
a release baseline, choose explicitly between eventNeverPersisted and newGeneration.
With a baseline, an unadmitted current revision defaults to inPlace; an admitted
revision requires explicit newGeneration and refuses eventNeverPersisted. The first
asserts that the current generation has never been stored: it edits that generation
in place and preserves renamed property IDs. The second keeps every historical
shape and identity, appends the changed shape as generation N+1, and assigns new
property identities there while retaining the event contract ID. Historical
properties refuse either choice. Extract an inline event before changing its contract.
Without a baseline, adding even an optional property requires this choice: an in-place schema addition
is not safe for a stored generation. Add selects an owner and explicit declaration
or mapping hops; the planner never infers producer values. Required properties
need literal fixture values. See adding a property along its chain.
Only the target and explicitly selected alsoRename, alsoRemove or add hops change.
Evolution flags do not select connected events. One proposal can select several
events with different starting generations, but uses one mode for them all. Its
required property.generations array reports each selected event once, in canonical
address order, with that event’s own from, to, mode and mutability.
Mutability reports basis, releaseBaselineRevision and original admittedIn.
A missing baseline reports noBaseline with both other fields null; unadmitted
revisions report notInBaseline; admitted revisions report baselined. In-place edits report
from == to; an empty array means no accepted planned event edits, not success.
There is no singular generation-report alias.
deployableWithoutTransformation reflects the entire candidate, including earlier
multi-generation events. A new generation is not deployable to Chronicle without
transformations. esmVersionKnown is false when the candidate does not bind;
esmVersionChange is then null, not an inferred version. Source acceptance and
executable readiness remain separate. Review the retained property proposal
before applying it.
Public contracts and origin
Section titled “Public contracts and origin”Unmarked events remain private local facts. Mark a local contract public event Shipped.
An event from another application uses event Dispatched from "Fulfillment"; an origin implies
public visibility, so public event Dispatched from "Fulfillment" means the same thing.
The origin is an opaque, nonblank string. It is never resolved as a declaration or filesystem
path, and renaming a declaration never rewrites it. Quotes, backslashes and control characters
use the ordinary string escaping rules. When combining origin and generation,
write the generation first: public event Dispatched generation 1 from "Fulfillment".
The C# and TypeScript parsers, printers, typed AST JSON, authoring workspace, Monaco, VS Code and the event model
board preserve these fields. A model that uses public visibility or an origin selects ESM v9, released in Screenplay 4.117.0 (see interoperability); models that use neither keep their existing version and canonical bytes. The executable model records each event’s visibility and origin, and the reference evaluator publishes a public event as a recorded fact when its outbound projection or reducer fires. It does not deliver to another application. An imported public event (one with an origin) declares its shape locally; public or foreign imports that cannot supply a local shape still refuse with PLAY0268. Stage and renderers do not realize these contracts yet. See decision 0049
(accepted ruling) and decision 0051 (accepted admission) for why the origin is a quoted store name and how direction defaults.
Inline produces event keeps its existing syntax; use a standalone declaration for these fields.
The assembled model (C# and TypeScript compilers, both editors) validates contract usage, not just headers. Commands cannot publish public events.
An explicitly outbound Translate slice consumes private local events and produces exactly one local public
contract type. An explicitly inbound Translate slice consumes foreign public contracts and produces private
local events. Other slices cannot consume foreign contracts. A translation declaring or using public metadata
must state its direction; legacy translations without public metadata retain their historical inbound behavior.
An outbound slice publishes its public event with a projection or reducer that targets it
or a reaction; an inbound slice consumes foreign public events with source events
or a reaction. Both forms select ESM v9 when bound.
Qualified references and imports use declaration scope, not a name-based guess. See
Public event boundary diagnostics for the codes and current construct limits.
Example
Section titled “Example”event InvoiceRegistered invoiceId InvoiceId customerId CustomerId invoiceNumber InvoiceNumber lines InvoiceLine[] currency CurrencyCode registeredAt DateTime status InvoiceStatusAuthoring metadata
Section titled “Authoring metadata”Standalone events and inline command events accept:
description "<text>", ordescriptionfollowed by a fence taggedtextormarkdown;documentationfollowed by one nonempty fencedmarkdownblock;- optional
id "<old-name>"when renaming an event that already has stored events.
Markdown description fences are specific to events, including inline declarations; descriptions on other constructs use text. A bare description fence remains accepted with deprecation warning PLAY0397.
Each directive can appear at most once. Description and documentation are authoring metadata: binding reports PLAY0270. An event id pin that differs from the display name selects ESM v16 and carries the effective stored type name as storedName. The id string does not replace or rehash the portable EventContractId; catalog identity remains authoritative. Rendering documentation into generated XML or TypeScript comments is a separate downstream capability.
Leave id absent for a new event. Cratis convention is plain [EventType] without arguments; a pin exists to preserve an old persisted name on a rename, not to decorate a new declaration. A pin equal to the current name reports information diagnostic PLAY0471 with a typed removal repair.
Without a release baseline, workspace and MCP event renames pin the previous effective name by default, because an authoring catalog does not prove whether events have been stored. With a baseline, an unadmitted event rename does not add a pin; any admitted revision requires preserving the pin and refuses eventNeverPersisted. Rename results report eventRename.mutability and eventRename.pinAdded. If you know the event has never been persisted, set EventNeverPersisted on the rename request (eventNeverPersisted: true in MCP) to omit a new pin. That option also removes a redundant pin equal to the current name (the one PLAY0471 reports); a pin naming an earlier, different identity is never overwritten. Renaming back to the pinned name removes the pin. Generations must agree on their effective identity; contradictory pins refuse the rename. Command, slice and module renames do not add event pins.
The effective stored name is the id pin, otherwise the event name. A pin must be nonempty, have no surrounding whitespace, and follow the event-name identifier rule: an ASCII letter or underscore first, then word characters. Binding refuses invalid pins, different effective names across generations, and two events in one application with the same stored name (PLAY0472). storedName appears once on the current contract entry; prior revisions share it. A pin equal to the display name is a no-op: it omits storedName, does not select v16 or report PLAY0686, and its PLAY0471 removal repair preserves byte-identical ESM. Models without differing event pins keep their earlier version, bytes and revision.
Consumers must explicitly admit ESM v16 and preserve storedName, or refuse the model; older strict readers reject the member. The reference evaluator still identifies facts by contract and display name. Warning PLAY0686 is retired, and the compatibility eventPinWarnings array in MCP proposals is empty. Stage rendering is a separate admission: an application already storing events under the new name needs a reviewed baseline correction or a data migration before switching its generated stored type.
Screenplay has no source-language-version pin, so decision 0075’s refusal for a source pinned below the admitting language version is not currently applicable. Consumer version admission and strict-reader refusal still apply.
Rename preserves trivia by default, including when inserting or removing the pin. Unsupported source edits refuse rather than silently reformatting the document.
To evolve an inline event, first use WorkspaceEventRefactorings.ProposeExtractInlineEvent or MCP propose-extract-inline-event. The declaration moves into the slice, keeping its id, description, documentation, tags and property identities. Its production keeps explicit routing or receives the previously implicit identifier destination. Extraction requires unchanged canonical ESM and catalog assignments, preserves comments exactly once or refuses, and needs explicit canonical formatting consent. Add later generations only after extraction; there is no reverse inlining refactoring.
event Renamed description "The project acquired another name" documentation ```markdown The project keeps its event source identity. ``` name StringProperty-shaped lines remain properties: id String, description String, and documentation String still declare payload fields. Inside an inline event they must also provide their mapping, such as description String = name.
Inline declarations
Section titled “Inline declarations”produces event <Name> inside a command declares the same slice-owned event as a standalone declaration. The command reference explains its typed mappings and destination default. Extract an inline event before introducing later generations. Plain productions can reference it regardless of declaration order. The event and its properties have the same workspace addresses and stable identities as the equivalent standalone event in that slice. Inline typed mappings cannot repeat a property name (PLAY0168).
Type modifiers
Section titled “Type modifiers”| Modifier | Syntax | Example |
|---|---|---|
| Collection | <Type>[] | lines InvoiceLine[] |
| Optional | <Type> optional | note String optional |
Data subject
Section titled “Data subject”An event has one data subject, defaulting to its event source. When it records data about someone else, mark the property identifying that person with trailing subject:
concept CustomerId : Uuidmodule Billing feature Contact slice StateChange ChangeContact event CustomerContactChanged customerId CustomerId subject note StringThe modifier is last, after any other property modifiers; in an inline event it precedes the mapping =. A property named subject or of type subject remains a property, not a modifier. Each event generation chooses its mark independently. Two marks are errors on each extra property, naming the first; choosing the subject has no automatic repair. The compiler cannot detect subtler mixing of several people’s data and must not guess the subject.
Allowed targets are required scalar String, Uuid, concepts over either, and Int-backed concepts, matching the stream-id type set. Bare Int, enums, collections, composite types, optional values, Decimal, Bool, Date and DateTime (including concepts over them) are refused. Concepts marked pii or secret, including their aliases and legacy spellings, cannot be subjects: EventContext.Subject is stored in plaintext. Use a surrogate identity. Unavailable imported shapes remain unresolved with an unknown-type warning rather than guessed.
An empty String value cannot be caught at compile time and Chronicle falls back to the event source; consider not empty validation on the identity concept. Equal identity strings across entity kinds share one subject, so favor Uuid identities.
Phase 1 is report-only lineage metadata. Binding reports PLAY0270; canonical executable-model bytes and revision are identical to the unmarked model. No C# [Subject] provider output, projection lineage propagation, erasure, export or redaction is promised. Executable admission is a separate phase. The event context’s existing $eventContext.subject means the event source unless a subject was supplied; this phase does not change its runtime value. Concept pii and secret markers are admitted by ESM v13 with the event source as the default subject. An event carrying a personal value or an explicitly subject-scoped secret together with an explicit subject mark refuses binding with PLAY0268 until lineage is admitted, including through composite members and collection elements.
subject is refused on commands, composite types, operation inputs, trigger or reaction data and response fields (decision 0008). Read model marks are not yet supported; support and Chronicle’s reserved _subject, __subject and __subjects names are tracked in #559. secret scope subject selects encryption scope; policy matches subject means the thing acted on, not the caller ($context.causedBy.subject).
MCP declaration-details event summaries expose subject: { "source": "eventSource" } by default, or { "source": "property", "property": "customerId" }. The properties view exposes isSubject; syntax-schema exposes the optional PropertySyntax.isSubject flag, omitted from structural JSON when false. Rename, move and inline extraction preserve the flag.
tag lines attach tags to every append of the event — Chronicle stores them alongside the event so consumers can filter and group on them. A tag value is a bare identifier or string literal for static tags, or a $context. expression for tags resolved from context at append time.
event InvoiceRegistered tag invoicing tag "billing" tag $context.identity.id invoiceId InvoiceIdEach historical revision retains its declared literal tags; new appends use the current revision’s event tags.
Tags inside produces event are event-type tags, just like the standalone form. Tags can also be declared per production site — on a plain produces block and on a capture append block — where they apply to that specific append rather than every occurrence of the event type. ESM v1 admits literal event-level and production-level tags as append metadata, in that order. Chronicle’s IEventSequence.Append accepts tags and persists them in EventContext.Tags (Decision 0001); tags do not change the event payload. $context-computed tags remain syntax-only; ESM v2 admits selected scalar $context produces mappings, not computed tags.
Guidance
Section titled “Guidance”- Name events in the past tense and make them self-describing:
InvoiceRegistered, neverCreated. - One purpose per event. If an event needs a nullable property to cover two situations, model the second situation as its own event.
- Compliance is inherited. A property typed with a
@piiconcept is PII — nothing extra to declare on the event. - The event-source identity is not an event property; it travels in typed event context in ESM v2. A command’s
produces … for <identifier>binds the destination from a required scalar command identifier. A projection can use$eventSourceIdwithout copying that identity into every event payload, and a specification can name it withfor <value>on itsgivenandthenevents. Marking an event propertyidentifieris an error. - Declare the shapes you reference. A property typed
InvoiceLine[]needs atype InvoiceLine; the compiler warns when it resolves against nothing the document declares.