Skip to content

Getting started

By the end of this page you’ll have a .play file that models a small feature — a command, the event it produces, and a screen that reads it — open in an editor that highlights the language, completes it as you type, and flags mistakes live. That editor is what this repository ships; running a .play as an application is Stage’s job, and this tutorial gets you to the point where a Screenplay is ready for it.

You need Node.js 20 or newer. Clone the repository and open a terminal in its root:

Terminal window
git clone https://github.com/Cratis/Screenplay.git
cd Screenplay

Install the workspace and start the standalone editor:

Terminal window
yarn install
yarn build
yarn dev

yarn dev serves the editor at http://localhost:9200. It opens on a complete sample — the invoicing .play — so the very first thing you see is the language rendered with full syntax highlighting, including the embedded C#, TypeScript, and React blocks.

Clear the editor and start a bounded context. A .play file is indentation-based — structure follows the offside rule, and a construct owns everything indented beneath it. Type the outline of a module with one feature and one slice:

module Library
feature Lending
slice StateChange BorrowBook

As you type slice, the completions offer the four slice types — StateChange, StateView, Automation, Translate. StateChange is the one that accepts a command and records what happened.

Inside the slice, declare the command, the rule that guards it, and the fact it produces:

slice StateChange BorrowBook
command BorrowBook
bookId BookId
borrowedBy MemberId
authorize IsMember
validate
bookId not empty message "A book is required"
produces BookBorrowed
bookId = bookId
borrowedBy = borrowedBy
borrowedAt = $context.occurred
event BookBorrowed
bookId BookId
borrowedBy MemberId
borrowedAt DateTime

Notice what the file is saying in one read: who may call it (authorize), what has to be true (validate), and the past-tense fact it appends (producesevent). The event never carries an identity of its own — $context.occurred and the event source come from the surrounding context, not the payload.

That slice references a policy, IsMember, that you haven’t declared. The language service knows it: IsMember is underlined with a diagnostic — reference to an undeclared policy. This is the live feedback the editor gives you for the whole language — unknown slice types, unknown primitive types, references to undeclared policies or events, and tab indentation are all flagged as you type.

Declare the policy at the top of the file, above the module, and the diagnostic clears:

policy IsMember
require authenticated

A command that records facts is only half a feature. Add a second slice — a StateView — that projects the event into a read model and puts it on screen:

slice StateView OnLoan
query BooksOnLoan => OnLoanReadModel[]
projection OnLoan => OnLoanReadModel
from BookBorrowed key bookId
borrowedBy = borrowedBy
borrowedAt = borrowedAt
screen OnLoan
data OnLoanReadModel[] via query BooksOnLoan

The projection uses the embedded Projection Declaration Language to fold events into a read model; the query exposes it; the screen renders it. No update code, no proxy to regenerate by hand — the whole read path is declared in six lines.

You have a .play file that models a complete slice of behavior — command, event, projection, query, and screen — with authorization and validation, all in one file the editor understands. That file is a Screenplay ready to hand to Stage.

Where to go next: