Skip to content

Acme Commerce: a walkthrough

Acme Commerce is the large sample in this repository: a mid-size shop in five bounded contexts - Catalog, Storefront, Sales, Purchasing and Warehouse - written as ONE Screenplay application in Samples/AcmeCommerce. The contexts never share a database. They talk only through twelve public events, and each context translates what it receives into its own words, so a change in one context’s language never leaks into another’s.

This page tells how the sample was built and why each part looks the way it does. Every statement here is checked against the sources of the sample; the sample’s own README is the short reference. What the language can say is said in the .play files; only the brainstorming material and a few project settings live only in Studio.

At the time of writing the application is 56 .play files:

DomainModuleSlices (change / view / automation / translate)ScreensSpecifications
CatalogMerchandising5 / 5 / 0 / 31018
StorefrontShop9 / 6 / 1 / 61527
SalesOrders8 / 6 / 2 / 71530
PurchasingBuying12 / 6 / 1 / 32034
WarehouseStock18 / 6 / 5 / 92458

The shell adds the Application module (2 slices, 4 screens, 2 specifications). Together that is 169 specifications.

WhatWhere
The Screenplay sourceSamples/AcmeCommerce in this repository
The Studio projectthe project Acme Commerce with the application Acme Commerce Shop, in the Cratis tenant of Cratis Studio
The loader that fills the Studio projectscripts/samples/acme-commerce in the Cratis/Studio repository

The Studio project is permanent: it is never deleted, and it is only refreshed through the loader. Sign in to Cratis Studio with an account in the Cratis tenant and pick Acme Commerce in the project picker. A project’s address has the shape https://app.cratis.studio/projects/<projectId> and an application’s https://app.cratis.studio/projects/<projectId>/application/<applicationId>, so open the project from the picker rather than from a copied link. The application is called “Acme Commerce Shop” rather than plain “Acme Commerce” because Studio application names are unique per tenant and an early scratch application had already taken the shorter one.

The sample was written in this order, and each step compiles on its own:

  1. The shared kernel (Shared/): concepts, event sources, policies and personas.
  2. The shell of each domain (<Domain>/shell/): its theme, UI profile, navigation with logo and sample data.
  3. Catalog first, because it publishes the first contracts (P1 to P3).
  4. Storefront, which only listens to Catalog and then publishes the first order (P4).
  5. Sales, Warehouse and Purchasing, which close the loops: order to cash to dock, and replenishment.
  6. The custom component package, the sample data and the tooling that packages a domain for Studio and builds a renderable variant.
application.play the ONLY `domain AcmeCommerce` line; `reaction batches atomic`, `command reads protected`
Shared/ the shared kernel
<Domain>/shell/ the domain's own theme, ui profile, navigation + logo, sample-data pack
assets/logos/ the five logo SVGs
<Domain>/model/ state changes, state views, automations, forms, screens, specifications
<Domain>/translation/ Translate slices: outbound publication, inbound anti-corruption
_packages/scene.commerce/ the custom component package @acme/scene.commerce
tools/ domain-package.sh, render-variant.sh, render-bridge/, packages/

Four rules about composing a folder into one application shaped the layout:

  • One domain line per tree. Only application.play has it; the domain folders carry none.
  • One name per event in the application. The same name on a publisher and on a consumer compiles but does not bind (PLAY0472). A consumer therefore declares a foreign public event under a consumer-local name, <Event>At<Consumer> - ProductPublishedAtStorefront - and from "<Origin>" keeps the lineage.
  • No .. imports. Shared/ is a sibling folder; compiling a folder walks every .play file in it.
  • A read model’s first event creates it. Every required property is set there with a literal default. Specifications that reach an unadmitted projection or depend on its read model report “unsupported”; unrelated specifications still run.

Shared/ holds what every context means the same way.

Concepts with their rules. Validation lives on the concept, so the rule travels with the value into every command, event and read model. Sku is the key every context knows a product by:

concept Sku : String
validate
not empty message "A SKU is required"
min 7 message "A SKU has at least 7 characters"
max 24 message "A SKU has at most 24 characters"
starts with "ACM-" message "A SKU starts with ACM-"
not contains " " message "A SKU has no spaces"

