---
title: Render plans
editUrl: https://github.com/Cratis/Stage/edit/main/Documentation/reference/render-plans.md
description: Cratis Stage render plans resolve Scene UI profiles and record artifact ownership, diagnostics and required removals before publication.
lastUpdated: 2026-11-05T00:00:00.000Z
---


A Screenplay application is deliberately target-agnostic. A `screen` names components and slots; it never names
a package, a theme or a shell. That is what makes the same screen work on the web and on a phone — and it means
something has to decide, per target, which component vocabulary is active, which shell is in force, which theme
applies, and how everything lays out. A **render plan** is that decision, made once per target, before anything
is emitted.

A plan is produced from a `ui profile`. Each one names its platforms, its packages, and the shell and theme it
selects:

```screenplay
ui profile Admin
  target platform web, ios
  layout AppShell
  theme Aurora

  packages
    PrimeReact
```

## What a plan is resolved against

Every resolution rule a plan applies comes from `Cratis.Scene.Engine` — the same engine a running Scene renderer
uses. Stage sequences the calls and reports what they return; it implements no breakpoint, priority or variant
rule of its own, so a plan and a running application can never disagree about what a screen resolves to.

```mermaid
flowchart TB
    Play[["📄 *.play"]] -->|"compile · merge"| App[["🎬 SceneApplication<br/>target-agnostic"]]
    Catalog[["📦 package catalog"]] --> Plan
    App --> Plan{{"RenderPlanner.Plan"}}
    Plan --> Web["🖥️ RenderPlan · Admin (web)"]
    Plan --> Ios["📱 RenderPlan · Admin (ios)"]
    Web --> Findings["⚠️ findings"]
    Ios --> Findings
```

The catalog is the one input a `.play` file cannot supply. A profile lists package *names*; the declarations
behind those names — what each package contains, depends on and ships — live outside the document, so the caller
passes them in as `ScenePackage` records.

## Producing a plan

`EventModelLoader` compiles the model and resolves it in one call:

```csharp
using Cratis.Scene.Model.Packages;
using Cratis.Stage.Contracts;
using Cratis.Stage.Contracts.Scene;

IReadOnlyList<ScenePackage> catalog =
[
    new ScenePackage("core", "1.0.0", PackageKind.ComponentLibrary, [], ["core:title", "core:table"], [], [], [], []),
    new ScenePackage("Tailwind", "3.0.0", PackageKind.Styling, [], [], [], [], [], []),
    new ScenePackage("PrimeReact", "1.2.0", PackageKind.ComponentLibrary, [new PackageDependency("Tailwind")], ["core:table"], [], [], [], ["Aurora"]),
];

var plan = await EventModelLoader.LoadRenderPlanFromDirectoryAsync("./model", catalog);

foreach (var target in plan.Targets)
{
    Console.WriteLine($"{target.Profile.Name} ({target.Profile.TargetPlatform}): {string.Join(", ", target.Profile.Packages)}");

    foreach (var finding in target.Findings)
    {
        Console.WriteLine($"  {finding.Kind}: {finding.Message}");
    }
}

if (!plan.IsComplete)
{
    Console.Error.WriteLine("Some targets did not fully resolve.");
}
```

When you already hold a translated application — because you are also inspecting the screens, or planning the
same model against more than one catalog — plan it directly:

```csharp
var application = await EventModelLoader.LoadSceneApplicationFromDirectoryAsync("./model");
var plan = RenderPlanner.Plan(application, catalog);
```

