Runtime¶
This page covers how a registered variant runs: its frame, its four-phase lifecycle, the functions that run it from code, the registry queries, configure!, substrate registration and mounting the shell.
The shell runs and resets variants for you as you click through the sidebar and press Run all or Re-run. A test, a custom shell or a screenshot script calls the same functions directly.
Per-variant frame allocation¶
Every variant runs in its own frame, registered under the variant's id and carrying the variant's decorator stack. The frame stays allocated after a run until destroy-variant! tears it down; any state machines the variant spawned receive their :rf.machine/destroy event as part of that teardown. Running a variant again first resets its frame in place, so every run starts from a fresh app-db while a mounted view keeps its subscriptions.
Coexistence with host application state¶
Story keeps runtime slots in every variant frame's app-db under the reserved :rf.story/* namespace:
:rf.story/lifecycle— a copy of the lifecycle machine's current state, for direct reading.:rf.story/loaders-complete?— the flag a:loaders-complete-whenevent handler sets to say the loaders are done.:rf.story/assertions— the vector of assertion records the:rf.assert/*handlers append during the run.
Your event handlers, and any other code that writes app-db, must keep the :rf.story/* keys when they seed or reset db. A handler that returns a {:db {...}} built from scratch wipes those slots and breaks every variant that runs the event. Build the new :db from the incoming one, as {:db (assoc db ...)} or {:db (merge db {...})}.
The four-phase lifecycle¶
For every run, in strict order, draining the frame's queue between steps. Before phase 1 the runtime resets the frame, applies the decorator stack's :frame-setup work and merges any :db-seed into app-db.
| Phase | Trigger | Semantics |
|---|---|---|
| 1. Loaders | Variant body's :loaders |
Dispatch each event into the variant's frame and drain. The phase is complete when :loaders-complete-when holds: by default as soon as the queue drains; otherwise a registered event id whose handler sets :rf.story/loaders-complete?, or a vector of events that must all have been dispatched. |
| 2. Setup | The plan's :setup |
Dispatch the setup events in order — an :extends parent's first, then composed fragments', then the variant's own — draining between events. |
| 3. Render | The shell or render-variant |
The view renders against the post-setup app-db with the effective args (the five-layer precedence chain) and the decorator stack (globals, then story, then variant). run and run-variant do not render. |
| 4. Script | The plan's scripts | Walk the steps in order. :rf.assert/* records accumulate in :assertions; failures don't throw. See Scripts. |
A loader that throws, or a :loaders-complete-when that never holds, records a failing assertion and parks the lifecycle at :loading; a headless run then skips setup and the script, and the run does not pass.
The execution verbs¶
All under re-frame.story. These are the verbs a test calls. The tutorial's chapter 4 shows them in a test namespace.
A test namespace that runs variants needs a substrate adapter installed and re-frame.epoch loaded, because the runner reads each run's evidence from the epoch tape. re-frame.test-support/make-reset-runtime-fixture with {:adapter re-frame.substrate.plain-atom/adapter} installs the adapter; add :async? true for cljs.test async tests.
run¶
- Signature:
- Description: Run
targetand resolve with the unified run result. A keywordtargetis a registered variant; a map is an inline plan, a variant body that is compiled, run in a fresh anonymous frame and torn down, and never registered. On the JVM the promise is aCompletableFuture, so@(run id)blocks for the result; in CLJS it is ajs/Promise. It never rejects: an unknown variant, a failed setup or a thrown handler resolves with:status :error. -
Options:
Key Value :runner:headless(default),:hiccup,:cljs-reactive,:domor:browserto fix the runner;:autoto take the cheapest one whose capabilities cover the plan. An unknown value falls back to:headless.:escalatetruemeans the same as:runner :auto.:active-modesMode ids whose args join the args chain, in order. :cell-overridesArg overrides, as the Controls panel makes them. :substrateThe substrate to render under. runexecutes the plays that run on mount: a:scriptunless it sets:auto-run? false, and the first of:playsplus any other play that sets:auto-run? true.
is¶
- Signature:
- Description: Run
targetasrundoes and report the result toclojure.testorcljs.test: one report per assertion record, plus a failing report when the run is:cannot-run,:error, or:failfrom the tape floor without a failing assertion to carry it. A run that passes with no assertions reports one pass. On the JVMisblocks and returns the result;optsalso takes:timeout-ms(default30000), past which it throws aTimeoutExceptionrather than hanging the test run. In CLJS it returns a promise that resolves with the result once it has reported, for use insidecljs.test/async. Atargetthat is already a run result is reported as it is.
report-result!¶
- Signature:
- Description: Report an already-resolved run result through
clojure.testorcljs.test, exactly asisdoes.
explain¶
- Signature:
- Description: Compile
targetwithout running it and return how it was assembled. The map carries:source,:source-chainand:parent-chain;:composeand:strict-conflicts;:merge, the strategy applied to each field;:args,:substitutions,:effective-args,:view-args-schemaand:view-args-validation;:network,:sub-overrides,:db-seedand:fidelity;:setup-order,:script-order,:checksand:assertions;:required-runner;:platforms; and:tags.optstakes:active-modesand:cell-overrides, so the args match a run under them. It throws when the target cannot compile, for example:rf.error/story-unknown-variantfor an unregistered id or:rf.error/story-compose-conflictfor a:composeconflict.
variant-plan¶
- Signature:
- Description: The compiled plan that
runexecutes and the canvas renders::variant/id,:story/id,:world(setup, scripts, args, decorators, fidelity and the other world inputs),:expect(checks and assertions),:required-runner,:tags,:source,:source-chainand:explain.explainreturns this plan's:explainwith the run-time args folded in.
render-variant¶
- Signature:
- Description: Render
target's view from its plan without running:scriptor assertions.optstakes:control-overrides, arg overrides applied on top of the plan's args. The result carries:status(:rendered,:invalid-args,:cannot-runor:error),:plan,:plan-hash,:frame,:effective-args,:validationand:rendered. An override that breaks the view's:rf/propsschema stops before the view is called, with:invalid-args. On the JVM, with no host renderer, the status is:cannot-run.
Runner capabilities¶
Each row adds to the preceding row:
| Runner | Added capabilities |
|---|---|
:headless |
:app-db, :effects, :schema, :trace, :pure-subs. |
:hiccup |
:hiccup-structure. |
:cljs-reactive |
:reactive-counts. |
:dom |
:dom. |
:browser |
:pixels, :a11y-engine. |
The required-runner set is the union required by the selected program and
assertions. A selected runner does not create unavailable host evidence:
DOM requires a document, pixel assertions require captures, and axe
assertions require an existing scan. visual-snapshot currently returns
:cannot-run under every runner. a11y can evaluate a stored browser scan.
Programmatic runtime¶
All under re-frame.story. Reach for these from a custom shell, a test fixture, a cljs.test adapter, an MCP-tool body, or any host that wants to materialise a variant outside the standard Story chrome.
run-variant¶
- Signature:
- Description: Allocate the variant's frame, run the lifecycle, and resolve with the unified run result, the same shape
runresolves with. On the JVM the promise is aCompletableFuture; in CLJS ajs/Promise. The frame stays allocated afterwards, soread-assertionsandlifecycle-statecan inspect it. Rendering isrender-variant's —run-variantproduces no rendered output.
reset-variant¶
- Signature:
- Description: Tear the variant's frame down and run it again from its declared start, resolving as
run-variantdoes.
watch-variant¶
- Signature:
- Description: Call
callbackon every lifecycle transition of the variant's frame, with{:frame-id <id> :from <state> :to <state> :event <event>}. Returns a zero-argument function that unsubscribes.
destroy-variant!¶
- Signature:
- Description: Tear down the variant's frame. Any spawned state-machines receive their
:rf.machine/destroyevent. Idempotent.
lifecycle-state¶
- Signature:
- Description: The current state of the variant's lifecycle machine — one of
:pre-mount/:mounting/:loading/:ready/:error. Returns:pre-mountwhen the variant has not been run yet.
The opts map for run-variant and reset-variant accepts:
{:active-modes [:Mode.app/dark-large] ;; coll of mode ids, deep-merged into args
:cell-overrides {:label "Override"} ;; controls-panel-shaped runtime overrides
:substrate :reagent} ;; / :uix
The result carries :status, :variant/id, :frame, :lifecycle, :runner, :required-runner, :assertions, :checks, :schema-violations, :consumed-selectors, :warnings, :app-db, :effects, :effective-args, :decorators, :images, :sub-runs, :renders, :epoch-tape, :narrative, :snapshot, :plan-hash, :run-hash and :elapsed-ms. The tutorial's chapter 4 shows one.
Args + decorator resolution¶
resolve-args¶
- Signature:
- Description: Materialise the effective args map for a variant given the active modes + cell overrides. The five-layer precedence chain (global → story → mode → variant → cell-override), deep-merged for maps, vector-replaced for vectors.
resolve-decorators¶
- Signature:
- Description: Return the variant's resolved decorator stack classified by kind:
{:hiccup [...] :frame-setup [...] :fx-override [...] :errors [...] :fingerprints {...}}. Each entry carries the reference's:id,:argsand the registered:body;:errorslists references that did not resolve. Composition order:(concat globals story variant).
variant-frames¶
- Signature:
- Description: The set of variant-ids currently allocated as frames.
variant-frame?¶
- Signature:
- Description: Predicate.
Snapshot identity + share¶
snapshot-identity¶
- Signature:
- Description: The variant's snapshot identity — the variant id plus a content hash over the canonicalised variant, its resolved args, decorators, loaders, substrate and active modes. Returns
{:variant-id ... :active-modes [...] :substrate ... :content-hash "<8 hex digits>"}.optstakes:active-modes,:cell-overridesand:substrate. Used by visual-regression keying (Local visual review) to identify what the user is looking at without leaking the variant's args. The hash computes over real values (pre-substitution); downstream emission goes throughproject-egress.
variant-share-url¶
- Signature:
- Description: Build a sharable URL for
variant-id.optstakes:active-modes,:cell-overridesand:substrate, so a paste-and-open session reproduces the cell. The one- and two-argument forms return the query string alone, asvariant=story.login-form%2Fidle; the three-argument form prefixesbase-url, ashttp://localhost:8043/?variant=story.login-form%2Fidle. The shell writes the address bar with the same encoding, so copying the address bar gives the URL this function builds. It is a pure function, on the JVM and in ClojureScript.
Assertion-side accessors¶
read-assertions¶
- Signature:
- Description: The current
:rf.story/assertionsvector forvariant-id. Each entry is a:rf.assert/*record.
assertions-passing?¶
- Signature:
- Description: Given a run result, true iff the run's
:statusis:pass, so a:cannot-runrun or a schema-floor:failis false even when every assertion record passed. Given a bare assertions vector, such asread-assertionsreturns, true iff every record has:passed? true; an empty vector passes.
canonical-assertion-ids¶
- Signature:
- Description: The eight canonical
:rf.assert/*ids as a set: the seven dispatched assertion events plus:rf.assert/schema-error.known-assertion-idsadds the DOM, a11y, visual and reactive-count ids the plan compiler also accepts.
Registry queries¶
The query family Story exposes for its own chrome, the MCP jar, and any tooling that walks the registrar's side-table.
registrations¶
- Signature:
- Description: All registrations for
kind, as a map from id to body (Story kinds::story,:variant,:workspace,:fragment,:check,:story-panel,:tag,:mode,:decorator).
handler-meta¶
- Signature:
- Description: The registered body for
id.
ids¶
- Signature:
- Description: All registered ids of
kind.
registered?¶
- Signature:
- Description: Predicate.
all-kinds-with-counts¶
- Signature:
- Description: Map from each registered kind to its count.
variants-of¶
- Signature:
- Description: Variant ids whose namespaced id-prefix matches
story-id.
variants-by-story¶
- Signature:
- Description: Map from parent-story-id to its variant ids.
variants-with-tags¶
- Signature:
- Description: The set of variant ids whose effective tags — inherited from a story or
:extendsparent, less any:!tagremovals — intersecttag-set.
list-tags¶
- Signature:
- Description: All registered tags (canonical + project).
list-modes¶
- Signature:
- Description: All registered modes.
canonical-tags¶
- Kind: Var (set)
- Description: The seven canonical tags.
canonical-axes¶
- Kind: Var (map)
- Description: The five canonical tag axes, each mapped to
{:user-extensible? bool}and, for the three with a recommended vocabulary,:values::status(#{:alpha :beta :stable :deprecated}),:role(#{:design :dev :product}),:state(#{:empty :small :medium :large :special}), and the open:teamand:feature.
canonical-status-values¶
- Kind: Var (set)
- Description: The recommended
:statusvalues,#{:alpha :beta :stable :deprecated}.
canonical-role-values¶
- Kind: Var (set)
- Description: The recommended
:rolevalues,#{:design :dev :product}.
tags-by-axis¶
- Signature:
- Description: The registered tag ids whose
:axisisaxis, such as:status; the empty set when none is.
tags-without-axis¶
- Signature:
- Description: The registered tag ids that declare no
:axis. The sidebar's tag filter shows them in its OTHER row.
tags-default-excluded¶
- Signature:
- Description: The registered tag ids whose body sets
:default-filter :exclude.
tag->axis-index¶
- Signature:
- Description: Map from every registered tag id to its axis; a tag with no axis maps to
:re-frame.story.registrar/no-axis.
registered-substrates, the substrate set, sits with register-substrate! under Substrate registration.
configure!¶
The boot-time entry point for project-wide defaults. The host calls it once before mounting the shell.
configure!¶
- Signature:
- Description: Set Story's global config. Every key is under
:rf.story/*, and the set is closed: an unknown key, such as the typo:rf.story/edtior, throws:rf.error/unknown-story-config-key.
Every key:
(rf.story/configure!
{;; Args — Layer 1 of the five-layer precedence chain
:rf.story/global-args
{:theme :light :locale :en}
;; Decorators — the project-wide prefix of every variant's stack
:rf.story/global-decorators
[[:app/theme-provider :dark]
[:app/locale-provider :en]]
;; Editor — drives the source-coord 'Open in editor' chip
:rf.story/editor :cursor ;; / :vscode (default) / :idea / {:custom <tpl>}
;; On-disk root — prepended to classpath-relative source-coord :file slots
:rf.story/project-root "/path/to/my-app"
;; On-box dev-UI egress profile — the per-(tool, frame) privacy boundary
:rf.story/egress-profile :rf.egress/local-redacted})
Two keys reach beyond Story:
:rf.story/project-rootis passed on to Xray. Story copies the value into Xray's:rf.xray/project-rootthroughre-frame.story.xray-preset/propagate-project-root!, so the source links in the embedded Xray resolve against the same root. The copy runs one way: to point Xray at a different root, callxray-config/configure!afterrf.story/configure!.:rf.story/egress-profilesets what Story's own panels show of sensitive values. The value is one of the six:rf.egress/*profiles; the two meant for your own machine are:rf.egress/local-redacted, the default, which hides values at sensitive paths, and:rf.egress/local-raw, which shows them. The recorder, the per-variant trace buffer and the assertion listeners all pass values throughre-frame.core/project-egressunder this profile, and show a[● REDACTED]hint where they redact. The profile applies to Story and its frames; there is no process-wide switch. An unknown profile raises:rf.error/unknown-egress-profile;nilresets to the default.
Egress profile values¶
:rf.story/egress-profile accepts the six framework profiles:
| Profile | Intended output |
|---|---|
:rf.egress/local-redacted |
Local panels with classified sensitive data hidden; default. |
:rf.egress/local-raw |
Trusted local inspection including sensitive and large values. |
:rf.egress/off-box-observability |
Observability output. |
:rf.egress/off-box-tool |
External tool output. |
:rf.egress/ssr-hydration |
Serialized state for client hydration. |
:rf.egress/public-error |
Public error payloads. |
nil resets to the default. Unknown values raise
:rf.error/unknown-egress-profile. The recorder still redacts credential
input text. Share URLs, copied EDN and screenshots carry the values you
choose to share; the panel profile is not a redactor for those artifacts.
Substrate registration (CLJS-only)¶
register-substrate!¶
- Signature:
- Description: Register a substrate render fn under
substrate-id.render-fntakes(variant-id view-id args)and returns a hiccup vector (Reagent) or a React element (UIx). The host calls this once at boot for each substrate it wants Story to render against (:uix, etc.). The:reagentsubstrate is registered automatically by the canonical-vocabulary auto-install.
registered-substrates (substrate registration)¶
- Signature:
- Description: The set of registered substrate ids. Used by tooling that enumerates available substrates for a variant's
:substratesopt-in.
Shell lifecycle (CLJS-only)¶
The shell is Story's UI. The host calls mount-shell! from its entry namespace, typically when the page's hash is #/stories.
mount-shell!¶
- Signature:
- Description: Mount the Story shell at
dom-nodeand return its handle,{:root <react-root> :node <dom-node>}. One shell at a time: mounting while a shell is mounted tears the previous one down first. There is no options map — what the shell opens on (selected variant or workspace, mode tab, modes, viewport, background) is read from the page URL's query parameters at mount, over a localStorage fallback. Returns nil without any DOM call whendom-nodeis nil or in production builds (re-frame.story.config/enabled?false).
unmount-shell!¶
- Signature:
- Description: Unmount the active shell, or the shell named by
handle(the valuemount-shell!returned). Idempotent.
active-shell¶
- Signature:
- Description: The active shell's handle, or nil when no shell is mounted.
Static-mode probe¶
static-mode?¶
- Signature:
- Description: True iff Story is running in static-export mode (the bundle was built with
:closure-defines {re-frame.story.config/static-mode? true}). In static mode the shell stops polling for new registrations and does not open the first-visit help overlay. Call it to render something only in a published build, such as a badge.
Coeffects registered by Story¶
An event handler can read the toolbar's state through two coeffects, declared under :rf.cofx/requires. The same two ids are registered as subscriptions, for a view.
| Cofx id | Shape | Notes |
|---|---|---|
:story/active-modes |
[<mode-id> ...] |
The toolbar's active modes. |
:story/active-args |
{<arg-key> <value>} |
The deep-merge of the active modes' :args, the mode layer of the precedence chain. |
See also¶
- Registration — the registration macros that populate the registrar
run-variantwalks. - Scripts — phase 4's full grammar;
read-assertions/assertions-passing?consumers. - MCP surface — the same fns above, consumed by the
tools/story-mcp/jar over JSON-RPC. - Story tutorial — Your first variant —
mount-shell!in context. - Story tutorial — Snapshot identity and sharing —
snapshot-identity+variant-share-urlin worked usage. - Framework API — Lifecycle —
rf/init!runs beforemount-shell!. The adapter must be installed before Story attaches. - Xray API — Configuration keys —
:rf.xray/project-root, the slot Story's:rf.story/project-rootbridges into.