Other shared concepts are Money (a Decimal, >= 0, decimals <= 2), Quantity (an Int, > 0), CurrencyCode (three letters), PurchaseOrderNumber (starts with PO-) and EmailAddress, which is marked pii with a stated reason. Each domain adds the words only it uses: Storefront has Slug, ShortDescription, the enumeration concept DeliveryMethod (standard, express, pickup) and the composite type BasketLine. Concepts that every context uses include the enumerations OrderStatus, ProductStatus and Carrier and the composite types PostalAddress and OrderLine.

Event sources. Every event source is declared once in Shared/eventsources.play, because a source is a name several contexts may refer to: Product, Customer, Basket, Order, Supplier, and so on, with named streams where one identity has more than one history.

Policies and personas. Shared/access.play declares the policies (IsShopper, IsSalesManager, CanCurateCatalog, CanBuy, CanWorkWarehouse, …) and then the personas that hold them: Shopper, Merchandiser, SalesRep, SalesManager, Buyer and WarehouseClerk: six personas, each a real person who works in the shop. A module admits its own persona(s):

policy CanCurateCatalog
require role "Merchandiser"

Processing purposes. Storefront (ShopOrderProcessing), Sales (OrderFulfilmentProcessing) and Purchasing (SupplierRelations) each declare a purpose for the personal data they handle, and the module names it with purpose.

The theme. Shared/theme.play declares the Acme theme and the packages it is compatible with, including the custom package Acme.Commerce.

These are the contracts. A public event is the only thing that crosses a context boundary.

#Public eventPublished byConsumed by
P1ProductPublishedCatalogStorefront, Warehouse
P2ProductPriceChangedCatalogStorefront, Sales
P3ProductRetiredCatalogStorefront, Warehouse
P4OrderPlacedStorefrontSales
P5OrderConfirmedSalesWarehouse, Purchasing
P6OrderStatusChangedSalesStorefront
P7StockReservedWarehouseSales
P8StockReservationFailedWarehouseSales
P9StockLevelChangedWarehouseStorefront
P10StockBelowReorderPointWarehousePurchasing
P11PurchaseOrderReceivedPurchasingWarehouse
P12ShipmentDispatchedWarehouseSales

The flows they carry: the catalogue (F-A: Catalog to Storefront and Warehouse), order to cash to dock (F-B: Storefront, Sales, Warehouse and back), replenishment (F-C: Warehouse, Purchasing, Warehouse) and change of price (F-D: Catalog to Storefront and Sales).

Everything else is private. A domain says what it has done in its own words (ProductApprovedForSale, OrderConfirmationIssued, ReservationMade) and only the Translate slices turn a private event into a public one. A context never sees another’s private events.

Outbound translation: one public contract per slice

Section titled “Outbound translation: one public contract per slice”

Catalog’s PublishProduct slice turns the private ProductApprovedForSale into the public ProductPublished. The approval carries a snapshot of what was approved, so the public event is a plain projection of it:

slice Translate PublishProduct
description "Publishes ProductPublished when a product is approved for sale"
direction outbound
public event ProductPublished
productId ProductId
sku Sku
name ProductName
...
projection ProductPublisher => ProductPublished
from ProductApprovedForSale
productId = $eventSourceId
sku = sku
name = name
...

Sales’ PublishOrderStatus is the many-to-one case: six private events (approved, rejected, on hold, shipped, fulfilled and cancelled) become ONE public OrderStatusChanged with a status.

Inbound translation: the anti-corruption layer

Section titled “Inbound translation: the anti-corruption layer”

Storefront declares the foreign event in its own shape and then captures it into a private event in the shop’s own language. The shop addresses a product by its SKU, so every product capture uses key sku:

slice Translate ListProduct
direction inbound
event ProductPublishedAtStorefront from "Catalog"
productId ProductId
sku Sku
...
capture ProductFeed
source events
from ProductPublishedAtStorefront
key sku
append ProductListed
when added
productId = $.productId
title = $.name
shortDescription = $.details
slug = $.sku
...
available = 0

Catalog’s name becomes Storefront’s title; the SKU becomes the slug and, through key sku, the event source id; availability starts at zero until Warehouse says otherwise (P9). The sample declares 16 consumer-local events like this one, one for each consumer of each public event in the table above. See captures.