Both throw `InvalidEventModel` when the source does not *compile*. Nothing that happens after that point
throws — see [Findings](#findings) for why.

## What a plan contains

`ApplicationRenderPlan` carries one `RenderPlan` per target in `Targets`, plus `Findings` for anything wrong
with the application as a whole, and `IsComplete` when nothing anywhere was left unresolved.

Each `RenderPlan` carries:

| Member | What it holds |
| --- | --- |
| `Profile` | The target, with `Packages` replaced by the resolved transitive closure in ascending override-priority order — the list every other member was resolved against. |
| `Packages` | The `PackageSelection` behind that list: what was `Added` on the target's behalf, and any `Missing`, `VersionConflicts` and `Cycles`. |
| `LayoutName` | The name of the shell this target renders inside, or `null` when nothing in scope declares the one it selects. |
| `Layout` | The shell's structure, when the application declares it. `null` with a `LayoutName` set means an active package provides the shell — a catalog carries names, never structures. |
| `Theme` | The theme this target applies, or `null` when it selects none. |
| `ThemePackages` | The active packages the theme's tokens actually apply to, so a renderer scopes tokens instead of applying them globally. |
| `SizeClass` | The size class the arrangements below were evaluated at — the target's `target size`, or the class Scene's own calculator yields at its default breakpoints when the target declares none. |
| `Components` | One `ComponentResolution` per component name the application references, naming the package it resolved to and every package that also declared the name but was shadowed. |
| `ScreenTemplates` | The `ScreenTemplateResolution`: where each screen template nests inside `Layout`, and anything `Unplaced` or in a cycle. |
| `Arrangements` | One `ArrangementSelection` per slot-bearing structure: the flow tree, slot variant or element variant that applies at `SizeClass`. |
| `Findings` | Everything this target could not fully resolve. |
| `IsComplete` | Whether `Findings` is empty. |

The separate Cratis `ArtifactRenderPlan` carries generated files and diagnostics rather than
Scene resolution findings. Its `Outcome` is `Complete`, `Partial`, or `Refused`. With explicit
render-around opt-in, `Refusals` lists each omitted specification's semantic identity, kind,
and diagnostic codes in ordinal identity order. Original errors remain, so a partial plan's
`Success` is false. No planned file realizes a refused identity through its `Sources`.
The output digest includes `refusals-v1` only for nonempty refusals; empty refusals preserve
existing digests. `CratisPlanResult` exposes the same outcome and refusals.
See [Render around refused specifications](/stage/reference/stage-rendering/#render-around-refused-specifications)
for the exact opt-in and refusal boundary.

## Degraded owner omissions

When Stage renders around an unsupported event member in an audited ESM version,
`Outcome` is `Degraded`, not `Complete`. `Refusals` identifies the root event (`STAGE-DEGRADE-001`) and every omitted
dependent (`STAGE-DEGRADE-005`). No emitted artifact's `Sources` includes an omitted owner.
Selected omitted constructs appear in the semantic inventory as `GraphProvenOmittedLeaf`
placeholders, with dependent entries linked to their root events. Report format version 1
is unchanged. These placeholders describe missing code; they are not generated stubs.

Strict mode retains the original whole-render refusal and emits no artifacts. See
[Rendering around unsupported event members](/stage/reference/stage-rendering/#render-around-unsupported-event-members)
for eligibility and remedies.

## Specification case tables

The Cratis artifact render plan emits one specification class per named case in a Screenplay
specification table. Each effective specification is named `<Spec>_<Case>` and renders as
`when_<spec>_<case>.cs`. Case parameter values replace `case.<parameter>` references before rendering.
If two derived names normalize to the same C# identifier, the plan refuses them with
`STAGE-CRATIS-004` rather than overwriting a specification file.

## Findings

A finding is reported, never thrown. Resolution keeps going and the plan comes back complete with everything
that *did* resolve, so one pass shows every problem across every target rather than stopping at the first — and
so your build decides which findings are fatal.

This is deliberately a different mechanism from a compile error. Screenplay's compiler answers "is this source
valid?", and Stage throws `InvalidEventModel` when the answer is no. A finding answers a question the source
alone cannot: "is this valid source *enough* for this particular target?". It belongs to a target, not to a span
of text, which is why it lives on the plan.

| Kind | What it means |
| --- | --- |
| `NoTargetDeclared` | The application declares no `ui profile`, so there is no deployment target to render for. |
| `PackageNotInCatalog` | A package the target activates has no declaration in the catalog, so nothing it contributes can be resolved. Includes `core`, which is always active as the final fallback. |
| `PackageDependencyMissing` | A package the target activates depends on a package nothing in the catalog satisfies. |
| `PackageVersionConflict` | A dependency resolved by name, but to a version its declared range does not accept. |
| `PackageDependencyCycle` | Packages depend on each other, so no override priority exists between them and name collisions have no defined winner. |
| `LayoutNotFound` | The target selects a shell neither the application nor any package it activates declares. |
| `ScreenNotOnSelectedLayout` | A screen resolved against a different shell than this target selects. A screen carries one layout name, so a target selecting another shell would render it somewhere it was never resolved for. |
| `ThemeNotFound` | The target selects a theme the application does not declare, so no tokens apply. |
| `ThemeIncompatible` | The theme is not declared compatible with a package the target activates — that package's components render unthemed or wrongly themed. |
| `ComponentNotResolved` | A component name the application references resolves against no active package: a hole in the screen where that component should be. |
| `ScreenTemplateUnplaced` | A screen template's `fits slot` names no single container — nothing declares that slot, or several do and the name is ambiguous. |
| `ScreenTemplateCycle` | Screen templates nest inside each other, so no tree can be built from them. |
| `StructureInvalid` | Scene's structure index could not index the application's modules, features and slices, for example because two siblings share an id. Navigation targets and toolbar buttons resolve against that index, so they are not trustworthy until it is. |
| `NavigationInvalid` | Scene's navigation diagnostics found a problem in a `navigation` region: a target that does not exist or is out of a module's scope, a slot no layout declares, a duplicate item id, or a module with no feature to go to. The message carries Scene's code, such as `NavTargetMissing`. |
| `ToolbarButtonInvalid` | Scene's toolbar validation found a problem in a `button`: a slice that does not exist, a slice that is not a State Change slice, or a button that names no slice. The message carries Scene's code, such as `ToolbarButtonSliceNotStateChange`. |
| `SampleDataInvalid` | Scene's sample data validation found a problem with a pack or a profile's selection: two pack names that derive the same id, an invalid locale, a repeated shape, or an active, mapped or bound pack that is not available. Design and preview time only; a production build is unaffected. |
| `SizeClassVariantMissing` | A freeform arrangement declares no variant for the size class this target renders at. Freeform has no fallback variant by design, so those slots have nowhere to go. |

## One invocation, one plan per target

An application ships several targets — a web app and a companion mobile app are two `ui profile`s, and a single
profile naming two platforms is two targets as well. Stage plans **all of them from one invocation**, and never
runs a build per target.

The targets come out of a single compile of a single `.play` source set. Planning them together is the only way
they are guaranteed to describe the same application; a build per target compiles the same source once per
target and can only compare the outcomes afterwards, if at all. It is also what makes "this template is
unplaceable on the phone but fine on the web" a question the build can answer at all, and what lets a build fail
once, coherently, with every target's findings in hand.

Planning together does not mean emitting together. `Targets` is a list and every entry is independently
emittable, so shipping the web bundle now and the mobile package later is a filter over the plan rather than a
second build:

```csharp
var mobile = plan.Targets.Where(target => target.Profile.TargetPlatform is "ios" or "android");
```

The split belongs at emission, where the artifacts genuinely differ — not back at compilation, where they do
not.

## Artifact manifest for code rendering

`ArtifactRenderPlan` is the separate, destination-independent code-rendering contract (not the Scene `RenderPlan` above). Its `Artifacts` are ordered by normalized relative path. Each `PlannedArtifact` carries exact `Bytes`, their lowercase `Sha256`, and `Sources`: sorted, distinct `SemanticId`s of the model elements that file realizes. Scaffold files have empty `Sources` because they realize no semantic element; a Scene composition carried as a profile input is the exception and names the application identity. The plan carries the model's `SemanticRevision`; SHA-256 hashes only artifact bytes, not sources or plan metadata. Use path, sources, and hash together when recording a mapping from model elements to generated files.

### Self-reducing model ownership

With the explicit `self` reducer convention, the existing slice file contains the
read-model record, queries, public static `Reduce` overloads, and non-public typed-context
transition helpers. No separate reducer class or file is planned. The combined file's
`Sources` includes its declaring slice, models, queries, projections, and the observed
event contracts that previously belonged to the separate reducer file. The generated-type
inventory likewise contains no nonexistent reducer class.

The default classic layout retains its existing bytes, paths, and ownership. A convention
input alone does not change an output-only digest when it affects no selected artifact.
Migrating an existing classic read model plans the removal of its reducer file. See
[Reducer rendering convention](/stage/reference/plan-from-sources/#reducer-rendering-convention) and
[Required removals](#required-removals).

### Required removals

`ArtifactRenderPlan.RequiredRemovals` lists files the publisher must delete together with
writing the plan's artifacts. Each `ArtifactRemoval` has a portable `RelativePath`, the
lowercase SHA-256 the file must still have (`ExpectedSha256`), and the `Model` whose
artifact replaces it. Removals are normalized like artifact paths and sorted ordinally.
A plan refuses removals that collide with an artifact path or with each other ignoring
case, removals whose model has no planned artifact, and removals on a plan with errors.
An empty list leaves the plan digest unchanged; a nonempty list is part of the digest.

A publisher must delete each removal atomically with its writes, refuse when the file's
hash differs, and treat an already absent file as removed so recovering an interrupted
publication stays idempotent. `CratisRendering.CheckPublication` returns
`CratisPublicationCheck.UnsafeRemovals` (`STAGE-REDUCER-005`) for a changed file and for a
migrated read model's previous reducer file that the plan would leave behind.

### Source ownership inventory

The Cratis renderer records the following identities in `Sources`. The plan normalizes every list by removing duplicates and sorting identities ordinally; list order does not imply ownership priority.

| Artifact | Sources |
| --- | --- |
| Combined StateChange slice file | Declaring slice, its commands and event declarations. |
| Combined StateView slice file | Declaring slice, its read models, projections and queries. |
| Combined Automation slice file | Declaring slice, its commands, events, read models, projections and queries. |
| Separate reducer | Target read model, observed event declarations and declaring slice. An input event's declaring slice is not added. |
| Generated specification files | Specification and its declaring slice, for command, read-model, query and reaction scenarios, including case-table artifacts. |
| Separate reactor | Reaction identity; its declaring slice is not explicitly included. |
| Common concept or composite type | Declaration identity. |
| Constraint | Declaring slice and target event declarations. |
| Event-source registration | Event-source identity and its stream identities. |
| Typed-context wrapper | Operation identity, when present; otherwise empty. |
| Operation policy and policy-context wrapper/body | Protected command or query identity. |
| Shared policy registrations and policy-context runtime/bodies | Application identity. |
| `<slice-folder>/Screens.scene.json` | Declaring slice identity only, when the profile carries screen partitions. Bound commands or queries do not change the owner. |
| `scene.json`, `src/bindings.ts`, `src/stage-scene.json`, `src/stage.ts` | Application identity only, for both authored and default Scene composition; emitted at application scope, and by a scoped plan whose profile carries the user interface input. |
| Scaffold | Empty, except application locale inputs, which name the application. |

`Sources` preserves provenance, not exclusive folder ownership. A reducer observes event declarations from other slices without becoming those slices' reducer. Consumers selecting slice-owned business files should use the explicit declaring slice where provided and model mapping for artifacts such as reactors. App-scope Scene files remain application-sourced: select the declaring slice's `Screens.scene.json` for its screen code instead of presenting the whole application UI.

Screenplay ESM v1 gives constraints and named policies no individual semantic identities. Cratis maps constraint files to their owning slice and target event identities, and generated policy registrations to the protected command/query identities. Source-only enrichment changes the plan digest and manifest metadata, but not artifact bytes or their `Sha256` hashes.

## Slice-owned screen fragments

Call `CratisRendering.WithScreenPartitions(profile, compilation)` with the compilation that owns the profile's translated Scene before planning. When no screens are authored, the input carries an empty partition without requiring a Scene payload. The optional `cratis-authoring:screen-partitions` input is versioned and hashed. It retains the compilation's sources and identity catalog because the executable model contains no screen declarations. Planning recompiles that authority in memory to verify each declaring slice and the canonical translated screen payload. The input records real slice identities, screen records and app-scoped inherited contributions; it invents no screen or component semantic identities. Legacy profiles without this input retain their existing artifacts and receive no inferred ownership.

Each declaring slice gets `<slice-folder>/Screens.scene.json` containing a `screens` array. Screen keys, component IDs, forms and bindings remain in the records. Templates, shared shell data and inherited module/feature contributions remain app-scoped; fragments are Scene code, not standalone applications or React components. The application Scene is assembled from those same partitioned records, with inherited contributions restored, and retains its canonical bytes.

`CratisRendering.PlanFrom(loaded, selection, options)` emits the selected slices' fragments with the same bytes as an application-scope plan. A feature selection produces the union, including nested features. By default, scoped plans omit all four Scene aggregates rather than publish a partial application; `STAGE-SCREEN-006` explains that application scope, or the user interface option, is required to publish shared UI. A scoped plan whose profile carries the `cratis-render:user-interface` input (set through [`CratisPlanOptions.UserInterface`](/stage/reference/plan-from-sources/#user-interface-option)) emits the four aggregates composed over the slices that input names plus its own selection: only those slices' screens, commands and queries are composed, bound and imported, so the bindings never import a proxy whose slice has not been generated. With every slice present, the aggregates are byte-identical to the application-scope ones. A profile that also carries the `cratis-render:user-interface-domains` input (set through [`CratisPlanOptions.UserInterfaceDomains`](/stage/reference/plan-from-sources/#an-application-generated-one-domain-at-a-time)) adds the slices generated under the application's other domains and imports each of their proxies from that domain's folder; the input is admitted only alongside `cratis-render:user-interface`. An input naming an identity that is not a slice of the model is refused with `STAGE-SCREEN-007`. A slice without an authored screen receives `STAGE-SCREEN-005` when partitions are carried: Stage emits no slice-owned default fragment and never attributes synthetic application UI to that slice.

Admission refuses unsupported-version, stale, malformed, duplicate or unresolved partitions, missing screen coverage and disagreements with either the carried Scene or the compilation's translated screen records, without partial artifacts. Naming another existing slice as owner is refused even when it belongs to the same feature. Screen partitions add managed paths and therefore change artifact lists and output digests; rename/removal uses the normal publication ownership and drift checks.

## Authored code documentation

Render hosts that hold a `SemanticCompilation` can preserve authored descriptions and command, event, and read-model documentation by passing their resolved profile and the same compilation to `CratisRendering.WithAuthoringMetadata` before creating the `ArtifactRenderRequest` for `CratisArtifactRenderPlanner.Plan`. This preserves any Scene or strings inputs already in the profile. The model-only `CratisRendering.Plan` overloads cannot recover syntax-only metadata. The `cratis` CLI calls its metadata adapter when constructing the render profile, preserving authored documentation through `WithAuthoringMetadata`. If the compilation's paths or workspace documents do not meet the metadata reader's requirements, the CLI renders without those comments and reports warning `CLI-RENDER-006`.

Descriptions become XML-escaped `<summary>` comments on generated commands, events, read models, query methods, composite types, reactions, and specification classes. A specification description is repeated on each generated scenario class realizing that specification. Command, event, and read-model Markdown documentation becomes escaped text in `<remarks>`; it is not converted to HTML. Reaction descriptions and documentation become the generated reactor class's summary and remarks. The retained syntax renderers also preserve trigger descriptions; those have no separate generated symbol in the semantic path. Line endings, including Unicode next-line, line-separator and paragraph-separator characters, and trailing whitespace are normalized. With no authored metadata, existing generated summaries and artifact bytes are unchanged.

Metadata travels as a versioned, hashed profile input keyed by semantic identity, not as a generated sidecar file. A documentation-only edit changes the affected artifact hashes without changing the executable model revision. Planning refuses malformed, stale, duplicate, or unsupported-version metadata. Declarations without a corresponding generated symbol do not gain an artificial documentation artifact; the generated frontend scaffold is not a declaration-documentation surface.

## What a plan does not decide

- **It emits nothing.** A plan is the resolved input an emitter consumes; producing a static web bundle or a
  native app package is a separate step.
- **It does not invent a catalog.** Pass one in. With no catalog, every package is unknown and every component
  name is unresolved, which the findings will say plainly.
- **It does not resolve a shell a package only names.** A catalog carries package names and the names of what
  they contribute, never structures — so a shell provided by a blueprint is named in the plan, and its
  arrangements and template placements are not part of it.
- **It does not push findings back into Screenplay's diagnostics.** Screenplay diagnostics describe source text
  and have no target to attribute a per-target outcome to.
