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:
| Domain | Module | Slices (change / view / automation / translate) | Screens | Specifications |
|---|---|---|---|---|
| Catalog | Merchandising | 5 / 5 / 0 / 3 | 10 | 18 |
| Storefront | Shop | 9 / 6 / 1 / 6 | 15 | 27 |
| Sales | Orders | 8 / 6 / 2 / 7 | 15 | 30 |
| Purchasing | Buying | 12 / 6 / 1 / 3 | 20 | 34 |
| Warehouse | Stock | 18 / 6 / 5 / 9 | 24 | 58 |
The shell adds the Application module (2 slices, 4 screens, 2 specifications). Together that is 169 specifications.
Where to find it
Section titled “Where to find it”| What | Where |
|---|---|
| The Screenplay source | Samples/AcmeCommerce in this repository |
| The Studio project | the project Acme Commerce with the application Acme Commerce Shop, in the Cratis tenant of Cratis Studio |
| The loader that fills the Studio project | scripts/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.
How it was built
Section titled “How it was built”The sample was written in this order, and each step compiles on its own:
- The shared kernel (
Shared/): concepts, event sources, policies and personas. - The shell of each domain (
<Domain>/shell/): its theme, UI profile, navigation with logo and sample data. - Catalog first, because it publishes the first contracts (P1 to P3).
- Storefront, which only listens to Catalog and then publishes the first order (P4).
- Sales, Warehouse and Purchasing, which close the loops: order to cash to dock, and replenishment.
- The custom component package, the sample data and the tooling that packages a domain for Studio and builds a renderable variant.
One application, five folders
Section titled “One application, five folders”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 packassets/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.commercetools/ domain-package.sh, render-variant.sh, render-bridge/, packages/Four rules about composing a folder into one application shaped the layout:
- One
domainline per tree. Onlyapplication.playhas 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- andfrom "<Origin>"keeps the lineage. - No
..imports.Shared/is a sibling folder; compiling a folder walks every.playfile 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.
The shared kernel
Section titled “The shared kernel”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.
The public events (P1 to P12)
Section titled “The public events (P1 to P12)”These are the contracts. A public event is the only thing that crosses a context boundary.
| # | Public event | Published by | Consumed by |
|---|---|---|---|
| P1 | ProductPublished | Catalog | Storefront, Warehouse |
| P2 | ProductPriceChanged | Catalog | Storefront, Sales |
| P3 | ProductRetired | Catalog | Storefront, Warehouse |
| P4 | OrderPlaced | Storefront | Sales |
| P5 | OrderConfirmed | Sales | Warehouse, Purchasing |
| P6 | OrderStatusChanged | Sales | Storefront |
| P7 | StockReserved | Warehouse | Sales |
| P8 | StockReservationFailed | Warehouse | Sales |
| P9 | StockLevelChanged | Warehouse | Storefront |
| P10 | StockBelowReorderPoint | Warehouse | Purchasing |
| P11 | PurchaseOrderReceived | Purchasing | Warehouse |
| P12 | ShipmentDispatched | Warehouse | Sales |
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).
Private versus public events
Section titled “Private versus public events”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 = 0Catalog’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 between the domains
Section titled “Reactions between the domains”Reactions are the automations inside a domain that hear a private event and invoke a command. Examples that run across the whole flow:
- Storefront:
CheckoutCompletedinvokesClearBasketContents, which empties the basket. - Sales: an
OrderReceivedbelow 500 invokesApproveOrder(where quotedTotal < 500), and anOrderApprovedinvokesConfirmOrder, which is what produces P5. - Warehouse:
ReservationRequestedinvokesReserveStockUnits; a drop in available stock invokesRaiseReorderAlarm, which is what produces P10; a reservation plans a shipment; a dispatch takes the stock off the shelf. - Purchasing:
ReplenishmentRequested(from P10) invokesCreateReplenishmentPurchaseOrder.
reaction SmallOrderAutoApproval runs as system role "SalesManager" when OrderReceived orderId quotedTotal invokes ApproveOrder orderId = orderId where quotedTotal < 500A reaction runs as a stated role (runs as system role ...), so the policies of the command it invokes still apply.
Domain by domain
Section titled “Domain by domain”Catalog (Merchandising)
Section titled “Catalog (Merchandising)”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,ProductReadinessandCatalogFigure. - A decision against state.
ApproveProductForSalereadsProductReadinessand refuses with “Describe the product before approving it” or “Price the product before approving it”. - Constraints.
UniqueSku(unique sku on ProductDrafted) andOneApprovalPerProduct(unique event ProductApprovedForSale,released by ProductWithdrawn- a withdrawn product may be approved again). - Public events. P1, P2, P3 through
PublishProduct,PublishPriceChangeandPublishRetirement.
Storefront (Shop)
Section titled “Storefront (Shop)”The Shopper browses, fills a basket and checks out. Features: Browse, Basket, Checkout, MyOrders.
- Inbound.
ListProduct(P1),UpdateDisplayPrice(P2),DelistProduct(P3),UpdateAvailability(P9) andTrackOrderStatus(P6). - Outbound.
PublishOrderturnsCheckoutCompletedinto P4. - Rules.
AddToBasketreads the listing and refuses a sold-out product;UniqueCustomerEmail;EmailAddressmust contain@. - A multi-step flow. Checkout is a wizard whose three steps (
EnterShippingAddress,ChooseDeliveryMethod,CheckOut) are command forms;CheckOutreads the basket size and the customer account and is refused for an empty basket. - Its own page shapes.
ProductGridandSideSummaryare screen templates the module declares itself (see below). - Navigation. The module overrides the application’s topbar with its own: Browse, Basket, My orders.
Sales (Orders)
Section titled “Sales (Orders)”Order intake, approval, confirmation and status. Features: Intake, Approval, Fulfilment, Reporting.
- Inbound.
ReceiveOrder(P4),UpdatePriceList(P2),RecordReservation(P7),HoldOrderOnShortage(P8) andRecordShipment(P12). - Outbound.
PublishOrderConfirmed(P5) andPublishOrderStatus(P6). The private event behind P5 isOrderConfirmationIssued; the public contract keeps the nameOrderConfirmed. - Policy-driven automation. Small orders approve themselves; larger ones wait in the manager’s inbox.
- Who may do what.
ApproveOrder,RejectOrder,ApplyLargeDiscountand the inbox are for the SalesManager (authorize IsSalesManager); a SalesRep’sApplyDiscountstops 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) andOneConfirmationPerOrder. - Navigation. The module extends the application’s sidebar with a Reports item.
Warehouse (Stock)
Section titled “Warehouse (Stock)”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 withReservationMadeor says it could not withReservationFailed),WatchReorderPoint(oneReorderPointCrossedper crossing; the alarm re-arms when stock comes back),SnapshotStockOnMovement,PlanShipmentWhenReservedandTakeStockWhenDispatched. - Constraints.
UniqueLocationCodeandOneDispatchPerShipment.
Purchasing (Buying)
Section titled “Purchasing (Buying)”Suppliers, replenishment and purchase orders. Features: Suppliers, Replenishment, PurchaseOrders, Demand, Overview.
- Inbound.
RecordDemand(P5) andRequestReplenishment(P10). Outbound.PublishGoodsReceived(P11). - Automation.
AutoReplenishdrafts a one-line purchase order with the SKU’s preferred supplier. It skips when an open order already covers the SKU (the constraintOneOpenReplenishmentOrderPerSkuis released by goods received or a cancellation), and when there is no preferred supplier it recordsReplenishmentBlocked, which the replenishment read model shows as statusblocked. - 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,OneSubmissionPerPurchaseOrderandOneOpenReplenishmentOrderPerSku.
Constraints
Section titled “Constraints”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.
Specifications
Section titled “Specifications”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.
The shell
Section titled “The shell”One application per domain
Section titled “One application per domain”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.
The UI profile
Section titled “The UI profile”<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| Domain | Accent | Menu mode | Menu theme | Scheme |
|---|---|---|---|---|
| Storefront | #C2410C | horizontal | light | light |
| Catalog | #4338CA | static | light | light |
| Sales | #1D4ED8 | static | dark | light |
| Purchasing | #B45309 | slimPlus | light | light |
| Warehouse | #0F766E | compact | dark | dark |
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.
Sample data
Section titled “Sample data”<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.
Navigation and the logo
Section titled “Navigation and the logo”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:
| Module | Statement | Effect |
|---|---|---|
Shop | navigation topbar overrides | the storefront has its own light topbar |
Orders | navigation sidebar extends | adds a Reports item to the sidebar |
Merchandising, Buying, Stock | none | inherit the application’s navigation |
Toolbars
Section titled “Toolbars”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 CanCurateCatalogA 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 plusThere are 23 toolbar buttons across the domains, mostly in Purchasing (10) and Warehouse (7).
Screens
Section titled “Screens”Screens fill templates. The sample uses the foundation blueprint templates by reference, for example
template blueprint Cratis.Blueprint.Default ListScreen version "^1":
| Template | Screens | Used for |
|---|---|---|
QuickCreateDialog | 30 | the input surface of a command: new product, add line, adjust stock, … |
ConfirmDialog | 13 | a command that needs only a yes or no: approve or withdraw a product, release a hold, fulfil an order, pick or pack a parcel |
ListScreen | 12 | work lists: the product catalogue, the approval inbox, purchase orders, shipments, stock levels |
RecordPage | 9 | one thing in full: product page, order page, purchase order page, supplier page, stock item |
DashboardScreen | 4 | the dashboards of Catalog, Sales, Purchasing and Warehouse |
BoardScreen | 3 | the pricing board, the orders pipeline and the picking queue |
FormScreen | 2 | the Catalog set-price and describe-product forms |
WizardScreen | 2 | Storefront checkout and Purchasing “New purchase order” |
SideSheet | 1 | Purchasing goods receipt |
MasterDetailScreen | 1 | Purchasing suppliers |
own ProductGrid, SideSummary | 1 and 3 | Storefront 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.
Every command has its own screen
Section titled “Every command has its own screen”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:
| Surface | Example | Written as |
|---|---|---|
| toolbar button that opens the command’s dialog | Catalog “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 list | Warehouse “Adjust stock”, “Count”, “Dispatch”; Purchasing “Choose supplier” | action AdjustStock label "Adjust stock" under a toolbar |
| state-dependent action on the selected row | Catalog “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 page | product 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 |
| reaction | confirm an order after approval, clear the basket after checkout | reaction ... invokes Command |
The domain’s own templates
Section titled “The domain’s own templates”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"] ...ProductGridis a responsive grid of product cards (picture, name, price, an add-to-basket button) with a filters rail beside it (its slot is not calledgrid: in an arrangementgridis the layout kind). The filter builder’s value narrows the cards, and the list pages and searches itself.Browseuses it: a heading, the filter rail and the live cards.SideSummaryis 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,ProductPageandOrderTrackinguse 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.
Browsetherefore fillsfilters(the same filter builder as the template’s) as well asheaderandproducts, andBasket,ProductPageandOrderTrackingfill all four slots ofSideSummary.
See layout arrangement.
Custom components: @acme/scene.commerce
Section titled “Custom components: @acme/scene.commerce”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>:
| Component | Used by |
|---|---|
productCard | Storefront Browse (the product grid) and the Catalog product page |
stockBadge | Warehouse stock levels |
orderTimeline | the 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.priceThe 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.
Compile, test, package, render and run
Section titled “Compile, test, package, render and run”# the whole application: 56 files, 0 errors, 0 warningsscreenplay --warnaserror Samples/AcmeCommerce
# every specificationscreenplay 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.
Packaging a domain for Studio
Section titled “Packaging a domain for Studio”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:
SCREENPLAY=screenplay Samples/AcmeCommerce/tools/domain-package.sh Catalog /tmp/acme-commerce-packagesFor 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.
Rendering it
Section titled “Rendering it”tools/render-variant.sh builds a renderable variant and runs cratis render:
Samples/AcmeCommerce/tools/render-variant.sh # custom components replaced by core tablesPACKAGE_SET=1 Samples/AcmeCommerce/tools/render-variant.sh # with @acme/scene.commerce, approved by the package setIt 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
Translateslices or public events. The variant turns each inbound private event into a bridge command, then deletestranslation/. - Stage refuses
reaction batches atomicand protectedreads; the variant strips them and drops the specifications that exercised a removed guard. - Without
PACKAGE_SET=1the 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.
Running on Stage
Section titled “Running on Stage”The rendered application is built and tested like any other application:
cd <render destination>dotnet buildnpm installnpm run builddotnet test AcmeCommerce.Specscratis 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.
What each Studio feature shows
Section titled “What each Studio feature shows”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 feature | What 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 events | the twelve contracts, each in its publisher and, under its consumer-local name, in every consumer |
| Concepts board | the shared concepts and the domain’s own, with their validation rules; the loader adds sticky notes saying why the rules exist |
| Constraints board | the eleven constraints of the sample, with notes on why the rules exist |
| Event sources | the shared event sources and their streams |
| Personas and policies | the seven personas with their application roles; the policies each persona holds |
| UI profile | the shell: the AppShell root template, the Acme theme, the packages |
| Brainstorming boards | “Acme big picture” (Storefront domain) and “How we got to |
| Sticky notes | the open questions, risks, decisions and assumptions on those boards, and the explanations on the concept and constraint boards |
| Chats | one 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.
Known gaps
Section titled “Known gaps”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.
| Gap | Issue | State |
|---|---|---|
| Public events, event origins and Translate slices (direction, captures, outbound projections) | StudioIssues #758 | open; the import pull request is open |
| Automation reactions, triggers, schedules and what they produce | StudioIssues #760 | open; the import pull request is open |
Personas, authorize use sites and given caller | StudioIssues #762 | open; the persona and the access parts of the import are merged |
| The UI profile, theme, root template and own templates | StudioIssues #763 | open; the pull request for theme and packages is open |
Import hardening: enum and Int concepts, keyed stream routes, placed-file imports, .strings | StudioIssues #764 | open; 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 session | StudioIssues #765 | open |
| Brainstorming board tools for regions, chats and layout; sticky notes on persona and event-source boards | StudioIssues #766 | open |
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.
Related
Section titled “Related”- Folders and imports: composing many files into one application.
- Reactions, captures and constraints.
- Screens, templates, navigation regions and sample data.
- Compiler and CLI.