Reactions are the automations inside a domain that hear a private event and invoke a command. Examples that run across the whole flow:

  • Storefront: CheckoutCompleted invokes ClearBasketContents, which empties the basket.
  • Sales: an OrderReceived below 500 invokes ApproveOrder (where quotedTotal < 500), and an OrderApproved invokes ConfirmOrder, which is what produces P5.
  • Warehouse: ReservationRequested invokes ReserveStockUnits; a drop in available stock invokes RaiseReorderAlarm, which is what produces P10; a reservation plans a shipment; a dispatch takes the stock off the shelf.
  • Purchasing: ReplenishmentRequested (from P10) invokes CreateReplenishmentPurchaseOrder.
reaction SmallOrderAutoApproval
runs as system role "SalesManager"
when OrderReceived
orderId
quotedTotal
invokes ApproveOrder
orderId = orderId
where quotedTotal < 500

A reaction runs as a stated role (runs as system role ...), so the policies of the command it invokes still apply.

The Merchandiser curates what is sold. Features: Products, Pricing, Publication.

  • Commands and events. DraftProduct / ProductDrafted, DescribeProduct / ProductDescribed, SetProductPrice / ProductPriceSet, ApproveProductForSale / ProductApprovedForSale, WithdrawProduct / ProductWithdrawn.
  • Read models. ProductRow, ProductDetail, PricedProduct, ProductReadiness and CatalogFigure.
  • A decision against state. ApproveProductForSale reads ProductReadiness and refuses with “Describe the product before approving it” or “Price the product before approving it”.
  • Constraints. UniqueSku (unique sku on ProductDrafted) and OneApprovalPerProduct (unique event ProductApprovedForSale, released by ProductWithdrawn - a withdrawn product may be approved again).
  • Public events. P1, P2, P3 through PublishProduct, PublishPriceChange and PublishRetirement.

The Shopper browses, fills a basket and checks out. Features: Browse, Basket, Checkout, MyOrders.

  • Inbound. ListProduct (P1), UpdateDisplayPrice (P2), DelistProduct (P3), UpdateAvailability (P9) and TrackOrderStatus (P6).
  • Outbound. PublishOrder turns CheckoutCompleted into P4.
  • Rules. AddToBasket reads the listing and refuses a sold-out product; UniqueCustomerEmail; EmailAddress must contain @.
  • A multi-step flow. Checkout is a wizard whose three steps (EnterShippingAddress, ChooseDeliveryMethod, CheckOut) are command forms; CheckOut reads the basket size and the customer account and is refused for an empty basket.
  • Its own page shapes. ProductGrid and SideSummary are screen templates the module declares itself (see below).
  • Navigation. The module overrides the application’s topbar with its own: Browse, Basket, My orders.

Order intake, approval, confirmation and status. Features: Intake, Approval, Fulfilment, Reporting.

  • Inbound. ReceiveOrder (P4), UpdatePriceList (P2), RecordReservation (P7), HoldOrderOnShortage (P8) and RecordShipment (P12).
  • Outbound. PublishOrderConfirmed (P5) and PublishOrderStatus (P6). The private event behind P5 is OrderConfirmationIssued; the public contract keeps the name OrderConfirmed.
  • Policy-driven automation. Small orders approve themselves; larger ones wait in the manager’s inbox.
  • Who may do what. ApproveOrder, RejectOrder, ApplyLargeDiscount and the inbox are for the SalesManager (authorize IsSalesManager); a SalesRep’s ApplyDiscount stops at 15 percent.
  • Constraints. OneOpenOrderPerBasket (unique event OrderReceived; it lives with the consumer because a constraint that mentions an inbound-captured event cannot sit in the inbound slice, PLAY0616) and OneConfirmationPerOrder.
  • Navigation. The module extends the application’s sidebar with a Reports item.

Physical stock. Features: Levels, Reservations, Receiving, Dispatch, Overview. The largest domain, with nine translate slices and five automations.

  • Inbound. RegisterSkuFromCatalog (P1), the SKU block on retirement (P3), the reservation request from P5 and the stock receipt from P11.
  • Outbound. P7, P8, P9, P10 and P12.
  • Automations. ReserveStock (it either promises the stock with ReservationMade or says it could not with ReservationFailed), WatchReorderPoint (one ReorderPointCrossed per crossing; the alarm re-arms when stock comes back), SnapshotStockOnMovement, PlanShipmentWhenReserved and TakeStockWhenDispatched.
  • Constraints. UniqueLocationCode and OneDispatchPerShipment.

Suppliers, replenishment and purchase orders. Features: Suppliers, Replenishment, PurchaseOrders, Demand, Overview.

  • Inbound. RecordDemand (P5) and RequestReplenishment (P10). Outbound. PublishGoodsReceived (P11).
  • Automation. AutoReplenish drafts a one-line purchase order with the SKU’s preferred supplier. It skips when an open order already covers the SKU (the constraint OneOpenReplenishmentOrderPerSku is released by goods received or a cancellation), and when there is no preferred supplier it records ReplenishmentBlocked, which the replenishment read model shows as status blocked.
  • Single-value keys. A supplier’s offers are keyed by a generated SupplierOfferId (the offer is an event source of its own) instead of supplier + SKU: Stage refuses composite read-model keys (STAGE-ESM-048) until Chronicle#4265 ships, and the sample has no composite key anywhere. They return when Chronicle#4265 ships.
  • Constraints. UniqueSupplierTaxId, UniquePurchaseOrderNumber, OneSubmissionPerPurchaseOrder and OneOpenReplenishmentOrderPerSku.

Eleven constraints in all. Most are uniqueness rules written where the command appends the event:

constraint UniqueSku
unique sku on ProductDrafted
message "That SKU is already in the catalogue"

unique event X makes the event happen once per event source; released by Y names another slice’s event that lifts the rule again.

Every behaviour above has a specification: a command is given a caller (authenticated, role "...") and prior events and then either produces events or is refused with a message; a view is given events and then shows a read model. The accepted path of a command whose slice publishes a public event states that public event after the private ones (then ProductPriceChanged after then ProductPriceSet), so the publication is proved where the command is; the translate slice keeps its own specification for the event-driven path.

Scene keeps the logo and the UI profile at application level only (a module shows the application’s logo locked), so a domain that has its own logo, theme and shell is its own application: Acme Shop, Acme Catalog, Acme Sales, Acme Purchasing and Acme Warehouse. They share the platform (Cratis.Components and the Default blueprint’s AppShell, ^1) and differ in logo, accent, colour scheme, menu mode and menu theme. Each <Domain>/shell/ folder holds three files, and a domain package (tools/domain-package.sh) carries them to Studio.

<Domain>/shell/shell.play declares the domain’s theme and its ui profile: the web target, the packages, an accent colour, the menu and a root template taken from a blueprint - the AppShell of Cratis.Blueprint.Default, pinned at ^1. Storefront, the customer-facing one:

theme AcmeStorefront
compatible with Cratis.Components
compatible with Cratis.Blueprint.Default
compatible with PrimeReact
compatible with Acme.Commerce
ui profile Storefront
target platform web
target size regular
theme AcmeStorefront
packages
Cratis.Components
Cratis.Blueprint.Default
root template blueprint Cratis.Blueprint.Default AppShell version "^1"
option navigationMode horizontal
option menuTheme light
appearance
color scheme light
accent "#C2410C"
sample data
active AcmeStorefront
DomainAccentMenu modeMenu themeScheme
Storefront#C2410Chorizontallightlight
Catalog#4338CAstaticlightlight
Sales#1D4ED8staticdarklight
Purchasing#B45309slimPluslightlight
Warehouse#0F766Ecompactdarkdark

The name, logo image, corners and font that the designer’s App and Look tabs show are not in the ui profile grammar yet; Studio’s sample loader saves them (identity stage) from assets/logos.

<Domain>/shell/sampledata.play is the domain’s sample data pack (AcmeStorefront, AcmeCatalog, …, locale nb-NO): one shape per read model that a screen shows. It is design and preview time only: a production build never sees it.

With a blueprint root template the layout carries only the navigation regions. <Domain>/shell/navigation.play declares the domain’s layout (StorefrontNavigation, CatalogNavigation, …): the topbar with the logo - logo image "data:image/svg+xml;base64,...", the SVG of assets/logos inlined by tools/embed-logos.py, because a document carries no file beside it and a data URI cannot 404 - and a hierarchical sidebar, module then feature. A domain module says how it relates to the shell:

ModuleStatementEffect
Shopnavigation topbar overridesthe storefront has its own light topbar
Ordersnavigation sidebar extendsadds a Reports item to the sidebar
Merchandising, Buying, Stocknoneinherit the application’s navigation

The application’s topbar declares buttons once, for every page (Catalog/shell/navigation.play), and a policy gates each:

navigation topbar
button newProduct title "New product" slice Merchandising.Products.DraftProduct emphasis primary icon plus
authorize CanCurateCatalog

A screen’s toolbar belongs to that screen. Catalog’s product list carries a primary button that opens the slice’s dialog:

toolbar
toolbar actions
button new title "New product" slice Merchandising.Products.DraftProduct emphasis primary icon plus

There are 23 toolbar buttons across the domains, mostly in Purchasing (10) and Warehouse (7).

Screens fill templates. The sample uses the foundation blueprint templates by reference, for example template blueprint Cratis.Blueprint.Default ListScreen version "^1":

TemplateScreensUsed for
QuickCreateDialog30the input surface of a command: new product, add line, adjust stock, …
ConfirmDialog13a command that needs only a yes or no: approve or withdraw a product, release a hold, fulfil an order, pick or pack a parcel
ListScreen12work lists: the product catalogue, the approval inbox, purchase orders, shipments, stock levels
RecordPage9one thing in full: product page, order page, purchase order page, supplier page, stock item
DashboardScreen4the dashboards of Catalog, Sales, Purchasing and Warehouse
BoardScreen3the pricing board, the orders pipeline and the picking queue
FormScreen2the Catalog set-price and describe-product forms
WizardScreen2Storefront checkout and Purchasing “New purchase order”
SideSheet1Purchasing goods receipt
MasterDetailScreen1Purchasing suppliers
own ProductGrid, SideSummary1 and 3Storefront browse; basket, product page and order tracking

Command input is a form declared once and found by its command (form DescribeProductForm for DescribeProduct), with a two-column layout where it helps.

A command’s slice owns the screen the person sees when they run it: a QuickCreateDialog for a command that asks for input, a ConfirmDialog for one that only needs a yes. The board draws it under the persona who may run the command, and the slice previews and plays on its own. Only the commands a reaction invokes on its own (confirm an order, clear a basket after checkout, take dispatched stock, snapshot the stock level, raise the reorder alarm, reserve stock units, plan a shipment) have no screen: nobody runs them.

The screen where the persona works reaches the command, in the way that fits it:

SurfaceExampleWritten as
toolbar button that opens the command’s dialogCatalog “New product”, Purchasing “New purchase order”, “Register supplier”, Warehouse “Register SKU”button new title "New product" slice Merchandising.Products.DraftProduct
action on the selected row of a listWarehouse “Adjust stock”, “Count”, “Dispatch”; Purchasing “Choose supplier”action AdjustStock label "Adjust stock" under a toolbar
state-dependent action on the selected rowCatalog “Approve for sale” and “Withdraw” on the product lists; Warehouse “Pick order”, “Pack”, “Cancel shipment”action "Approve for sale" with when item.status == "draft" execute ApproveProductForSale and otherwise hidden
state-dependent action on a record pageproduct page, order page (“Release hold” only while on hold, “Mark fulfilled” once shipped), purchase order (“Archive” once received or cancelled)the same guarded action, beside the data of the one record
reactionconfirm an order after approval, clear the basket after checkoutreaction ... invokes Command

Storefront does not borrow a blueprint shape for the pages that define the shop. It declares its own templates, created from the current screen and categorised under Shop. A template is a shape with a designed first draft: the slots it declares, how they share the space, and the stock components (Cratis.Blueprint.Default) it seeds each slot with, so the template editor draws a real product grid and a real summary layout - not an empty place:

screen template ProductGrid
display "Product grid"
category Shop
scopes slice stateview
fits slot content
outlet quickView
header
filters
products
arrangement flow
...
content filters
component Cratis.Blueprint.Default.filterBuilder gridFilters
...
content products
component Cratis.Blueprint.Default.dataList gridCards
property variant = "cards"
property rowActions = ["Add to basket"]
...
  • ProductGrid is a responsive grid of product cards (picture, name, price, an add-to-basket button) with a filters rail beside it (its slot is not called grid: in an arrangement grid is the layout kind). The filter builder’s value narrows the cards, and the list pages and searches itself. Browse uses it: a heading, the filter rail and the live cards.
  • SideSummary is the main region with a narrow summary beside it: line items left, the totals and the one thing to do next right. It rearranges for a compact width. Basket, ProductPage and OrderTracking use it and fill all four slots.
  • The template’s content is the designed first draft the template editor draws. A screen that uses the template fills every slot it shows: what a screen puts in a slot is what is drawn there, and a screen preview draws only what the screen filled - a slot the screen leaves out is not drawn from the template’s draft. Browse therefore fills filters (the same filter builder as the template’s) as well as header and products, and Basket, ProductPage and OrderTracking fill all four slots of SideSummary.

See layout arrangement.

The Acme.Commerce package is plain ES modules with React left external, so the host’s own React is used and there is no build step. It declares three components, authored as component Acme.Commerce.<name>:

ComponentUsed by
productCardStorefront Browse (the product grid) and the Catalog product page
stockBadgeWarehouse stock levels
orderTimelinethe Sales order page
component Acme.Commerce.productCard card
property name from query ProductById.name
property sku from query ProductById.sku
property price from query ProductById.price

The package declares itself in index.js: the export acmeCommerceRuntimePackage holds a manifest (Acme.Commerce, version 1.0.0, kind ComponentLibrary, components productCard, stockBadge, orderTimeline) and the three components keyed Acme.Commerce:<component>.

Rendering a profile that names a package the host does not ship needs an approved package set: a JSON file that says which npm module carries which components (the format is in the Scene documentation, “Approved package sets”). render-variant.sh reads it from Samples/AcmeCommerce/tools/packages/acme-commerce.package-set.json when PACKAGE_SET=1; it names the npm module @acme/scene.commerce, the export acmeCommerceRuntimePackage and the package’s style sheet. With it, cratis render adds @acme/scene.commerce to the generated application’s dependencies and writes its approved packages file. The repository ignores every packages folder (**/packages/* in .gitignore), so this one file is added with git add -f; a fresh checkout has it.

Terminal window
# the whole application: 56 files, 0 errors, 0 warnings
screenplay --warnaserror Samples/AcmeCommerce
# every specification
screenplay test Samples/AcmeCommerce

--warnaserror turns warnings into failures; the sample is held at zero. The compiler reports 19 informational diagnostics (PLAY0469: an event copies a command identifier into its payload although that identifier already identifies the event source), which are not warnings. screenplay test ran 169 specifications: 168 pass and 1 is reported unsupported - ReceivingAnOrderFromTheStorefront in Sales, because the reference evaluator cannot use the composite capture field lines as a scalar. The exit code of test is therefore 3, not 0. cratis screenplay validate Samples/AcmeCommerce reports the model valid as well.

Studio imports one event model per domain. tools/domain-package.sh assembles a single domain as a standalone model - the domain line, Shared/ and the domain folder - proves that it compiles with --warnaserror and passes its specifications, and writes <Domain>.zip:

Terminal window
SCREENPLAY=screenplay Samples/AcmeCommerce/tools/domain-package.sh Catalog /tmp/acme-commerce-packages

For Catalog that is 11 files compiled with 0 errors and 0 warnings, 20 specifications passed, and a ZIP of 16 files. SCREENPLAY names the compiler command (the default is screenplay on the path).

A standalone domain has no name clash, so the package gives every inbound contract its published name back (ProductPublishedAtStorefront becomes ProductPublished); set KEEP_LOCAL_NAMES=1 to keep the local names.

tools/render-variant.sh builds a renderable variant and runs cratis render:

Terminal window
Samples/AcmeCommerce/tools/render-variant.sh # custom components replaced by core tables
PACKAGE_SET=1 Samples/AcmeCommerce/tools/render-variant.sh # with @acme/scene.commerce, approved by the package set

It exists because Stage cannot render everything the language says yet. The variant is Catalog and Storefront by default (DOMAINS="Catalog Storefront"); the header of the script lists every difference from the source and why:

  • Stage renders state-change, state-view and automation slices, not Translate slices or public events. The variant turns each inbound private event into a bridge command, then deletes translation/.
  • Stage refuses reaction batches atomic and protected reads; the variant strips them and drops the specifications that exercised a removed guard.
  • Without PACKAGE_SET=1 the three custom components are replaced by core tables and the package is removed from the profile.

For Catalog and Storefront this was taken all the way (with cratis 3.75.0, as recorded in the sample README): cratis render --package-set, dotnet build, npm run build, the generated dotnet test (90 of 90), and a browser walk of the screens with the hierarchical sidebar, the Shop topbar override and the toolbar button. Sales, Purchasing and Warehouse are not in the rendered variant: a reaction that invokes a command which gained inputs from a removed reads cannot be satisfied from its trigger payload.

The rendered application is built and tested like any other application:

Terminal window
cd <render destination>
dotnet build
npm install
npm run build
dotnet test AcmeCommerce.Specs

cratis run [PATH] starts a local Stage sandbox for a .play file or a folder; its options include --port, --package-set and --packages for approved custom packages. The sample README records that a live Stage cannot load a custom package that was not built in, and the renderer limits above apply to Stage too: it does not run Translate slices or public events. This page therefore makes no claim that the whole five-domain application runs on Stage; what is proven is the Catalog and Storefront variant above.

The Studio project is filled in two ways. The Screenplay import brings in what the .play files say, one event model per domain. The loader adds what only Studio holds: the project skeleton, personas, brainstorming boards and sticky notes. See Share a public model with Studio for the import and export of a model.

Studio featureWhat the sample shows
Event model board (one per domain)the slices of the domain as an event model: commands, events, read models and screens, with the specifications and the sample data behind the screens
Public eventsthe twelve contracts, each in its publisher and, under its consumer-local name, in every consumer
Concepts boardthe shared concepts and the domain’s own, with their validation rules; the loader adds sticky notes saying why the rules exist
Constraints boardthe eleven constraints of the sample, with notes on why the rules exist
Event sourcesthe shared event sources and their streams
Personas and policiesthe seven personas with their application roles; the policies each persona holds
UI profilethe shell: the AppShell root template, the Acme theme, the packages
Brainstorming boards“Acme big picture” (Storefront domain) and “How we got to ” for each domain: a chaos storm on the left, then a column per lane, the public events, the rules and why they exist, what became the event model, and what was parked on purpose
Sticky notesthe open questions, risks, decisions and assumptions on those boards, and the explanations on the concept and constraint boards
Chatsone conversation per board, in which the personas debate a rule (for instance the SalesRep and SalesManager on a discount over the rep’s limit)

Sticky notes in Studio have text only, no colour. The loader therefore prefixes each note with its kind - [QUESTION], [RISK], [DECISION], [ASSUMPTION] or [NOTE] - so every note reads in plain text.

The Studio project is loaded in stages (skeleton, import, studio-only, verify), and the loader’s verify stage reads back what is present and what is missing. Some of what the table lists depends on importer changes that are still in review (next section), so a page in Studio can show less than the table says; the .play source is the reference.

Not every construct comes through the Studio import yet. Each gap is tracked in Cratis/StudioIssues; the state below is the state of the issue and of its pull request when this page was written.

GapIssueState
Public events, event origins and Translate slices (direction, captures, outbound projections)StudioIssues #758open; the import pull request is open
Automation reactions, triggers, schedules and what they produceStudioIssues #760open; the import pull request is open
Personas, authorize use sites and given callerStudioIssues #762open; the persona and the access parts of the import are merged
The UI profile, theme, root template and own templatesStudioIssues #763open; the pull request for theme and packages is open
Import hardening: enum and Int concepts, keyed stream routes, placed-file imports, .stringsStudioIssues #764open; placed files, .strings and the latest event generation are merged, whole-number and enum concepts are open
Cross-domain screen navigation and several event models in one Play sessionStudioIssues #765open
Brainstorming board tools for regions, chats and layout; sticky notes on persona and event-source boardsStudioIssues #766open

The loader works around what it must until these land: it adds the personas and application roles itself, and it lays out the concept and constraint boards (an imported card is placed at the origin, StudioIssues #775). Look at a Studio page before you rely on it: where the import does not carry a construct yet, the .play source is the truth.