Skip to content

re-frame2 — API

Type: Reference Reference for the CLJS implementation's API: signatures, status, cross-references. No rationale — per-Spec docs own the why. Pattern-level contracts live in 000-Vision §The pattern and the per-Spec docs. :fx-overrides asymmetry: id-valued at the pattern level; CLJS reference also accepts fn values — see 002 §:fx-overrides.

Conventions

  • Status — exactly one base value, optionally combined with one or more parenthesised qualifiers. The closed set below is the same vocabulary the generated api-manifest.edn curates (its :status field) — the two MUST agree:
    • Base values:
      • v1 (ships in v1).
      • v1 (preserved) (exists in current re-frame; preserved unchanged).
      • v1 (preserved + extended) (exists today; v1 adds new arity or behaviour).
      • EP-NNNN (a surface introduced or reshaped by a named pre-alpha EP, shipping in v1 — e.g. reg-event (EP-0018), reg-interceptor (EP-0022); the canonical lineage is the named EP, cited in the row's Notes).
      • post-v1 lib (design spec in v1 Specs but ships in a post-v1 library).
      • post-v1 (planned, rf2-<id>) (specced normatively but not yet shipped; the impl is tracked by the named bead — per the Projection-maintenance rule below).
    • Qualifiers (parenthesised, combinable): dev-only (elided in production builds — the macro emit site or runtime body, depending on the API); changed, EP-NNNN (a preserved v1 surface a named EP changed — e.g. v1 (changed, EP-0017)); optional capability (ships only when the owning optional artefact is on the classpath, optionally narrowed optional capability, dev/test); internal lowering only (an EP surface retained as a framework-internal lowering seam, not a public authoring form — e.g. EP-0022 (internal lowering only)).
    • Examples: v1, v1 (preserved), v1 (dev-only), v1 (preserved, dev-only), v1 (changed, EP-0017), EP-0018, EP-0022 (internal lowering only), v1 (optional capability), post-v1 lib, post-v1 (planned, rf2-<id>).
    • The re-frame.alpha namespace is dissolved — no APIs in this reference live outside re-frame.core (with the documented per-namespace exceptions: re-frame.test-support and re-frame.test-helpers).
  • Macro/Fn: marked M (macro) or Fn.
  • Spec column — names exactly the canonical owning Spec (the per-Spec doc whose contract this API implements). Migration rules and other cross-references are NOT in the Spec column; they appear in the Notes column when relevant.
  • Configure keys — runtime configuration is uniformly via (rf/configure! {<key> <opts>, …}), a single nested map. Every <key> is enumerated in §Configure keys below; per-area tables call out which keys their APIs read but do not redefine the key's vocabulary.
  • Per-artefact public namespaces. The core surfaces live in re-frame.core. Per-feature artefacts ship their own public namespace; consumers :require the namespace directly (with the documented exception of the epoch surface, which late-bind re-exports through re-frame.core). Front-porch boundary. re-frame.core is the small app-developer front porch — registration, dispatch, subscribe, the frame basics, interceptors, lifecycle, configure, and the core trace / egress / registrar-query / app-realm surfaces. The optional-feature registration MACROS stay on the façade (reg-route, reg-flow, reg-app-schema / reg-app-schemas, reg-machine / defmachine, reg-resource / reg-mutation / reg-resource-scope, reg-error-projector, reg-head, reg-http-interceptor) because they capture call-site source-coords and have no owned-namespace macro form — registration stays central. But the optional features' non-registration query / introspection / lifecycle helpers are NOT re-exported from re-frame.core; reach them through their owning namespace: re-frame.schemas (app-schemas / app-schema-meta / app-schemas-digest / set-schema-fns! / schema-fns / default-schema-fns), re-frame.machines (reg-machine* / make-machine-handler / machine-transition), re-frame.routing (match-url / route-url). The registrar INVERSE is the exception to that rule and stays on the façade as the one kind-keyed clear (§Clearing registrations): it dispatches across four optional artefacts and core, so no owning namespace could carry it, and the façade row is what turns an absent artefact into a documented :rf.error/<artefact>-artefact-missing rather than a hard require failure. The epoch re-exports remain on the façade as the one documented late-bind exception (rows below); the SSR QUERY surface is deliberately NOT among them (rf2-kuky.44 — loading re-frame.ssr is what installs the SSR runtime, so every SSR app already names the artefact namespace):

    Namespace Artefact Surfaces
    re-frame.core core the registration / dispatch / subscribe / interceptor / lifecycle / configure surfaces; late-binds re-exports for ONE artefact: the re-frame.epoch surface (epoch-history, restore-epoch!, replay-epoch!, replace-frame-state!) — the epoch listener stream is reached through the core-native (register-listener! :epoch …) verb, not a per-channel re-export. SSR contributes only its two REGISTRATION macros (reg-head / reg-error-projector, rowed in §Registration like every other artefact's registrar); its query surface is reached at home on re-frame.ssr / re-frame.ssr.head. The streaming-render-shell / streaming-render-continuation / streaming-build-final-payload triple is not re-exported — the streaming surface is host-adapter territory and the SSR-aware host (re-frame.ssr.ring / equivalents) requires [re-frame.ssr :as ssr] directly. Re-exports activate when the named artefact is on the classpath; absent artefacts surface :rf.error/<feature>-artefact-missing errors.
    re-frame.test-support core assert-path-equals, poll-until, fixture machinery (per §Testing). Runtime-state axis — registrar, frames, app-db, drain. assert-path-equals mirrors the :rf.assert/path-equals Story event. View-tree assertions live in the sibling re-frame.test-helpers.
    re-frame.test-helpers core View-assertion helpers — hiccup-walk (find-by-testid / find-by-attr family, text-content, extract-handler, invoke-handler), the testid authoring helper, and the expand-tree walker (per §Testing — View-assertion helpers). View-tree axis — hiccup data, testids, attached handlers. Runtime-state assertions live in the sibling re-frame.test-support.
    re-frame.ssr day8/re-frame2-ssr render-to-string, emit-ui-tree, render-tree-hash, streaming-render-*, project-error, head-model->html, hydrate! (per §SSR). reg-head, head-model and default-head are defined in the sibling re-frame.ssr.head, which consumers (:require [re-frame.ssr.head :as head]) directly; the reg-head REGISTRAR also rides the re-frame.core façade. re-frame.ssr re-exports head-model and head-model->html from that sibling, so the whole read side of the head contract sits on the same door as render-to-string.
    re-frame.ssr.ring day8/re-frame2-ssr-ring the Ring host-adapter (default-html-shell, streaming-prefix/suffix, trusted-shell hooks per Spec 011); ssr-handler's :renderer construction opt is the render-body seam — (fn [{:keys [frame-id request opts]}] → {:body-html :render-hash}), default the local :root-view render, refused by stream-handler (Spec 011 §HTTP response contract).
    re-frame.schemas day8/re-frame2-schemas app-schemas, app-schema-meta, app-schemas-digest, set-schema-fns!, schema-fns, default-schema-fns (per §Schemas).
    re-frame.machines day8/re-frame2-machines (post-v1 scaffolding) reg-machine / defmachine (registration), make-machine-handler (pure factory: Level-2 testing, the spawn path), machine-transition, the :rf.machine/spawn / :rf.machine/destroy fx (per §Machines).
    re-frame.epoch day8/re-frame2-epoch epoch-history, restore-epoch!, replay-epoch!, replace-frame-state!, (rf/configure! {:epoch-history ...}), and the epoch listener stream reached via (rf/register-listener! :epoch id f) / (rf/unregister-listener! :epoch id). Re-exported / late-bound through re-frame.core via late-bind hooks — (:require [re-frame.epoch]) at boot before consuming the surfaces through re-frame.core (per Tool-Pair §Time-travel — Artefact home).
    re-frame.adapter.uix day8/re-frame2-uix UIx-specific surfaces (per §UIx adapter).
  • Projection-maintenance rule. This doc is a non-canonical projection — the canonical contract lives in the per-Spec docs cited in each row's Spec column. The projection MUST stay in sync with shipped artefacts. Every row carries: owner (Spec column) — the canonical spec doc; artefact / namespace — where the public-var lives (table above); public-var statusv1 / v1 (preserved) / post-v1 lib / post-v1 (planned, rf2-<id>) for surfaces specced normatively but not yet shipped (the spec contract holds; the impl is tracked by the named bead); verification pointer — the conformance fixture, the per-artefact test, or the AI-Audit row that asserts the row holds. Rows that document a surface neither shipped nor on a tracking bead MUST be cut from this projection — the design's normative claim then lives only in the owner spec.


Tier taxonomy

Every public-surface row in this document is either supported — a documented API that downstream apps and tools may rely on, in the status the row's Status column gives — or, for the one implementation tier, public-for-technical-reasons only (exported but explicitly not a surface to depend on; see below). Supported is not the same as front-porch: a system that ships restore-epoch!, project-egress, adapter hooks, and replace-frame-state! has a large public surface, but only a small slice of it is what a new app developer should ever reach for. The Tier column on every table below records that slice, using a closed vocabulary of eight values:

Tier Meaning Who reaches for it
front-porch The tight set a new app developer needs to build a working app — register / dispatch / subscribe, the frame basics, the everyday reads. Loaded by default by the Guide and the skills. Every app author, day one.
advanced Power-user surfaces that solve real problems but are not first-reach — epoch-listener registration, trace projection, flows internals, the *-twin fn forms, the lower-level lifecycle (destroy-adapter!). Opt-in. Experienced authors, library authors, niche cases.
tooling Dev / inspection surfaces consumed by Story, Xray, the pair-MCP servers, and other Spec 009 / Tool-Pair tools — trace listeners, the registrar query API, off-box-egress projections, the epoch query surface, the Story run-result read accessors. Not for application logic. Tool and instrumentation authors.
adapter The substrate-adapter hooks — the per-substrate adapter Var, the hooks and set-*! seams a substrate adapter implements or installs. Adapter authors (Reagent / UIx / custom).
testing Test-only helpers — fixture machinery, assertion helpers, view-tree walkers, the HTTP-stub surfaces. Elided or simply absent from production paths. Test authors.
internal-public A supported host/tool embedding point: stable, exported, and safe to call, but not an application surface — provided for a specific tool/host integration, not for app logic. The Xray mount-<panel>! / mount-shell! family is the canonical (and now sole) instance — a host that builds its own Xray chrome may call them; an app never should. Narrower than it looks: it is not the bucket for every exported helper (those are implementation); it is the small set a host may legitimately embed against. Specific host/tool integrations only.
implementation Public-for-technical-reasons only — exported because a sibling namespace, the tool chrome, or a test must reach it across a namespace boundary (ClojureScript has no cheap cross-namespace-private seam), but it is NOT a supported surface: an app or tool MUST NOT depend on it, and it may change or vanish without notice. The per-feature artefact lifecycle/validation/cache helpers (re-frame.schemas, re-frame.ssr, re-frame.routing, re-frame.machines, re-frame.flows), the Ring host-adapter internals, and the Xray panel-leaf Panel reg-views live here. No facade export may sit at this tier — a facade export tiered implementation is annotation rather than removal, and the api-manifest drift-check refuses it (per Conventions §Removing or demoting a facade export); that is why the Story vocabulary formerly listed here is now tooling and match-schema-expectations left the facade instead (rf2-i6kh). Nobody downstream — internal plumbing that merely happens to be public.
deprecated On the way out; a replacement exists. Retained only long enough for callers to migrate. (Pre-alpha currently carries none — removed surfaces live in §Removed / not shipped, not here.) Nobody new — migrate off.

The Guide and the skills load FRONT-PORCH only by default. A new app developer reading the Guide, or an AI agent operating through the skills, sees the front-porch tier and nothing else unless they opt in. Advanced, tooling, adapter, testing, and internal-public surfaces are opt-in — reached by explicitly pulling in the relevant chapter, the relevant artefact, or the relevant tool. The implementation tier is never reached for: it is not opt-in, it is off-limits — exported only for technical reasons (see the table) and excluded from the supported surface entirely. The acceptance bar for this taxonomy: a new app developer can read the one-page front-porch list (the union of all front-porch rows below) and never trip over trace projection, epoch-listener registration, adapter hooks, or Xray internals.

Tier vs Status. Tier and Status are orthogonal axes. Status records shipping lineage (v1 / v1 (preserved) / post-v1 lib / …). Tier records who-reaches-for-it. A surface can be v1 and tooling (register-listener!), or post-v1 lib and front-porch (the Story run verb is post-v1 lib/tooling; a hypothetical post-v1 ergonomic core helper would be post-v1 lib/front-porch). Read the two columns together.

The front-porch / back-room split is the first instance, generalised. The multi-frame surface (§View ergonomics, per 002 §The multi-frame surface) was already organised as a front-porch / back-room split — dispatch / subscribe / with-frame on the porch, the {:frame …} override in the back room (and capture-frame, the ONE public HOLD primitive, alongside it). The Tier column generalises that split across the whole API: front-porch is the porch; advanced / tooling / adapter / testing / internal-public are the back rooms (all still supported); implementation is below the floorboards — exported but off the supported surface entirely.

Closed vocabulary, restrictive-by-default. The eight values above are the complete set — no ninth tier is added without an explicit governance decision. When a surface is genuinely ambiguous between two tiers, pick the more restrictive one (advanced over front-porch; internal-public over advanced; implementation over internal-public when the var is not actually a host/tool integration point but merely an exported helper) and note the call in the row's Notes. The downstream manifest consumes Tier as a first-class field, so the value must come from this closed set. The internal-public / implementation boundary: ask "is this a surface a host or tool legitimately embeds against?" — if yes, internal-public (the Xray mount family); if it is exported only so the framework's own namespaces / tests can reach it across a boundary, implementation. The split keeps the supported surface honest.

What the Tier column covers. Tier is a property of a public var (a fn / macro / Var). The tables that enumerate keyword-addressed registrations — standard events (:rf.route/navigate, :rf/hydrate, …), standard subs (:rf/route, :rf/response, …), standard fx (:rf.nav/push-url, :rf.http/managed, …), standard cofx, reserved fx-ids, the :fx-entry catalogue, the spec-internal schemas, the configure keys, and the error/trace-event catalogues — are not vars and carry no Tier column. They are part of the contract of whichever artefact owns them; their availability follows that artefact's tier (e.g. the :rf.route/* events ship with the advanced routing artefact). The front-porch one-page list is the union of front-porch-tiered var rows.

Tiering of cross-tool surfaces (Story, Xray, pair-MCP)

Story, Xray, and the MCP support namespaces ship their own public-var rows in their own specs (007-Stories.md, tools/xray/spec, Tool-Pair.md); this projection rows only the slices that surface through re-frame.core or the per-feature artefacts. The Tier taxonomy is nonetheless repo-wide and authoritative over those surfaces — the per-tool specs classify against this closed vocabulary rather than inventing local terms. The standing classifications (resolved here, so the per-tool specs reference rather than re-litigate):

  • Story facadeevery re-frame.story export is tooling, with no implementation carve-out. The public execution verbs run / is / explain and the registration macros (reg-story / reg-variant / reg-workspace / reg-tag / reg-decorator / reg-story-panel / reg-fragment / reg-check / reg-mode, each with its *-fn partner) are tooling — a Storybook-shaped dev surface, not application logic. So are the run-result read accessors (run-result, result-status, result-passed?, run-result-schema, valid-run-result?, explain-run-result), the statement-of-record for reading what a run produced (per story spec 017 §Run result); the variant lifecycle (run-variant / reset-variant / watch-variant / destroy-variant! / render-variant); the registry query, canonical vocabulary, assertion, recorder, fingerprint, run-artifact, determinism and golden-slice families; and the *-id Vars for the built-in decorators. This flat classification is the settled outcome of the retrospective facade sweep (rf2-i6kh) and it reverses the former carve-out that tiered the run-variant / watch-variant / reset-variant vocabulary implementation as "public-for-technical-reasons only". That carve-out was the outlier: 007-Stories.md — the owning spec — names run-variant / reset-variant among the API the Story-as-test duality leans on; tools/story/spec/API.md §Facade re-export discipline lists the run/reset/watch/destroy lifecycle, the assertion and recorder facades and variant-share-url as user-callable; and the Story tutorials teach (story/run-variant …) directly. Per Conventions §Story / Xray nuance, for a tooling product tooling can legitimately be the front porch — the discriminator is the user's workflow, not the tier label. (is-variant / run-plan / is-plan, named by the former carve-out, no longer exist as vars.) The one export the sweep found with no such workflow, match-schema-expectations, left the facade for its owning re-frame.story.result rather than being annotated internal. (Absorbs story F-8.)
  • Xray facade (day8.re-frame2-xray.core) — every one of its 16 exports is tooling, and all 16 are :action :keep. The facade is the canonical entry point most hosts ever touch: the install verb (init!), the mount/visibility verbs (open! / open-overlay! / close! / toggle! / popout! / status), the inspected-frame pair (target-frame / set-target-frame!), the Story→Xray focus door (focus! + valid-focus-panels), load-theme!, and the four highest-traffic config setters (configure! / set-auto-open! / set-editor! / set-egress-profile!). Per Conventions §Story / Xray nuance, for a tooling product tooling can legitimately be the front porch. The retrospective facade sweep (rf2-ar67) recorded no move and no rename, and the reason is that tools/xray/spec/API.md §Wider public surface had already drawn the porch/workshop line and named the two surfaces deliberately kept OFF the facade — the per-key config setters beyond the four above, and the keybinding/attach! / detach! lifecycle pair — so the audit found the line drawn rather than needing to draw it. Unlike the other Xray rows these are :cljs-only manifest rows carrying :facade? true per row (the namespace is not JVM-loadable), and the CLJS probe holds it fully-rowed in both directions — the only Xray namespace held to completeness. One name, load-theme!, was recorded :keep with its spelling left open for a ruling — it is the sole export here that mutates, yet carried no bang, while load-* sits on no row of the §Lifecycle-verb law closed roster. Ruled under rf2-7nk1 (2026-09-06) and now executed: the export is spelled load-theme! under §Naming bucket 3 alone, with no roster row added and no Conventions.md edit — the roster names which lifecycle verb a surface takes; §Naming decides whether it carries !; a verb missing from the roster is not a reason to drop the bang. That is the same reading register-trace-listenerregister-listener! was settled on below, likewise without a roster row. Pre-alpha: no alias, no stub, no deprecation window.
  • Xray mount-<panel>! family (mount-epoch-panel!, mount-app-db-diff!, mount-trace!, mount-machine-inspector!, mount-routing!, mount-segment-inspector!, the master mount-shell!, …) — internal-public, the canonical (and now sole) instance of that tier. They are exported and stable, but they are a host-embed surface, not an application or even general-tool surface: a host that builds its own Xray chrome may call a mount-<panel>!; an app never should. This resolves the prior "public vs internal-but-stable" question against the closed vocabulary — the answer is internal-public. (Resolves xray M2.)
  • Xray panel-leaf Panel reg-views (day8.re-frame2-xray.panels.<area>/Panel, the Static-mode panel) — implementation. They are exported only so the shell can compose them across namespaces; they are NOT a host-facing single-panel embed surface — a host embeds the full shell via mount-shell! (per 008-Embedding-Contract), never a bare Panel. Demoted off internal-public (now reserved for the supported mount-embed surface) so the leaves do not read as a supported embed API.
  • Xray panel-helper functions (the per-panel render/projection helpers beneath the mount-fns) — tooling where they are a documented panel-author surface, otherwise unrowed-internal. (Absorbs xray H6.)
  • pair-MCP support namespaces — the trace/egress surfaces they consume (sensitive?, the registrar query API, the epoch query surface) are tooling, tiered at their re-frame.core / artefact home rows below.

Not-rowed internal carve-outs

The only internal (unrowed, MUST-NOT-depend-on) carve-outs in re-frame.core are two JVM-only macro-helpers re-exposed purely so pre-split tests can reach them:

  • re-frame.core/expand-reg-view^:no-doc; the canonical home is re-frame.core-reg-view-macro/expand-reg-view.
  • re-frame.core/parse-reg-view-args^:no-doc; the canonical home is re-frame.core-reg-view-macro/parse-reg-view-args.

Neither is rowed in this projection. Applications and tools MUST NOT depend on these re-exports; reach the canonical homes directly. These are not a tier — they are below the public surface entirely.


Registration

Return value. Every reg-* row below returns its primary id — the keyword (or path, for reg-app-schema) the caller registered with (its first positional argument). Per Conventions §reg-* return-value convention.

API M/Fn Signature Status Tier Spec Notes
reg-event M (reg-event id ?metadata handler) EP-0018 front-porch 002 The ONE public event form — a two-arg (fn [coeffects event-vec] effect-map) handler, coeffects in, a closed seven-key effects map out — #{:db :rf.db/runtime :fx} plus the four commit-plane classification effects #{:sensitive :large :clear-sensitive :clear-large} (see §Effect-map shape) — or nil no-op. The db write is an explicit {:db …} effect; there is no db-only return shape. Coeffects declared uniformly via :rf.cofx/requires. Full-context work is expressed with a registered interceptor (reg-interceptor, referenced by id). Metadata-map superset middle slot carries the reserved :interceptors key (a vector of interceptor refs). There is no reg-event-db/reg-event-fx, and reg-event-ctx is a framework-internal primitive — calling any of the retired names is a hard error (see §The retired event-registration names + the Removed §).
reg-sub M (reg-sub id ?metadata computation-fn) v1 name; grammar changed to :inputs front-porch 002 The only sub-registration form in v2. Dependencies are DECLARED under :inputs in the metadata map — a literal vector of query vectors (:static) or a producer fn of the query vector (:parametric) — and declared inputs always reach the body as a VECTOR. See §reg-sub input-production modes. Omitting :inputs is the layer-1 app-db reader.
reg-fx M (reg-fx id ?metadata handler) v1 (preserved + extended) front-porch 002 The handler is binary, context-first: (fn [ctx args]) per 002 §The binary fx-handler signature. ctx is a small map carrying :frame (the active frame id), :event (the originating event vector), and a runtime-internal :envelope; args is the value the event handler placed beside the fx-id in its :fx vector. A unary (fn [args]) handler survives on CLJS only via JS argument-dropping — it is not the blessed contract. :platforms metadata (a set of :server / :client) gates execution by active platform (default universal).
reg-cofx M (reg-cofx id ?metadata supplier) v1 (changed, EP-0017) front-porch 001, 002 Register a coeffect id with a value-returning supplier ((fn [] v) / (fn [arg] v)) and a registration grade — ambient (default) or recordable (:recordable? true, optionally :provided? true). A handler takes delivery by declaring :rf.cofx/requires (the value arrives FLAT under the id; 001 §:rf.cofx/requires). :rf/time-ms is the framework's one provided recordable registration. The ctx→ctx handler shape and inject-cofx are retired (no alias). See §Coeffects.
reg-interceptor M (reg-interceptor id ?metadata descriptor) EP-0022 front-porch 001, 002 The public application-authoring form for an interceptor — a first-class registered program member (registrar kind :interceptor). descriptor is one of {:before f} / {:after f} / {:before f :after f} (static) or {:factory f} (a parameterized family; the factory takes ONE arg and is the mechanism the standard [:rf.interceptor/path …] rides). Event/frame :interceptors chains reference registered interceptors by id (bare keyword) or [id arg], never inline values. Captures source coords; surfaces via handler-meta :interceptor. A migration value carrying an :id is accepted at this boundary only (the id must match). Replaces ->interceptor as the public authoring surface. See 001 §Interceptors + 002 §Registered interceptors and the chain grammar.
make-frame Fn (make-frame opts) / (make-frame opts descriptors) → frame value EP-0024 advanced 002 The ONE frame constructor: builds a live frame and returns the frame value (the lifecycle token — the routing ops dispatch / subscribe / frame-provider accept the value directly OR its id, normalizing a value to its id; destroy-frame! accepts either too but is the one lifecycle exception — the value is an exact-incarnation token that tears down only the incarnation it names (a stale value no-ops against a same-id successor), while the id is address-directed, per §Destroy. API-shrink #1, rf2-csbbwu removed the frame-value->id accessor; there is no need to unwrap the value). opts is a map (required — a non-map opts, including nil, fails loud with :rf.error/make-frame-bad-opts; the all-defaults frame is (make-frame {})). Accepts BOTH image-selection opts AND record-config opts in one call: :images (a non-empty vector; present ⇒ the selected generation; [] is an error:rf.error/make-frame-bad-images; absent ⇒ the default image generation over the whole source store + framework standards, failing loud on a cross-namespace same-[kind id] collision — :rf.error/image-duplicate-id; app isolation is named via :select-ns, not absence) / :id (registers in the one frame registry; a duplicate id is idempotent replacement — config + generation refresh, durable state preserved) / :adapter, plus any record-config key (:initial-events — seed app-db via a leading [:rf/set-db {…}] step — :fx-overrides, :platform, :ssr, :doc, :preset, :tags, …) honoured in the same call. (There is no :capabilities image-selection key — image-declared host capabilities are removed; :capabilities flows through as ordinary record-config.) Re-calling make-frame against the SAME :id with a NEW :images vector IS image hot-reload — the generation swaps while durable state is preserved (rf2-lxwpob folded the dedicated reload-images! verb into this; read the diff via generation-diff). make-frame is the ONE programmatic constructor (tools / tests / SSR / dynamic / image-loaded frames — rf2-h1vqa4 deleted the reg-frame spelling, no alias); the day-1 mount recipe is frame-root (ENSURE). (re-frame.frame/make-anon-frame-record! is the internal no-:id record helper.)
image M (image spec) → normalized, INERT image value — spec carries :id (optional), :select-ns ({:include [globs] :exclude [globs]} provenance selection), :registrations (inline registrar-keyed sections). Pure data — no registrar, no side effect; supplied to make-frame via :images (later image wins; shadows reported on :rf.gen/shadows) EP-0023 advanced 002 The one public image constructor.
generation-diff Fn (generation-diff before after){:added #{[kind id] …} :changed #{…} :removed #{…} :retained #{…}} — a PURE diff between two sealed image generations (read via frame-generation before/after a re-make-frame reload). EP-0023 tooling 002 Replaces the removed reload-images! verb's report — a read over two generation values, not a bespoke verb (rf2-lxwpob).
reg-view M (reg-view sym [args] body+) / (reg-view sym docstring [args] body+) / (reg-view ^{:rf/id :explicit/id} sym [args] body+) v1 front-porch 001, 002 Defn-shape; auto-defs the symbol; auto-derives id from (keyword *ns* sym); auto-injects dispatch / subscribe as lexical bindings; rejects non-defn-shape bodies at macroexpand.
reg-view* Fn (reg-view* id render-fn) / (reg-view* id metadata render-fn) v1 advanced 001, 002 Plain-fn surface beneath reg-view. No auto-def, no auto-inject, no compile check. Use for computed ids, library-generated views, Reagent Form-3 (create-class), or registration without a Var. The * follows Clojure's let/let*, fn/fn* idiom (per Conventions).
reg-machine M (reg-machine machine-id machine-spec) / (reg-machine machine-id opts machine-spec) v1 advanced 005 Optional re-frame.machines artefact. Walks the literal spec form at expansion time; co-locates per-element source on each :guards / :actions entry + a reference-site :source-coords on each :states-tree map node. Top-level call-site coords land on handler-meta. The optional opts metadata map is the canonical Spec 001 MIDDLE slot; it carries an event-vector :schema (the :where :event boundary on the dispatched outer vector) — the machine + event-vector-schema shape.
defmachine M (defmachine name spec) / (defmachine name docstring spec) v1 advanced 005 def-shape for the def-then-register pattern. Walks the literal spec at the definition site, stamping per-element source onto the def'd value so it travels into a later (reg-machine id name). Does not register.
reg-app-schema M (reg-app-schema path schema) / (reg-app-schema path metadata schema) — the schema is the positional value slot; the optional middle metadata map carries the :frame target (e.g. (reg-app-schema [:user] {:frame :session} UserSchema)) (rf2-qm7k83 Part A) v1 advanced 010 Optional re-frame.schemas artefact. Path is the registration id. App-db schemas are path-keyed and live in the schemas artefact's per-frame side-table (app-db schemas are NOT a registrar kind). Every other reg-* is keyword-id-keyed; here the first arg is the path vector (e.g. [:user]) and (app-schemas {:frame f}) / (app-schema-meta {:frame f :path [:user]}) look up by the same vector. The path-as-id asymmetry is principled (paths are first-class in get-in / assoc-in grain — schemas-at-paths matches the dataflow grain), not accidental; otherwise reg-app-schema is an ordinary family member — the schema is the positional value slot uniform with the rest of the reg-* family. A non-map middle metadata arg (the 3-slot form) is a loud :rf.error/app-schema-bad-metadata. Per Conventions §reg-* return-value rule.
reg-app-schemas M (reg-app-schemas {path-1 schema-1, path-2 schema-2, ...}) / (reg-app-schemas {…} opts) — bulk plural form for feature-modular apps that register 5–20 paths against the same prefix (per Conventions §Feature-modularity prefix convention). The bulk {path -> schema} shape is retained (the map value IS the schema — no positional ambiguity in a bulk map); the plural form is KEPT for its all-or-nothing atomic-batch contract (every path validated before any mutation — a naive doseq does not preserve it). Each entry routes through the singular reg-app-schema positionally and is stamped with this call's source-coords. Returns the vector of paths registered v1 advanced 010
reg-flow M (reg-flow flow-id metadata derive-fn) v1 advanced 013 Optional flows artefact. Per the canonical Spec 001 3-slot grammar (rf2-bqstzr): the pure :derive fn is the third VALUE slot; metadata carries :inputs / :output-path (both REQUIRED) plus optional :doc / :schema / the EP-0025 classification keys and the :frame mounting key. A :derive left inside the metadata map is rejected loudly (:rf.error/invalid-flow-metadata). Returns flow-id (per Conventions §reg-* return-value convention). Flows are frame-scoped and single-store: the :flow registrar kind is RESERVED-but-empty; introspection is via the frame-scoped re-frame.flows/flows / flow-meta and the whole-registry flows-snapshot (not handler-meta :flow). Per 013-Flows.md §The registration shape / §Frame-scoping.
reg-route M (reg-route id metadata path) — canonical 3-slot grammar: the URL :path pattern is the third VALUE slot, metadata the pure reflection map v1 advanced 012 Optional routing artefact. A :path left inside the metadata map is a loud :rf.error/route-bad-metadata.
reg-head M (reg-head id ?metadata head-fn) v1 advanced 011 Optional SSR artefact. New registry kind :head; routes name a registered head via :head route metadata. Captures source-coords; under the optional-artefact wrapper convention the surface routes through the :ssr/reg-head late-bind hook.
reg-error-projector M (reg-error-projector id ?metadata projector-fn) v1 advanced 011 Optional SSR artefact. New registry kind :error-projector; named per-frame via the frame's :ssr {:public-error-id ...} config (per make-frame / frame-root).

reg-sub input-production modes

reg-sub supports three input-production modes. Every subscription has an input query-vector producer, and it is declared ONCE — under :inputs in the metadata map, the same slot reg-flow uses. Layer-1 has no producer; a literal :inputs vector is the static producer; an :inputs fn is the query-parametric producer.

Mode Form Meaning
App-db reader (reg-sub id computation-fn) :inputs OMITTED. No upstream subscriptions. The computation fn receives app-db and the outer query-v.
Static inputs (reg-sub id {:inputs [q1 q2]} computation-fn) Inputs are literal query vectors known — and shape-checked — at registration.
Parametric inputs (reg-sub id {:inputs producer-fn} computation-fn) Inputs are computed from the outer query-v when a concrete cache entry is materialized.

Declared inputs always arrive as a vector. At zero, one or many, the computation fn receives [v0 v1 …] in declaration order. Moving a dependency between the literal and the producer form never changes the body, and adding a second input never turns a scalar argument into a vector. An explicit {:inputs []} declares no dependencies and delivers []; OMITTING :inputs is the layer-1 reader, which receives app-db itself. The two are distinct by design.

(rf/reg-sub :cart/by-price {:inputs [[:cart/items]]}
  (fn [[items] _] (sort-by :price items)))

(rf/reg-sub :cart/visible {:inputs [[:cart/by-price] [:cart/filter]]}
  (fn [[items f] _] (filter f items)))

An :inputs producer fn is a pure function from the outer query-v to a vector of input query vectors. It is not a v1 signal function: it must not call subscribe, deref app-db, dispatch, mutate, or perform IO; it receives only the outer query-v; and it must not return live reactions. It is never executed at registration — only at materialization.

(rf/reg-sub
  :article/page
  {:inputs (fn [[_ article-id]]
             [[:article/by-id article-id]
              [:comments/for-article article-id]
              [:viewer/current]])}
  (fn computation-fn [[article comments viewer] [_ article-id]]
    {:id article-id :article article :comments comments
     :can-edit? (:edit? viewer)}))

Input grammar. A literal :inputs MUST be a vector, and every element MUST be a query vector (a vector whose first element is a keyword); a producer fn MUST return that same shape:

inputs := [query-vector*]      ;; query-vector := vector with a keyword head
;; Accepted
[[:article/by-id id] [:viewer/current]]   ;; multiple inputs
[[:item/by-id id]]                        ;; single input — still a vector OF query vectors
[]                                        ;; no inputs (unusual but valid)

;; Rejected
:viewer/current                           ;; bare keyword
[:article/by-id id]                       ;; scalar query vector (ambiguous: arg vs two inputs)
[[:article/by-id id] :viewer]             ;; mixed vector + bare keyword
{:article [:article/by-id id]}            ;; map return

The scalar query-vector rejection is deliberate: [:x :y] is ambiguous at this boundary (one query with argument :y, vs two inputs). The only accepted single-query spelling is [[:x :y]]. No bare keyword shorthand, no map return, no reaction/derefable. A literal :inputs is checked at registration (:rf.error/reg-sub-bad-args) against the same grammar a producer's return is checked against at materialization (:rf.error/sub-input-fn-bad-return) — one grammar, two moments. An explicit {:inputs nil} is refused: nil is not "absent". The literal check is SHAPE-only and never a registry lookup, so {:inputs [[:a]]} may be registered before :a exists. Reach for a producer fn only when the upstream query vectors need values from the outer query-v; a literal vector is exactly a constant producer, and it is the form a tool can read as a static edge. Per 006 §Subscription input producers, 008 §compute-sub algorithm, and Conventions §reg-sub input grammar. Registration-shape and input-return errors signal loudly via :rf.error/reg-sub-bad-args, :rf.error/sub-input-fn-exception, and :rf.error/sub-input-fn-bad-return (catalogued in 009 §Error event catalogue).

:inputs is a vector here, and whether the family should instead take a named map is an open question shared with reg-flow — see 013 §Map-keyed :inputs instead of vector. It will be ruled ONCE, for reg-flow and reg-sub together; until then both ship the vector.

Clearing registrations

One verb, kind-keyed. The registrar is ONE map, (kind, id) → metadata (001 §Registry model), and clear is its inverse in the same grammar the read side already speaks. It REPLACED nine per-kind names — clear-event, clear-sub, clear-fx, clear-flow, clear-route, clear-http-interceptor, clear-resource, clear-mutation, clear-resource-scope — which differed on where the name lived (seven on the façade, two off it), whether a nilary clear-all existed, how the frame was named, and what came back. No shim, no alias, no deprecation (rf2-kuky.80).

(rf/clear :sub  :cart/total)                    ;; => :cart/total
(rf/clear :cofx :now)                           ;; six kinds that had no public inverse
(rf/clear :flow :cart/total)                    ;; ambient frame
(rf/clear :flow :cart/total {:frame :session})  ;; explicit frame

Each kind routes to its OWNING lifecycle fn — :flow vacates its output path and settles dependents, :route emits :rf.route/cleared, the resources kinds dispose per-frame runtime state — rather than short-cutting to the registrar. Kinds with no owning lifecycle fn (:event :sub :fx :cofx :interceptor :view :head :error-projector) go straight to re-frame.registrar/unregister!, which IS their owner. Returns the id for every kind. The per-kind clear column is in 001 §Registry model.

Opts are EXACT and frame-scoped-only. {:frame f} is accepted for :flow and :http-interceptor and nothing else, and it must be exactly that — sole key :frame, value a frame-id keyword or a live frame value. A near-miss like {:fram :session} THROWS :rf.error/registrar-clear-bad-request before any frame is resolved, where a tolerant destructure would have cleared the AMBIENT frame's registration silently (rf2-s32bf, Principles §No silent swallow). An unknown kind fails closed the same way, naming the closed set. Omitting opts reaches the owning fn's own ambient arity.

There is NO clear-all arity. Bulk clearing is a fixture concern: re-frame.registrar/clear-kind! and re-frame.test-support own it, and they were the only callers the nilary arities ever had. The cache / buffer clear-*! names (clear-sub-cache!, clear-trace-buffer! …) are a DIFFERENT axis — they clear runtime state rather than registrations — and keep both their names and their bangs.

API M/Fn Signature Status Tier
clear Fn (clear kind id) / (clear kind id {:frame f})id. The ONE registrar inverse. kind is the registrar kind set MINUS :frame (a live runtime object, torn down by destroy-frame!) PLUS :http-interceptor (a per-frame side table). Opts accepted for :flow / :http-interceptor only, and EXACT. See the note above. v1 (preserved) advanced
destroy-frame! Fn (destroy-frame! frame-id) — the normative teardown boundary. A frame value target carries exact-incarnation authority (tears down only the incarnation it names — a stale value no-ops against a same-id successor), while a frame-id keyword is address-directed (tears down whatever incarnation is currently live), per 002 §Destroy (rf2-moftbs). Per-feature artefacts (flows, machines, schemas, SSR, epoch) hang their frame-scoped cleanup off this call; flows release per 013 §Frame-destroy teardown. v1 front-porch
clear-sub-cache! Fn (clear-sub-cache! frame-id?) v1 (preserved) advanced

Dispatch and subscribe

API M/Fn Signature Status Tier Spec
dispatch M/Fn (CLJS) (dispatch event) / (dispatch event opts) v1 (preserved + extended); macro captures call-site for :rf.trace/call-site; on CLJS the same name is ALSO a plain-fn value-alias (Convention A, rf2-m90brg) for HoF / programmatic dispatch with no call-site capture front-porch 002
dispatch-sync M/Fn (CLJS) (dispatch-sync event) / (dispatch-sync event opts) v1 (preserved + extended); macro captures call-site for :rf.trace/call-site; on CLJS the same name is ALSO a plain-fn value-alias (Convention A, rf2-m90brg) for HoF / programmatic sync dispatch with no call-site capture front-porch 002
subscribe M/Fn (CLJS) (subscribe query-v) / (subscribe query-v opts) v1 (preserved + extended); opts may carry :frame (mirrors dispatch); macro captures call-site for :rf.trace/call-site; on CLJS the same name is ALSO a plain-fn value-alias (Convention A, rf2-m90brg) for HoF / programmatic subscribe with no call-site capture front-porch 002
subscribe-once Fn (subscribe-once query-v) / (subscribe-once query-v opts) → value (subscribe + deref + immediate unsubscribe; one-shot, non-reactive read for handler bodies, REPL — never inside a machine callback: an in-callback ambient read is unrecorded and breaks 005's replay contract; a machine reads facts via payload or a declared recordable cofx) v1 (preserved + extended); opts may carry :frame (mirrors subscribe, rf2-bfadc6) — closes the misbinding footgun for an author carrying the opts form over from subscribe advanced 006
unsubscribe Fn (unsubscribe query-v) / (unsubscribe frame-id query-v) → nil (decrement the cache ref-count; on the 1 → 0 transition the cache slot is disposed synchronously in-tick — no grace-period timer, per 006 §Reference counting and disposal). Carved out from the Conventions §Tear-down verb axis — the registrar decrement is (clear :sub id), so un- is reserved as the singular form for the sub-cache ref-count decrement. The two are different operations on different state. v1 advanced 006

To read a machine's snapshot, subscribe to the canonical [:rf/machine machine-id] vector (see §Standard registered subs (machines)).

opts map keys: :frame, :fx-overrides, :interceptor-overrides, :trace-id, :source. Envelope shape and semantics: see 002 §Routing: the dispatch envelope.

Canonical event / query-v shape (best practice). [<id>] (trivial), [<id> <single-scalar>] (single-arg), [<id> {<k> <v>}] (multi-arg → single map payload). Variadic [<id> a b c] is tolerated by the runtime for v1-migration and caller convenience; the linter nudges new code toward the map form. Full rationale and cross-refs: Conventions §Canonical event-vector shape.

dispatch-* family taxonomy

Per audit-of-audits state-machines #10, the dispatch-* family has two sub-shapes that look alike on first read but answer different questions. Both are dispatch operations — the family-prefix is honest — but they sit in different sub-families.

Stamping-pair sub-family (dispatch / dispatch-sync / subscribe). The pair-shape question is "do you want call-site stamping or not?" In call position the macro form captures :rf.trace/call-site from the surrounding source position so tooling can navigate from a trace event back to the originating expression. On CLJS the same name is ALSO a plain-fn value-alias (Convention A, rf2-m90brg — mirrors reg-event / reg-sub / etc.) for HoF composition ((map dispatch events)) where a macro can't sit inside the higher-order call — no call-site capture on that path. There is no *-suffixed twin (rf2-m90brg retired dispatch* / dispatch-sync* / subscribe* from the facade); a JVM programmatic caller reaches the owning ns fn directly (re-frame.router/dispatch! / -dispatch-sync!, re-frame.subs/subscribe). Both the macro path and the value-alias route through the same underlying dispatcher; only the trace stamping differs.


View ergonomics

The multi-frame surface is organised by intent, not mechanism (a front-porch / back-room split — per 002 §The multi-frame surface):

  • Single-frame (no frames in play): dispatch, dispatch-sync, subscribe.
  • Scope: with-frame, with-new-frame, frame-provider {:frame …} (SCOPE-only — scope an existing frame into a React subtree; fails loud if absent). (ENSURE is its sibling component frame-root {:id …} — see below.)
  • Hold (carry a frame's ops as a value, across async): capture-frame — the ONE public carry primitive (API-shrink #1, rf2-csbbwu removed frame-bound-fn / frame-bound-fn* from the facade entirely — capture-frame or an explicit {:frame …} opt expresses the real use cases).
  • Override: the {:frame …} opt — first-class explicit routing for tools / tests / SSR / fx handlers.
  • Reads / lifecycle: app-db-value, frame-state-value, current-frame-id, destroy-frame!, make-frame, frame-ids, frame-meta (see §Public registrar query API).
API M/Fn Signature Status Tier Spec
frame-provider Component (Reagent) SCOPE-only (rf2-nyea0r split — roots ensure; providers scope). [rf/frame-provider {:frame :todo} & children]: provides an ALREADY-CREATED frame id through React context; :frame accepts a frame-id keyword OR a live frame value (API-shrink #1, rf2-csbbwu); creates / refreshes / destroys nothing; FAILS LOUD when the frame is absent (:rf.error/frame-provider-frame-absent; a value/keyword that is neither raises :rf.error/bad-frame-provider-arg; a nil :frame:rf.error/no-frame-context). Given an :id (the ENSURE key), FAILS LOUD naming frame-root (:rf.error/frame-provider-given-id). For create-if-absent, use frame-root. v1 front-porch 002
frame-root Component (Reagent) ENSURE — a COMMIT-OWNED TWO-PASS boundary (rf2-nyea0r split). [rf/frame-root {:id :todo :images […] :initial-events [[:rf/set-db {}]]} & children]: creates the frame if absent (via make-frame) in a client useLayoutEffect (NOT during render — first render emits no descendant subtree; a Suspense-aborted render creates + seeds nothing, no ghost frame), REUSES it WITHOUT re-seeding if present (idempotent re-mount / keyed remount preserves durable state and does NOT replay :initial-events; StrictMode-safe), provides its id to descendants; NO destroy-on-unmount. :id required (a missing/non-keyword :id:rf.error/frame-root-missing-id); a mounted :id/opts change → :rf.error/frame-root-reconfigured; a stray :frame:rf.error/frame-root-given-frame. True ownership stays make-frame + destroy-frame! in a create-class. v1 front-porch 002
with-frame M (with-frame :keyword body) — pin to an existing frame-id. Vector arg is a compile-time error (use with-new-frame) v1 front-porch 002
with-new-frame M (with-new-frame [sym expr] body) — eval expr, bind sym, run body, destroy frame on exit. Keyword arg is a compile-time error (use with-frame) v1 front-porch 002
capture-frame Fn (capture-frame) or (capture-frame frame-id){:frame :dispatch :dispatch-sync :subscribe} — the keystone OPERATION BUNDLE. Captures the frame at CREATION; its ops always target the captured frame and survive async. Read app-db via (app-db-value (:frame h)), not the handle v1 front-porch 002
view Fn (view view-id) → the installed substrate's own mountable head for that registration, or nil when nothing is registered. Not hiccup, and (on CLJS) not the raw render-fn: the head is re-derived against the adapter installed now and memoized, so repeat lookups are reference-stable and React reconciles them as one component type (rf2-oz7wr). Reagent: the :contextType-carrying head, mounted as [(rf/view :id) args…]; UIx: a substrate-marked component type, mounted as ($ (rf/view ::row) props); Fresco: the minted boundary itself, mounted the way h/defview documents (inside a body as [head props], from outside via h/as-element) — h/defview's registration is debug-gated, so nil in a release build is the documented answer for a Fresco view. On the JVM the answer is the stored :handler-fn. The lookup form for late-binding a registered view by id. v1 advanced 001

with-frame (pin) and with-new-frame (eval-bind-run-destroy) are documented in 002 §with-frame and with-new-frame. The macros are non-overlapping: each rejects the other's argument shape at compile time, with :recovery pointing the caller at the right sibling.

capture-frame is the keystone affordance — it replaces the removed dispatcher / subscriber nouns and is the single answer to "carry a frame's dispatch/subscribe ops across an async boundary." The handle is locked: a per-call :frame opt MUST NOT override the frame captured at handle creation — the captured frame always wins (per 002 §capture-frame). frame-bound-fn / frame-bound-fn* are REMOVED from the facade (API-shrink #1, rf2-csbbwu) — capture-frame (or an explicit {:frame …} opt) expresses the real use cases and is the ONE public carry primitive; the frame-rebinding closure semantics survive internally as re-frame.frame/bind-fn for the framework's own reach.


Reagent adapter (Spec 006)

Reagent-specific surfaces live in re-frame.adapter.reagent (artefact day8/re-frame2-reagent, the browser default). Reagent is the default substrate; its frame-provider Component is rowed in §View ergonomics and the reg-view macro in §Registration, not here. Apps targeting Reagent :require [re-frame.adapter.reagent :as rf.adapter.reagent] and pass rf.adapter.reagent/adapter to (rf/init! …).

API M/Fn Signature Status Tier Spec
rf.adapter.reagent/adapter Var (map) the 11-key adapter spec map {:kind :make-state-container :read-container :replace-container! :subscribe-container :make-derived-value :render :render-to-string :register-context-provider :flush-render! :dispose-adapter!} (ratom-backed; per Spec 006 §Adapter contract) v1 adapter 006
rf.adapter.reagent/flush-views! Fn (flush-views!) / (flush-views! f) — flush pending Reagent renders synchronously (wraps React's act() for tests); the canonical cross-substrate test-flush hook v1 adapter 006, 008
rf.adapter.reagent/set-hiccup-emitter! Fn (set-hiccup-emitter! f) — install the render-tree → HTML fn for render-to-string (the SSR late-bind seam, published through the :reagent/set-hiccup-emitter! hook) v1 adapter 006, 011
rf.adapter.reagent/client-root Fn (client-root) → an inert handle — no DOM work at allocation, so it is defonce-safe at namespace load and Node-safe. Opaque: hold it, hand it to render! and unmount!, and nothing else (per Spec 006 §The client root) v1 adapter 006
rf.adapter.reagent/render! Fn (render! handle render-tree mount-point) / (render! handle render-tree mount-point opts) → nil — the first call creates the React Root at mount-point and renders into it, or with {:hydrate? true} hydrates the server markup once; every later call updates that SAME Root, so one call is both the boot path and the ^:dev/after-load hook. render-tree is hiccup v1 adapter 006, 011
rf.adapter.reagent/unmount! Fn (unmount! handle) → nil — release the Root and return the handle to inert. Idempotent, and a no-op after rf/destroy-adapter! has already drained it; a later render! mounts afresh v1 adapter 006

UIx adapter (Spec 006)

UIx-specific surfaces live in re-frame.adapter.uix (artefact day8/re-frame2-uix) — they are NOT re-exported from re-frame.core because core has no static dependency on the adapter (the dependency direction is adapter → core per Conventions §Adapter shipping convention). Apps targeting UIx :require [re-frame.adapter.uix :as rf.adapter.uix] and call the surfaces directly.

API M/Fn Signature Status Tier Spec
rf.adapter.uix/adapter Var (map) the 11-key adapter spec map {:kind :make-state-container :read-container :replace-container! :subscribe-container :make-derived-value :render :render-to-string :register-context-provider :flush-render! :dispose-adapter!} (6 required + 3 optional + :dispose-adapter! + the :kind discriminator, per Spec 006 §Adapter contract) v1 adapter 006
rf.adapter.uix/use-sub Fn (UIx hook) (use-sub query-v) → the current sub value, re-rendering the caller when it changes; resolves the frame from React context ONLY — the surrounding frame-provider / frame-root, never a with-frame dynamic scope (no boundary above → :rf.error/no-frame-context). (use-sub query-v {:frame target}) pins ONE read to an explicit frame — the same opts form subscribe publishes, target a frame-id keyword or a live frame value, :frame required in that arity. The ONE value-hook name across React function components: re-frame.fresco.native/use-sub is the same operation for an island under Fresco v1 adapter 006
rf.adapter.uix/use-frame Fn (UIx hook) (use-frame) → the frame api for the ambient provider frame — EXACTLY what (rf/capture-frame) returns ({:frame :dispatch :dispatch-sync :subscribe}), capture-frame in hook position; resolves from React context ONLY, exactly as the ambient use-sub does — the surrounding frame-provider / frame-root, never a with-frame dynamic scope (no boundary above → :rf.error/no-frame-context); reference-stable across re-renders for the same resolved frame incarnation — a same-id destroy-and-recreate retargets it, because the bundle is pinned to the incarnation capture-frame ran against and not to the address. No opts, no variants — explicit frames use (rf/capture-frame frame-id). v1 adapter 002, 006
rf.adapter.uix/frame-provider Fn (UIx component) SCOPE-only (rf2-nyea0r split — roots ensure; providers scope) — ($ rf.adapter.uix/frame-provider {:frame :session} child-1 child-2) provides an existing frame id; creates nothing; fails loud if absent (:rf.error/frame-provider-frame-absent); given an :id (the ENSURE key) fails loud naming frame-root (:rf.error/frame-provider-given-id); idiomatic $ trailing children v1 adapter 002, 006
rf.adapter.uix/frame-root Fn (UIx component) ENSURE — a commit-owned two-pass boundary (rf2-nyea0r split) — ($ rf.adapter.uix/frame-root {:id :session :images […]} child-1 child-2) creates the frame if absent in a client useLayoutEffect (not during render — a discarded render creates nothing), reuses it without re-seeding if present, provides its id to descendants; no destroy-on-unmount; takes make-frame opts; a missing/non-keyword :id:rf.error/frame-root-missing-id, a stray :frame:rf.error/frame-root-given-frame; idiomatic $ trailing children v1 adapter 002, 006
rf.adapter.uix/flush-views! Fn (flush-views!) / (flush-views! f) — wraps React's act() for tests v1 adapter 006, 008
rf.adapter.uix/set-hiccup-emitter! Fn (set-hiccup-emitter! f) — install render-tree → HTML fn (parity with the Reagent adapter's late-bind seam) v1 adapter 006, 011
rf.adapter.uix/client-root Fn (client-root) → an inert handle — no DOM work at allocation, so it is defonce-safe at namespace load and Node-safe. Opaque: hold it, hand it to render! and unmount!, and nothing else (per Spec 006 §The client root) v1 adapter 006
rf.adapter.uix/render! Fn (render! handle element mount-point) / (render! handle element mount-point opts) → nil — the first call creates the React Root at mount-point and renders into it, or with {:hydrate? true} hydrates the server markup once; every later call updates that SAME Root. element is a React element built with uix.core/$ — CLJS data raises :rf.error/hiccup-on-element-render-slot, on the first render and every later one alike. No com.pitch/uix.dom dependency: the Root is minted by the shared React spine v1 adapter 006, 011
rf.adapter.uix/unmount! Fn (unmount! handle) → nil — release the Root and return the handle to inert. Idempotent, and a no-op after rf/destroy-adapter! has already drained it; a later render! mounts afresh v1 adapter 006

Per Decision 1 the hook is named use-sub — ONE value-hook name for every React function component, the same spelling re-frame.fresco.native publishes for a Fresco island (rf2-kuky.57): the VERB subscribe returns a subscription, the NOUN use-sub returns its value. The adapter publishes no raw useContext frame reader and no source-coord wrapper Var; use-frame answers "which frame am I in" and reg-view* is how a view reaches the :adapter/wrap-view late-bind hook. Per Decision 3 there is no auto-injection — UIx components read via the hook and hold frame ops via use-frame (capture-frame in hook position: (let [{:keys [dispatch]} (use-frame)] …)); capture-frame is THE hold primitive, and reg-view injection / use-frame are its two ergonomic spellings. Per Decision 4 reg-view (the Reagent macro) does NOT cover UIx; UIx users register with rf/reg-view* if they need registry-keyed view addressing.

The shared React Context that backs frame-provider / frame-root lives in re-frame.adapter.context (CLJS-only file in core, factored out per Decision 2) — the Reagent and UIx adapters both consume the same createContext object so a mixed-substrate app's frame-provider chain composes across substrates.


Routing (Spec 012)

reg-route is rowed canonically in §Registration.

API M/Fn Signature Status Tier Spec
match-url Fn (match-url url){:route-id :params :query :fragment :validation-failed? ?:validation-error} or nil (:validation-error present only on a validation failure) v1 advanced 012
route-url Fn (route-url {:to route-id :params path-params :query query-params :fragment fragment}) → URL string (single address-map arity; strictly address-only — :url / :query-merge / policy / unknown keys reject loud) v1 advanced 012
route-link Fn (registered view at :route/link) [rf/route-link {:to :route-id :params {...} :query {...} :fragment "..." :prefetch :intent & html-attrs} & children] v1 advanced 012
history-url-strategy Var (map) The DEFAULT :url-strategy — HTML5 History, path-form. re-frame.routing/history-url-strategy, NOT on the re-frame.core façade (routing bundle isolation) v1 advanced 012
hash-url-strategy Var (map) #-prefixed :url-strategy for no-server-rewrite static hosting / secretary-era v1 migrations. re-frame.routing/hash-url-strategy v1 advanced 012
with-base-path Fn (with-base-path strategy base) — STRATEGY COMBINATOR (rf2-g8pbwg): wraps strategy (either shipped strategy, or a custom one) so a deployment sub-path (e.g. an app served from /realworld/) is stripped off every inbound URL and re-added to every outbound one, at all four egress/ingress consult points. A blank/nil base returns strategy unchanged. re-frame.routing/with-base-path v1 advanced 012

A :url-bound? true frame's browser URL-change listener is installed / removed by the FRAME LIFECYCLE, automatically — creation installs (strategy-aware: popstate for history, hashchange for hash, per :url-strategy), destroy removes (rf2-g8pbwg). There is no imperative install/remove pair to call; the retired install-url-listener! / remove-url-listener! / install-history-listener! / remove-history-listener! exports are GONE (pre-alpha, no back-compat shim).

reg-route's routing-owned metadata reserved keys: :doc, :params, :query, :query-defaults, :tags, :parent, :on-match, :can-leave, :can-enter, :scroll, :sensitive, :large. Two cross-feature bare keys are accepted beside them: :head, owned by SSR (011) and always accepted; and :resources, owned by the Resources artefact (016) and accepted only when that artefact publishes its :routing/extra-route-keys hook. The URL :path pattern is the third VALUE slot, not a metadata key. Canonical detail in 012-Routing.md §Reserved route-metadata keys; shape in Spec-Schemas §:rf/route-metadata.

route-link click rules: a plain primary-button click (no modifier keys, no defaultPrevented) calls .preventDefault and dispatches [:rf.route/url-requested {:url <synthesised>}] — one key, because a raw URL IS the address (§The request grammar) and the handler re-derives the route from it. Modifier-key clicks (cmd / ctrl / shift / alt) and auxiliary-button clicks (middle-click) defer to the browser so the native href opens in a new tab. A caller-supplied :on-click runs first; if it calls .preventDefault (or otherwise leaves defaultPrevented true) the framework's interception is skipped. Keys other than :to / :params / :query / :fragment / :prefetch / :on-click pass through to the underlying <a> element. Detailed semantics in 012-Routing.md §Linking from views.

Standard route-related events:

Event Notes Spec
:rf.route/navigate Navigate to a registered route. 012
:rf.route/handle-url-change URL-change handler for link / popstate / initial load / SSR; the cause rides :rf.route/cause (:link, :popstate, :initial, :ssr are the framework's own feeds); default scroll :top for :link, else :restore. 012
:rf.route/url-requested The user clicked a framework-owned link. 012
:rf.route/navigation-blocked A :can-leave guard rejected a navigation. 012
:rf.route/entry-denied A :can-enter guard rejected navigation into a route. TERMINAL — nothing commits and no pending value is created; dispatched exactly once per attempt, carrying {:destination :target :cause :requested-url :guard}. A framework no-op default handler ships, so denial is safe with no application handler. 012
:rf.route/continue User-dispatched event proceeding a blocked navigation. 012
:rf.route/cancel User-dispatched event abandoning a blocked navigation. 012
:rf.route/prefetch Warm-mode resource-only intent preload: [:rf.route/prefetch {address}] runs a named destination's effective resource plan ownerlessly WITHOUT navigating (no route state, guards, :on-match, or readiness change). 012
:rf.route/replan-resources Same-token replan of the ACTIVE route: [:rf.route/replan-resources {:cause <edn>}] reruns its effective resource plan against the current app-db WITHOUT navigating — same address, same nav-token, same owner; kept identities adopted, added ensured with the caller's :cause, dropped released; readiness re-projected. :cause is required. Not a reload; no guards, :on-match, URL or scroll work. 012

Standard route-related subs:

Sub Returns Spec
:rf/route The full :rf/route slice {:route-id :params :query :fragment :transition :error :nav-token} 012
:rf.route/id Current route id 012
:rf.route/params Current path params 012
:rf.route/query Current query params 012
:rf.route/transition :idle / :loading / :error 012
:rf.route/error Current error map (when :transition = :error) 012
:rf.route/fragment Current URL fragment (string or nil) 012
:rf.route/chain Vector of route ids from parent-most to current (per :parent links) 012
:rf/pending-navigation The pending-nav slot (per :rf/pending-navigation schema) when a navigation is blocked; nil otherwise 012

The route and the pending-nav slot are read as ordinary subscription vectors — @(rf/subscribe [:rf/route]) and @(rf/subscribe [:rf/pending-navigation]); the :rf.route/* granular subs above chain off [:rf/route]. There is no named-read-sugar fn: a runtime-db framework read is a subscription vector, one grammar (per Conventions §Reserved sub-ids).

Standard route-related fx (canonical detail in 012-Routing.md):

Fx Args Platforms
:rf.nav/push-url URL string :client
:rf.nav/replace-url URL string :client
:rf.nav/scroll scroll-spec map :client
:rf.nav/capture-scroll {:url <leaving-route-url>} :client
:rf.route/with-nav-token {:rf/reply-to <reply-target> :value <v> :nav-token <token> :route-id <route-id>} (per 012 §Threading the nav-token) universal

Standard route-related cofx (canonical detail in 012-Routing.md):

Cofx Delivers Spec
:rf.route/nav-token The active navigation epoch token (read from [:rf.runtime/routing :current :nav-token]), delivered flat under the coeffect key :rf.route/nav-token — declare via {:rf.cofx/requires [:rf.route/nav-token]} in an :on-match-reached handler to capture the epoch live at scheduling time for stale-result suppression. Per 012 §Navigation tokens. 012
:rf.route/route-id The current route id (read from [:rf.runtime/routing :current :route-id]), delivered flat under the coeffect key :rf.route/route-id. The capture-side companion to :rf.route/nav-token: a route loader declares both ({:rf.cofx/requires [:rf.route/nav-token :rf.route/route-id]}) so it captures the two facts the route-loader work id [:rf.work/route route-id nav-token loader-id] needs at scheduling time, rather than reading the route id from the live slice at stale-arrival — where a cross-route completion would read the superseding route's. Per 012 §Navigation tokens. 012

SSR (Spec 011)

Namespace: the surfaces below live in re-frame.ssr (artefact day8/re-frame2-ssr); consumers (:require [re-frame.ssr :as ssr]). The head surface is defined in the sibling re-frame.ssr.head, which consumers may (:require [re-frame.ssr.head :as head]) directly — re-frame.ssr re-exports head-model and head-model->html from it, so the whole read side of the head contract sits beside render-to-string. The reg-head and reg-error-projector REGISTRARS are rowed in §Registration and ride the re-frame.core façade like every other artefact's registration macro. The Ring host-adapter lives in re-frame.ssr.ring (artefact day8/re-frame2-ssr-ring). The SSR query surface is NOT re-exported through re-frame.core (rf2-kuky.44): loading re-frame.ssr is what installs the SSR runtime — the :rf/hydrate event, the :rf.ssr/check-* fx, the seven :rf.server/* fx and the :rf.server/request cofx — so no SSR app can be one that has not named the artefact namespace, and the guided :rf.error/ssr-artefact-missing the façade copies existed to give was unreachable. The streaming surface (streaming-render-*) and the Ring host-adapter (re-frame.ssr.ring) are likewise reached at home; an SSR-aware host requires the namespace directly. Apps targeting SSR add the artefacts to their deps regardless. Epoch is now the ONE late-bind façade exception.

The ssr-node crossing (rf2-8arzr) adds three host-adapter namespaces, all requires-directly and none re-exported. re-frame.ssr.ring.node (in day8/re-frame2-ssr-ring) provides renderer, the one non-local :renderer the reference ships — the JVM→Node adapter over the bounded sidecar at implementation/ssr-node. re-frame.ssr.render-state (in day8/re-frame2-ssr) is the render-visible projection the seam runs — project / serialize / deserialize / restore! over the two-partition envelope. re-frame.fresco.server (in day8/re-frame2-fresco, CLJS) provides render-body, the body-only entry a server bundle calls. Their contracts are 011 §Client-side hydration boot helper; the recipe is Render on Node. These are :implementation-tier host-adapter plumbing on the same footing as re-frame.ssr.ring's own vars and are not rowed below; all three are now carried by spec/api-manifest.edn at that tier — the two JVM-loadable ones by generator introspection, and re-frame.fresco.server by curated :cljs-only rows, since it requires react-dom/server and cannot be loaded on the JVM (rf2-8arzr.7, rf2-3ne8).

reg-head and reg-error-projector are rowed canonically in §Registration. The head-fn signature is (fn [db route] head-model); the projector-fn signature is (fn [trace-event] :rf/public-error).

API M/Fn Signature Status Tier Spec
render-to-string Fn (render-to-string view-or-hiccup opts) → HTML string v1 advanced 011
emit-ui-tree Fn (emit-ui-tree tree) / (emit-ui-tree tree {:doctype? bool}) → HTML string. Folds an already-rendered version-1 structural tree to HTML — pure, deterministic to the byte, JVM-runnable; it calls no view and resolves no subscription. Validates the root :rf.ui/tree-version first, before any emission: a missing / non-integer / unsupported version throws :rf.error/ssr-ui-tree-version-unsupported ({:got … :supported #{1}}); a malformed node past the gate throws the shared :rf.error/ui-tree-malformed. :doctype? is the only current option — it prefixes <!DOCTYPE html>. Per 004B §The SSR consumption boundary. v1 advanced 004B
render-tree-hash Fn (render-tree-hash render-tree) → 32-bit FNV-1a structural hash (lowercase hex). Identical output on JVM and CLJS for the same canonical-EDN representation. Per 011 §Hydration-mismatch detection. v1 advanced 011
project-error Fn (project-error frame-id trace-event):rf/public-error. Applies the active error-projector (selected by the frame's :ssr {:public-error-id ...} metadata) for the named frame. Per 011 §Server error projection. v1 advanced 011
head-model Fn (head-model frame-id) / (head-model frame-id {:head-id id :route route}):rf/head-model. The ONE head read (rf2-kuky.89, replacing render-head / active-head). Selection: an explicit :head-id, else the effective route's :head, else default-head; a selected-but-unregistered id raises :rf.error/no-such-head. The effective route is :route when the key is present (an explicit nil means no route), else the frame's [:rf.runtime/routing :current] slice — and the head fn is evaluated against that SAME route. Re-exported on re-frame.ssr from re-frame.ssr.head. Head rendering is a frame-scoped read, so the frame is carried, not ambient: the no-arg form was removed (EP-0002) and a nil frame-id raises :rf.error/no-frame-context rather than resolving against a synthesised default frame. v1 advanced 011
head-model->html Fn (head-model->html head-model) / (head-model->html head-model {:wrap? bool}) → inner-head HTML string. Re-exported on re-frame.ssr from re-frame.ssr.head v1 advanced 011
hydrate! Fn (hydrate! {:frame f :payload p :render-tree-fn f :root-id id}) → the applied payload, or nil on a client-only first load. The supported client boot: READ the payload (from :payload, else the DOM's __rf_payload) → dispatch-sync [:rf/hydrate …] against :frame → VERIFY via verify-hydration! when :render-tree-fn is supplied. Install is idempotent across a page's roots; a conflicting payload for the same id raises :rf.error/frame-payload-conflict. Per 011 §Client-side hydration boot helper v1 advanced 011
streaming-render-shell Fn (streaming-render-shell root-hiccup){:shell-html "…" :continuations [{:id <id> :subtree <hiccup>} …]}. Walks the tree once; at each :rf/suspense-boundary emits a <template …suspense-fallback> placeholder + records a continuation. Per 011 §Streaming SSR — (a). v1 advanced 011
streaming-render-continuation Fn (streaming-render-continuation frame-id entry){:id … :html "…" :delta {…} :failed? bool :continuations [{:id <id> :subtree <hiccup>} …]} (:continuations are nested boundaries discovered while draining — [] in the common case). Drains one continuation against frame-id's app-db; snapshots before-db / after-db and computes the per-subtree delta. Catches throws and surfaces the original fallback HTML inline (per 011 §Failure semantics — inline fallback). v1 advanced 011
streaming-build-final-payload Fn (streaming-build-final-payload frame-id render-hash opts) → canonical :rf/hydration-payload. Called after all continuations drain to populate the __rf_payload final chunk. v1 advanced 011

Standard SSR-related events:

Event What it does Spec
:rf/server-init Per-request server-side initialisation. Reads request cofx; dispatches setup events. :platforms #{:server}. 011
:rf/hydrate Seed the client-side app-db from the server-supplied payload. Runs once on client bootstrap. 011

Standard SSR-related fx (server-only; :platforms #{:server}):

Fx Args Spec
:rf.server/set-status :int (per :rf.fx.server/set-status-args) 011
:rf.server/set-header {:name :value} (per :rf.fx.server/set-header-args) 011
:rf.server/append-header {:name :value} (per :rf.fx.server/append-header-args) 011
:rf.server/set-cookie :rf.server/cookie map 011
:rf.server/delete-cookie {:name ?:path ?:domain} 011
:rf.server/redirect {:location ?:status} (default :status 302); truncates HTML. Caller-trusted :location 011
:rf.server/safe-redirect {:location ?:relative-only? ?:allow ?:status} — the caller-untrusted variant; parses :location, rejects javascript: / data: / vbscript: schemes, and enforces :relative-only? / :allow allowlist before setting :redirect. Open-redirect mitigation for attacker-controlled ?next= strings 011

Standard SSR-related fx (client-side hydration compatibility checks; :platforms #{:client}):

Fx What it does Spec
:rf.ssr/check-version Hydration-side framework version-compatibility check (emits :rf.ssr/version-mismatch on drift). 011
:rf.ssr/check-schema-digest Hydration-side schema-digest compatibility check (emits :rf.ssr/schema-digest-mismatch on drift). 011

Implementation-tier SSR internals (not rowed). The remaining client hydration / streaming lifecycle — verify-hydration!, read-server-payload, streaming-install!, drain-blocking-resources!, and the per-request response / request accessors (get-response / peek-response / flush-response! / get-request / set-request! / clear-request!) — lives in re-frame.ssr but is :implementation-tier host-adapter plumbing. The supported client path is hydrate! (rowed above, :advanced per rf2-kuky.87); a host that must observe the MOUNTED tree splits it into read-server-payload + :rf/hydrate + verify-hydration! (011 §Client-side hydration boot helper writes that discriminator). Per the projection's implementation-tier policy (§Tier taxonomy) they are carried by the manifest but not rowed here — the per-request response accumulator (:rf/response) is read through these accessors, not a subscription.

Standard SSR-related subs: there are none. re-frame.ssr and re-frame.ssr.ring register no subscriptions at all — their only registrations are the :rf/hydrate event, the server-only and client-only fx above, and the :rf.server/request coeffect. In particular there is no :rf/head sub and no :rf/public-error sub; @(rf/subscribe [:rf/head]) cannot resolve. Both keywords name a data shape registered in Spec-Schemas (:rf/head-model and :rf/public-error), not a registry entry. Read them through fns instead: the head model via head-model (it RETURNS the model — there is no side-channel register to read back), and the public-error projection via project-error. (Two rows here previously described them as subscriptions; corrected under rf2-8arzr.6, matching the long-standing statement at docs/api/re-frame.ssr.md §Subscriptions — there are none. Both were keyword rows, which api_md_check skips by design, so no gate contradicted them.)

Standard cofx (server-only):

Cofx Returns Spec
:rf.server/request The active HTTP request map 011

reg-fx's :platforms metadata key (a set containing :server and/or :client) gates fx execution by active platform; default #{:server :client} (universal) when the key is absent. Skipped fx emit a :rf.fx/skipped-on-platform trace event. Detail in 011 §:platforms metadata on reg-fx.

SSR error-projection policy is per-frame metadata (see Conventions §Configuration surfaces bucket 3): a frame opts in via the :ssr {:public-error-id ... :dev-error-detail? ...} map on its make-frame / frame-root config. See 011 §Server error projection for the keys.


HTTP requests (Spec 014)

:rf.http/managed is the canonical, optional HTTP-request fx — v1 (optional capability). CLJS reference ships it on Fetch (browser) and java.net.http.HttpClient (JVM). Args, behaviours, decode pipeline, retry semantics, abort surface, failure taxonomy, and reply addressing are normatively defined in 014-HTTPRequests.md; the surface below is the API-level summary.

API Kind Signature / shape Status Tier Spec
:rf.http/managed fx [:rf.http/managed args-map] — args per 014 §The args map and :rf.fx/managed-args v1 (optional capability) — (fx-id; follows the advanced HTTP artefact) 014
:rf.http/managed-abort fx [:rf.http/managed-abort request-id] — abort the in-flight request with the given :request-id v1 (optional capability) — (fx-id) 014
:rf.http/managed-canned-success fx [:rf.http/managed-canned-success {:value v}] — synthesises the canonical success reply (per 014 §Testing). Registered at load of re-frame.http.test-support (NOT re-frame.http.managed); the stub family ships in the same namespace per (audit-of-audits #15). v1 (optional capability, dev/test) — (fx-id; test) 014
:rf.http/managed-canned-failure fx [:rf.http/managed-canned-failure {:kind <:rf.http/*> :tags {...}}] — synthesises the canonical failure reply. Same registration gate (re-frame.http.test-support) and same co-location with the stub family. v1 (optional capability, dev/test) — (fx-id; test) 014
reg-http-interceptor M (reg-http-interceptor id interceptor-map) — register an HTTP interceptor on a frame's :rf.http/managed middleware chain (per 014 §Middleware). A façade macro (re-frame.core, capturing call-site source-coords; the fn form is re-frame.http.middleware/reg-http-interceptor). id is a keyword; interceptor-map carries at least one of :before (fn [ctx] ctx') and :after (fn [ctx response] response'), plus optional :frame (the EP-0002 override; absent, the carried scope it registers under resolves it — registering under no scope raises :rf.error/no-frame-context, never :rf/default) and any :rf/registration-metadata (:doc / :tags / :schema / :sensitive?). The surface mirrors the event-interceptor {:id :before :after} shape — symmetric request/response sides; :before chain in registration order, :after chain in reverse. v1 (optional capability) advanced 014
Clearing an HTTP interceptor is (rf/clear :http-interceptor id) / (rf/clear :http-interceptor id {:frame target}), rowed in §Clearing registrations — there is no clear-http-interceptor name (rf2-kuky.80). :http-interceptor is one of the two frame-scoped kinds, so the opts map is accepted, and it is exact and fail-closed exactly as before (rf2-s32bf). Omitting it resolves the frame from the carried scope; under no scope that raises :rf.error/no-frame-context — no :rf/default fallback (EP-0002). A valid explicit target need not name a currently live frame: clearing an absent frame's chain is an idempotent no-op, not an error. The frame-first (frame id) spelling is the artefact-internal clear-http-interceptor* seam, not a public arity. 014

Public API surface in re-frame.core for ports that ship Spec 014. Ports that omit it MUST NOT register :rf.http/* for any other purpose (per Conventions §Reserved namespaces).

The HTTP stub family is NOT a re-frame.core façade export — it is test-support infrastructure reached through its home namespace re-frame.http.test-support ((:require [re-frame.http.test-support :as http-test-support])), so it carries no rows above. (with-request-stubs route-map body-fn) is the scoped helper — route-map {[<method> <url>] {:reply ...}}, routed for the dynamic extent of body-fn per 014 §Testing; use the raw install-managed-request-stubs! / uninstall-managed-request-stubs! pair only when stubs must span multiple deftests.

Managed HTTP ships no public var namespace of its own. The request surface is the keyword-addressed [:rf.http/managed args-map] fx and its siblings above; the interceptor surface reaches users through the re-frame.core façade, and the test-stub helpers directly through re-frame.http.test-support. The per-verb call-site helper namespace re-frame.http was deleted pre-alpha (rf2-kuky.11) — an app that wants a shorter call site writes its own request-builder fn over the args map (base URL, default headers, a default :decode — policy a per-verb fn cannot carry).

Reply-payload shape

Every reply lands as the canonical uniform reply envelope (rf2-ibksxg — one dialect, no {:kind :success/:failure} reshape): {:status :ok :value v …} on success, {:status :error :error {:kind <:rf.http/*> …} …} on failure, {:status :cancelled :error {:kind :rf.http/aborted …} …} on abort. Reply addressing is explicit: :reply-to is the unified app-facing spelling — one event vector for both the success and the failure reply, the envelope appended as the last event-vector arg (the app branches on (:status reply)); the same :reply-to key resources / mutations use. :on-success / :on-failure are the split routing sugar — a named target per branch, both receiving the identical envelope. The two styles are exclusive: a map carrying :reply-to beside either branch key raises :rf.error/http-bad-reply-target (:reason :mixed-addressing) at dispatch, on key presence rather than value. All lower to the one internal / normalized :rf/reply-to descriptor (a conformance surface, no longer an everyday spelling). Omitting all reply targets raises :rf.error/http-no-reply-target at dispatch — the co-located default (reply merged under :rf/reply back to the originating event) was retired pre-alpha (rf2-et4c1s). Detailed in 014 §Reply addressing and §Reply payload shape.

Failure categories (closed set)

The eight :kind values inside a failure reply, all reserved under :rf.http/* (per Conventions §Reserved namespaces). See 014 §Failure categories for tags-by-kind:

:kind Meaning
:rf.http/transport Network / DNS / connection error pre-HTTP
:rf.http/cors CORS preflight rejected (CLJS-only)
:rf.http/timeout Per-attempt timeout fired
:rf.http/http-4xx Non-2xx 4xx response
:rf.http/http-5xx Non-2xx 5xx response
:rf.http/decode-failure 2xx response but decode rejected the body
:rf.http/accept-failure :accept returned {:failure user-map}
:rf.http/aborted Request aborted via :request-id or :abort-signal

Trace events emitted by :rf.http/managed

:operation :op-type When
:rf.http/retry-attempt :info Per intermediate attempt that matched :retry :on, plus a terminal retry-sequence stop marker once the sequence ends (budget spent, or a later attempt failed outside :retry :on); carries :request-id, :url, :attempt, :max-attempts, :failure, :next-backoff-ms (nil on the terminal stop marker)
:rf.http.interceptor/registered :info A reg-http-interceptor succeeded; carries :frame, :id (per 014 §Middleware)
:rf.http.interceptor/cleared :info A (clear :http-interceptor id) removed an existing slot; carries :frame, :id
:rf.error/http-interceptor-failed :error An interceptor :before or :after threw; carries :frame, :interceptor-id, :url, :cause (plus :phase :after on the response side). Request side: the request is NOT dispatched; response side: the reply is suppressed (per 014 §Middleware §Failure mode)

Resources (Spec 016)

The Resources artefact (day8/re-frame2-resources, post-v1 optional) ships declarative cached server-state. Its full registration / event / sub / accessor surface is normatively defined in 016-Resources.md; this projection rows the facade exports classified by the standing diff-time rule (every new re-frame.core facade export is classified + justified when it lands). The resource/mutation registrars and the :rf.resource/* / :rf.mutation/* keyword-addressed events, subs, and trace ops follow the artefact's tier (optional capability) and carry no Tier column (they are not vars).

API M/Fn Signature Status Tier Spec Notes
reg-resource M (reg-resource resource-id metadata request-fn) — canonical 3-slot grammar: the :request fetch fn is the third VALUE slot, metadata the reflection + config map (:scope, :params-schema, :data-schema, …) post-v1 lib (optional capability) advanced 016 The :resource registrar kind. Late-bound by the Resources artefact. A :request left inside the metadata map is a loud :rf.error/resource-bad-spec. Front-room of the resources surface but advanced (not front-porch): an optional post-v1 capability a new app does not reach for on day one.
(rf/clear :resource resource-id) — rowed in §Clearing registrations; there is no clear-resource name (rf2-kuky.80). Registration-lifecycle removal (the registrar decrement per Conventions §Tear-down verb axis); disposes resource-runtime state per 016 §Registration. 016
reg-mutation M (reg-mutation mutation-id metadata request-fn) — canonical 3-slot grammar: the :request write fn is the third VALUE slot, metadata the reflection + config map (:params-schema, :invalidates, …) post-v1 lib (optional capability) advanced 016 The :mutation registrar kind — a named causal write. A :request left inside the metadata map is a loud :rf.error/mutation-bad-spec. :rf.mutation/execute carries the call-site :reply-to continuation target.
(rf/clear :mutation mutation-id) — rowed in §Clearing registrations; there is no clear-mutation name (rf2-kuky.80). Registration-lifecycle removal of a mutation. 016
reg-resource-scope M (reg-resource-scope scope-id metadata resolve-fn) — register a pure named db-derived scope resolver in the canonical 3-slot grammar (rf2-bqstzr): the :resolve fn is the value slot, and metadata carries the declared :inputs {name [:db <rf-path>]} (plus optional :doc). :inputs is REQUIRED and there is ONE arity: the resolver's first arg is ALWAYS the resolved inputs map. Reading the whole db is spelled {:inputs {:db [:db []]}}, from which the tooling-marked explicit-cost :whole-db? is DERIVED. A :resolve inside the metadata map is rejected loudly. The :resource-scope registrar kind. post-v1 lib (optional capability) advanced 016 Facade classification (EP-0016 D3): a re-frame.core export of the optional Resources artefact, justified as the single scope-resolution currency reused by resource registration, route resources, event ensure, subscriptions, invalidation descriptors, exact targets, and clear-scope. advanced — an optional-artefact authoring surface, not front-porch. Per 016 §Named resource-scope resolvers.
(rf/clear :resource-scope scope-id) — rowed in §Clearing registrations; there is no clear-resource-scope name (rf2-kuky.80). The registrar decrement counterpart of reg-resource-scope (per Conventions §Tear-down verb axis). 016
resolve-resource-scope Fn (resolve-resource-scope db scope-id) — resolve a named scope resolver against a supplied db value; returns the canonical scope or nil. A pure resolver helper, not an effect (no app-state / dispatch side effects) and no observability side effect — it routes through the trace-free pure evaluator, so unlike the causal resolution sites it does not emit :rf.resource/scope-resolved. post-v1 lib (optional capability) advanced 016 Facade classification (EP-0016 D3 / issue 7): a re-frame.core export justified as the ergonomic helper for the logout/account-switch idiom — resolve the concrete old scope from the handler's coeffect db (no :snapshot-db payload, which would be an egress-bearing record under EP-0015). A plain function over the resolver registry; no new effect-API surface, no resolution-timing ambiguity. It routes through the trace-free pure evaluator, so — unlike the causal {:from-db …} / route-entry / mutation-settle resolution sites — it does not emit :rf.resource/scope-resolved (rf2-ru73k6 F3: a passive read advertised as pure has no observability side effect). Per 016 §clear-scope resolves the concrete scope from the coeffect db.
A registered resource's / mutation's spec map has no per-kind accessor (rf2-kuky.31): it is (:rf/resource (rf/handler-meta {:source :store :kind :resource :id id})) and (:rf/mutation (rf/handler-meta {:source :store :kind :mutation :id id})) — the generic registrar query plus the documented inner-key projection, rowed in §Public registrar query API. 016
resource-state Fn (resource-state {:resource … :scope … :params … :frame …}) — a resource instance's live runtime state (explicit frame target) v1 (optional capability) advanced 016
mutation-state Fn (mutation-state {:instance … :frame …}) — a mutation instance's durable runtime row ({:status :result :error …}), or nil (explicit frame target) v1 (optional capability) advanced 016

A :revalidate-on #{:focus :reconnect} frame's focus/reconnect listeners are installed / reconciled / removed by the FRAME LIFECYCLE, automatically — creation installs exactly the declared subset (:focus covers window focus AND document visibilitychange-to-visible; :reconnect covers window online), re-registration reconciles (replace-don't-stack, and a re-registration that drops the key relinquishes), destroy removes. :revalidate-on is a frame-config key on make-frame / frame-root, not a call: there is no install/remove fn, and the retired install-revalidation-listeners! / remove-revalidation-listeners! exports are GONE (pre-alpha, no back-compat shim). An absent key or an explicit #{} installs nothing; declaring the key without the Resources artefact on the classpath fails loud with :rf.error/resources-artefact-missing. This is the :url-bound? fold one key over — see §Routing.

Request decoration (EP-0016 Rider 3) reuses the existing HTTP facade. Auth/tracing/base-URL/retry decoration for resources and mutations is not a new resources surface — it is the existing reg-http-interceptor / (rf/clear :http-interceptor id) (§HTTP requests), registered once per frame and applied to every :rf.http/managed request (resource reads, mutations, plain managed calls). No new facade export is introduced for decoration; the doctrine is ownership (transport decoration lives in the managed-HTTP seam), per 016 §Request decoration belongs to the managed-HTTP seam.


Effect-map shape

Closed: seven top-level keys — #{:db :rf.db/runtime :fx} plus the four EP-0025 commit-plane classification effects #{:sensitive :large :clear-sensitive :clear-large}. Ordinary app handlers return only :db + :fx; :rf.db/runtime is reserved by convention for framework / runtime-extension authority (it writes the runtime-db partition — non-framework handlers emitting it are surfaced by dev diagnostics, not silently dropped). See Spec-Schemas §:rf/effect-map. Top-level :dispatch / :dispatch-later / :dispatch-n from v1 migrate via MIGRATION.md §M-8.

A foreign top-level key REFUSES the event. All seven keys above are commit-plane effects, applied together at the atomic commit boundary; any other top-level key is caught pre-commit at the router's FINAL-effects boundary, emits :rf.error/effect-map-shape on the always-on error channel, and aborts the event — no :db, no :rf.db/runtime, no classification install, no :fx. No partial commit, and nothing is silently dropped.

Key Notes
:db New app-db partition (replaces). The app-facing state key.
:rf.db/runtime New runtime-db partition (replaces). Reserved by convention for framework / runtime-extension authority — ordinary app handlers do not emit it.
:fx Vector of [fx-id args] pairs.
:sensitive EP-0025 commit-plane classification. A vector of :rf/path vectors, classified sensitive (durable app-db egress redaction). Applied WITH the :db write into the per-frame elision registry — not routed through :fx.
:large EP-0025 commit-plane classification. A vector of :rf/path vectors, classified large (durable app-db egress size marker). Applied WITH the :db write.
:clear-sensitive EP-0025 commit-plane classification. A vector of :rf/path vectors, un-classified from the sensitive axis (independent of :large).
:clear-large EP-0025 commit-plane classification. A vector of :rf/path vectors, un-classified from the large axis (independent of :sensitive).

Standard :fx entries:

[fx-id args] Args Status Spec Notes
[:dispatch [event-id ...]] event vector v1 002
[:dispatch-later {:ms ms :event event-vec}] options map v1 002
[:rf.http/managed args-map] args per 014 §The args map v1 (optional capability) 014 Framework-provided when the implementation ships Spec 014. CLJS reference: ships on Fetch + JVM HttpClient. See also :rf.http/managed-abort, :rf.http/managed-canned-success, :rf.http/managed-canned-failure.
[:rf.nav/push-url url-string] URL string v1 012
[:raise event-vec] event vector v1 005 machine-only: reserved fx-id recognised by the machine handler; routes the event back into the same machine, atomic and pre-commit. Outside a machine action's :fx, this fx-id is unbound.
[:rf.machine/spawn spawn-spec] spawn-spec map (per :rf.fx/spawn-args: :machine-id/:definition, :id-prefix, :data, :start) v1 005 Canonical actor-lifecycle fx (registered globally by re-frame.machines); installs a new dynamic actor (whose snapshot lives at [:rf.runtime/machines :snapshots <gensym'd-id>]). On the declarative :spawn / :spawn-all path the reducer binds the assigned id into the spawning machine's own :data under :rf/spawned (XState-context parity, per 005 §Recording the spawned id user-side). Emitted from any event handler's :fx (including machine actions and the :spawn desugar).
[:rf.machine/destroy actor-id] actor id (keyword) v1 005 Canonical actor-destroy fx (registered globally by re-frame.machines); runs the actor's :exit action, dissociates [:rf.runtime/machines :snapshots <actor-id>], and clears the actor's event-handler registration. Symmetric counterpart to :rf.machine/spawn.

Public registrar query API

For tooling, agents, story tools, 10x.

One query map, naming EXACTLY ONE source. Each of registrations / handler-meta takes exactly one argument: a query map carrying either :source :store or :frame f, never both and never neither.

(rf/registrations {:source :store :kind :event})            ; => {id metadata} or {}
(rf/registrations {:frame app-frame :kind :event})
(rf/handler-meta  {:source :store :kind :event :id :cart/add})  ; => metadata or nil
(rf/handler-meta  {:frame app-frame :kind :resource :id :articles/list})

Why the source is explicit. The retired positional arity — (registrations :event) — documented itself as reading the default source store, but delegated to re-frame.registrar's generation-aware reads, which consult the dynamic image generation FIRST. That generation is bound around every subscribe build, dispatch, fx and view resolution against an image-loaded frame, so a "store" read issued from inside a sub computation or an event handler silently read that frame's image instead. A public inspection request has to say which source it means; a bare keyword cannot. (rf2-kuky.30.)

Failures. A map carrying BOTH selectors, NEITHER, a :source other than :store, or a non-map argument throws :rf.error/registrar-query-needs-source. :source admits only :store today — the key exists so a future source can be added without another arity. :kind :flow or :kind :frame (the reserved-but-EMPTY registrar slots) throws :rf.error/registrar-kind-not-queryable, whose message names the real door — re-frame.flows/flows / flow-meta / flows-snapshot, frame-ids / frame-meta — rather than returning an authoritative-looking {}. Any other non-registry kind throws :rf.error/unknown-registry-kind.

Projections are Clojure, not library operations. There is no :pred key and no predicate arity: (keys (rf/registrations {:source :store :kind :route})), or (into {} (filter (fn [[_ m]] (:rf/machine? m))) (rf/registrations {:source :store :kind :event})).

Inner-key projection is the documented contract. A registration that carries a feature SPEC exposes it under a reserved inner key on its own metadata, and reading that key is the supported way to reach it — there is no per-kind <kind>-meta accessor (rf2-kuky.31). A machine is an :event registration carrying :rf/machine? true, and its registered spec reads back as:

(:rf/machine (rf/handler-meta {:source :store :kind :event :id :auth.login/flow}))

which is nil unless that :event registration is a machine. See 005 §Querying machines.

The complete set of inner keys is one per registrar kind that nests a spec:

Kind Inner key Reads back
:event (with :rf/machine? true) :rf/machine the reg-machine spec
:resource :rf/resource the reg-resource spec
:mutation :rf/mutation the reg-mutation spec
:resource-scope :rf/resource-scope the reg-resource-scope spec

Every other kind — :route among them — carries its metadata at the TOP level of the registration, so handler-meta alone is the whole read. Enumeration is likewise generic: (keys (rf/registrations {:source :store :kind :resource})), and so on for every kind.

No realm coordinate. (registrations {:realm r :kind k}) and friends do not exist, there is no re-frame.realm namespace, and frame resolution routes directly through the process registrar (re-frame.registrar / re-frame.frame / re-frame.image).

Frame-targeted query. The map-shaped form — (registrations {:frame f :kind k}), (handler-meta {:frame f :kind k :id id}) — resolves the (kind, id) set through live frame f's own sealed image generation ("target frame → resolved image generation → registration resolution"), surfacing the :rf.provenance/ns + inline/image facts the resolved descriptors carry (cross-image overrides are reported on the generation's :rf.gen/shadows shadow report). :frame is a registered frame id (keyword) OR a frame value (rf/make-frame's return token) — the same target shape rf/frame-generation accepts; the public routing address is the frame id. This is the READ of the image→frame model: the public tooling surface a tool reaches instead of the internal re-frame.live-frame / re-frame.image-assembly namespaces. Fail-loud: a :frame that does not resolve to a live frame carrying a generation throws :rf.error/frame-no-generation (no fallback to the default registrar — the read needs a live frame). The dedicated raw read frame-generation (below) returns the whole sealed generation. The {:source :store …} form never touches the generation path — it reads the process source store atom directly, so it answers the same from inside a frame-resolved context as it does from the REPL.

API M/Fn Signature Status Tier JVM-runnable? Spec
registrations Fn (registrations {:source :store :kind k}) / (registrations {:frame f :kind k}){id metadata-map}, {} if none. Use when you want metadata — registry walks that read source-coords, :rf/sensitive, :rf/machine?, :platforms, etc. :source :store reads the process source store and never consults a bound image generation. :frame f resolves through live frame f's own sealed image generation (only the ids that frame's image carries, with :rf.provenance/ns provenance facts); f is a frame id or a direct frame object, fail-loud on an unresolvable one. Exactly one selector, else :rf.error/registrar-query-needs-source; filtering is filter over the result (no :pred). See the notes above. v1 tooling 002
handler-meta Fn (handler-meta {:source :store :kind k :id id}) / (handler-meta {:frame f :kind k :id id}) → registration-metadata map, or nil. View registrations include source-coord keys (:ns / :line / :column / :file) per :rf/source-coord-meta (Spec-Schemas); pair tools resolve data-rf2-source-coord DOM annotations to :file via this lookup. :source :store reads the process source store and never consults a bound image generation; it is also the form the two DERIVED machine kinds take — (handler-meta {:source :store :kind :machine-guard :id [machine-id guard-id]}). :frame f resolves through live frame f's sealed image generation (with :rf.provenance/ns provenance facts), or nil when that frame's image carries no such [k id]; fail-loud on an unresolvable f. Exactly one selector, else :rf.error/registrar-query-needs-source. See the notes above. v1 tooling 002
frame-generation Fn (frame-generation f) → the sealed, resolved image generation live frame f is running — the inert image-assembly value with the four documented stable public keys :rf.gen/resolver ({[kind id] descriptor}), :rf.gen/images (the normalized images in :images order — later wins), :rf.gen/kinds, and :rf.gen/shadows (the cross-image shadow report — [{:registration [kind id] :image <defined-in> :shadowed-by <winner>} …]; [] when nothing was overridden — read the report directly off this key). f is a registered frame id or a direct frame object. The dedicated raw read over the frame→generation model — for describe-image-style views that want the whole generation (selected registrations, shadow report, provenance) without per-kind round-trips; the {:frame f …} form of the pair above gives per-(kind, id) resolution with provenance. Fail-loud :rf.error/frame-no-generation when f does not resolve to a live frame carrying a generation (no nil-as-default, no realm fallback). (There is no :rf.gen/requires capability slot — image-declared host capabilities are removed.) The public tooling surface tools (Pair MCP, Xray) reach instead of re-frame.live-frame / re-frame.image-assembly internals. EP-0023 tooling 002
frame-ids Fn (frame-ids) / (frame-ids ns-prefix) v1 tooling 002
frame-meta Fn (frame-meta frame-id) v1 tooling 002
app-db-value Fn (app-db-value frame-id) → the app-db partition value (plain map) — the out-of-band value read. The front-porch read is subscribe; app-db-value is the non-reactive snapshot read for tools, tests, REPL, and fx/handler bodies. v1 advanced 002
frame-state-value Fn (frame-state-value frame-id) → the coherent frame-state projection {:rf.db/app <app-db> :rf.db/runtime <runtime-db>}. The full-frame read for SSR / epoch / time-travel / Xray (EP-0001). The runtime-db-only read (retired runtime-db-value, rf2-t3lftq — API-shrink #3) is (:rf.db/runtime (frame-state-value frame-id)). v1 tooling 002

The static subscription-topology query and the runtime sub-cache snapshot are subscription-tooling surfaces, not re-frame.core facade reads. Reach them through their owning namespace re-frame.subs.tooling(sub-topology) (static dependency graph over the registrar) and (sub-cache-snapshot frame-id) (live cache state) — with subs/sub-topology / subs/sub-cache-snapshot as the JVM legacy aliases.

Schema-introspection accessors — app-schemas, app-schema-meta, app-schemas-digest — are rowed canonically in §Schemas.

compute-sub is rowed canonically in §Testing (pure sub computation against an app-db value).


App values and composition (EP-0013)

Removed from the public facade. This section is retained as a stable anchor for inbound cross-references; the surface it once documented is no longer public.

The retired composition vocabulary is not on the public facade. The app/realm/module composition surface — rf/app / rf/module (+ the rf/app-registrations / rf/app-requires / rf/app-owns inspectors), rf/install! / rf/reinstall!, rf/realm / rf/dispose-realm!, and the realm reads rf/realm-ids / rf/frame-realm / rf/installed-app — is not part of re-frame.core. The public model is image → frame → event stream: a feature namespace registers ordinary reg-* forms; an rf/image selects them by :select-ns provenance (or defines them inline via :registrations) and is supplied to make-frame (see §Registration) via :images (the later image wins; the generation's :rf.gen/shadows report — read via frame-generation — names cross-image overrides); re-calling make-frame against the SAME :id with a new :images vector hot-reloads a frame's image generation in place (preserving frame memory; the reload diff is a read — generation-diff over two frame-generation values, not a bespoke verb); and a frame is addressed by its process-local frame id (or a frame value — the routing ops normalize a value to its id, so it is accepted directly; API-shrink #1 rf2-csbbwu), with no realm coordinate. There is no re-frame.realm namespace, no installed-app value, and no realm coordinate on any wire record (see Spec-Schemas §:rf/realm): frame resolution routes directly through the process registrar (re-frame.registrar / re-frame.frame / re-frame.image), not a realm. The app/realm/module construction model — and the EP-0013→EP-0023 migration mapping — is documented in EP-0023 §Backwards Compatibility and the EP history; it is not a live data surface (there are no rf/migration-map / rf/migration-explain facade reads). For tooling that reads a frame's resolved registrations, use the frame-targeted {:frame f …} registrar queries + frame-generation (§Public registrar query API).


Schemas

Namespace: the introspection surfaces below live in re-frame.schemas (artefact day8/re-frame2-schemas); consumers (:require [re-frame.schemas :as schemas]). They are not re-exported from re-frame.core — apps targeting schemas add the artefact and require the namespace directly. The registration macros (reg-app-schema / reg-app-schemas) live in re-frame.core and route through the schemas artefact at registration time. Per the §Conventions per-artefact namespace table.

reg-app-schema is rowed canonically in §Registration.

The :frame opt is a frame TARGET. Every schema opts surface that names a frame — the :frame key of reg-app-schema's metadata map, the opts of reg-app-schemas, and the :frame slot of the reads app-schemas / app-schema-meta / app-schemas-digest — accepts either a frame-id keyword OR a frame value (rf/make-frame's return token), the same target shapes the registrar query API's :frame accepts. A frame value is normalized to its frame id (the routing address) before it keys the per-frame schema store, so a schema registered against a frame value is found by a later read-by-id (and vice versa). An explicit :frame that resolves to a non-keyword target (a string, a non-frame map, a vector) fails loud with :rf.error/app-schemas-bad-arg rather than silently becoming an unreachable registry key.

The READS take ONE map, and :frame is REQUIRED on it (rf2-kuky.84). app-schemas / app-schema-meta / app-schemas-digest each take a single opts map. There is no ambient-frame default — a schema read is a tooling question ABOUT a named frame, not an operation inside one — and no bare-frame-id or trailing frame-target sugar: a live frame VALUE is itself a map, so a type-sniffing positional argument could never be read locally (Principles §Name over place). A frameless or non-map call raises the catalogued :rf.error/no-frame-context with a message naming the {:frame f} spelling; there is no new error id. The frame need not be LIVE — no liveness or image resolution happens on this lane, so a read of a frame holding nothing answers {} / nil / the empty-set digest. The reg-* WRITE surfaces keep their own opts sugar; that is a Spec 010 registration contract, not a read.

The validator port is a VALUE, reached through one door. To swap the validator / explainer / printer at boot (e.g. drop Malli for a clojure.spec or Zod-style port), use set-schema-fns! — the single installer, which takes any subset of the three from one map so they never drift mid-boot. schema-fns reads back what is installed, and default-schema-fns is the framework's own bundle as a value. Those three cover every use: swapping one fn is a one-key install, restoring the framework defaults is (set-schema-fns! default-schema-fns), and capture / stub / restore is a let + finally over (schema-fns).

API M/Fn Signature Status Tier Spec
app-schemas Fn (app-schemas {:frame frame-id}){path registration-metadata} — a frame's WHOLE app-db schema registration map, or {}. The same {id → meta} shape registrations answers for registrar kinds; each value carries :path, :schema, :frame and the source-coords :ns / :line / :file. (update-vals … :schema) projects the schema values alone. v1 tooling 010
app-schema-meta Fn (app-schema-meta {:frame frame-id :path path}) — return the full registration-metadata map (:path, :schema, :frame, plus source-coords :ns / :line / :file and the rest of :rf/registration-metadata) for ONE registered app-db schema, or nil. Both keys are required. Pair-tool and 10x consumers reach for this when they need the registration anchor (e.g. click-back-to-code); (:schema …) is the schema value alone. Per 010 §Schemas as a tooling/agent surface and Spec-Schemas §:rf/app-schema-meta. v1 tooling 010
app-schemas-digest Fn (app-schemas-digest {:frame frame-id}) → string — computed over the {path → schema} projection, so its bytes are independent of the metadata riding alongside. v1 tooling 010
set-schema-fns! Fn (set-schema-fns! {:validate validate-fn :explain explain-fn :print print-fn})the one validator-port door. Install any subset of the validator / explainer / printer bundle from one map. Each key is optional; an absent key leaves the existing registration in place, so a one-key call is the way to swap a single fn. The one-call substitute-Malli boot pattern (a Zod / clojure.spec port installs all three together so they never drift mid-boot). :print nil coerces to the default EDN canonicaliser so the digest is never undefined; :validate nil / :explain nil disable that fn. Last-write-wins per key; writes are NOT transactional. Returns the installed bundle as a map {:validate … :explain … :print …} reflecting the live state of all three fns after the call — including keys the call did not touch, so a caller wanting back just the fn it installed selects that key. Also the restore path: install a value read from schema-fns, or default-schema-fns. Per 010 §Default validator and the validator-fn extension point. v1 advanced 010
schema-fns Fn (schema-fns){:validate … :explain … :print …} — the READ half of the port. Returns the installed validator / explainer / printer in the same shape set-schema-fns! accepts and returns, so (set-schema-fns! (schema-fns)) is a no-op. :validate / :explain may be nil; :print never is. This is what test isolation is built from — capture, stub, and reinstate in a let + finally, without a dedicated snapshot or restore verb and without reaching the framework-internal atoms. Per 010 §Default validator and the validator-fn extension point. v1 advanced 010
default-schema-fns Var (bundle value) default-schema-fns — the framework's own validator bundle as a plain map carrying exactly :validate / :explain / :print; the state the port holds before an app installs anything. (set-schema-fns! default-schema-fns) restores the framework defaults, and because the value carries the same fn objects the port was seeded with, the :rf.warning/schema-validator-unavailable check for "still on the framework default" answers true again afterwards. Named for what it is — the FRAMEWORK default, not "the Malli bundle": its :print is the EDN canonicaliser rather than anything Malli supplies, and its :validate / :explain soft-pass while the Malli adapter is unloaded. Per 010 §Default validator and the validator-fn extension point. v1 advanced 010

See 010 §Schemas for :schema metadata, validation timing, and dev/prod elision. (The framework does not accept a :spec metadata key on reg-* metadata; the key is :schema. Per MIGRATION §M-54.)


Event-emit (always-on, production-survivable)

Per 009 §What IS available in production (#2). A minimal always-on listener surface that survives :advanced + goog.DEBUG=false and delivers one tight record per processed event. Parallel to (not a fallback for) the dev-only trace surface; per-event only — no per-sub, per-fx, or per-:rf.event/db-changed records. Record shape {:event :event-id :frame :time :outcome :elapsed-ms} (the always-on event-emit substrate's own record fields, distinct from trace :tags keys); the :event slot is passed through the size-elision wire-boundary walker once before fan-out, so schema-marked :sensitive? paths land as :rf/redacted and :large? paths land as :rf.size/large-elided. :outcome is one of :ok (clean settle), :error (the interceptor chain threw), :rolled-back (a candidate :db / machine-data transition was refused before install), :flow-error (a flow's :output threw), or :rejected (a :boundary? true handler's :schema refused the event's payload, so the handler never ran) — an aborted dispatch is never reported as a clean :ok. Two of those five behave differently under a release build, and the pair is worth reading together: :rolled-back has no producer in production (candidate validation is dev-only), while :rejected does, because boundary validation is ungated per 010 §Production builds. It is not the only schema check a release build keeps — C-000.35 settles that by what the check is for, not by who declared the schema it reads, and the framework's other load-bearing checks are ungated alongside it — but it is the only one this :outcome vocabulary names in its own right; the others throw on their own paths, and a throw reports :error. See 009 §What IS available in production for the full per-member contract.

This substrate is IMPLEMENTATION tier; the public door is the sink. The registry fans an UNPROJECTED record across EVERY frame and is not routed under any frame's egress policy, so it is not a surface an application registers on: :events LEFT the public register-listener! vocabulary (see §Observation listeners). Production observation for hosted back-ends (Datadog / Honeycomb / Sentry / …) is the frame-owned :observability :handled-events sink (Spec 015 §Frame-owned observability sink policy), declared on the frame's make-frame / frame-root config — or once per process with (rf/configure! {:observability …}) (§Configure keys) — and wired with register-observability-sink!, which hands the sink an already-PROJECTED record. A cross-frame seat is the process default; a raw record is an explicit :rf.egress/local-raw profile on that entry.

Sensitive data marking is path-based per the upcoming data-classification mechanism (separate spec doc; in progress). The handler-meta :sensitive? annotation is removed.

The always-on event-emit registry is addressable via re-frame.event-emit for framework-internal consumers and for tests (which also own between-scenario clears). It carries no public registration verb: an application declares a :handled-events sink instead.

Error-emit (always-on, production-survivable)

Per 009 §What IS available in production. Sibling of the event-emit surface above; runs through the always-on error-emit substrate. Survives :advanced + goog.DEBUG=false.

This substrate is IMPLEMENTATION tier; the public door is the sink. The registry fans the record across EVERY frame UNPROJECTED — the :event vector is wire-elided, but the :exception rides RAW and no frame egress policy is applied, so raw owner-local data can leave a frame here. That fail-open posture is why it is not an application-facing surface: :errors LEFT the public register-listener! vocabulary (see §Observation listeners), and independent corpus observation regardless of a frame's policy is WITHDRAWN as a public primitive. The off-box error-observation surface is the frame-owned :observability :errors sink (Spec 015 §Frame-owned observability sink policy), declared on the frame's make-frame / frame-root config — or once per process with (rf/configure! {:observability …}) (§Configure keys) — and wired with register-observability-sink!, which PROJECTS the record under the owning frame's classification + the sink's egress profile (sensitive paths redacted, :exception dropped under :rf.egress/public-error) BEFORE the sink sees it. :exception was never the discriminator between the two doors: the sink also delivers :exception under the default :rf.egress/off-box-observability profile — only :rf.egress/public-error drops it, and then only the record's top-level throwable. The two carries the raw registry alone had are now the process default's: a FRAMELESS record (:frame nil) and a record whose frame incarnation is already dead both reach (rf/configure! {:observability …}) (§Configure keys) under an explicitly nil governing frame, and a dead frame's id still never resolves to a same-id successor's sink. A post-mortem shipper that needs the host throwable + stack declares :rf.egress/local-raw on its entry.

The listener payload is a union of three record shapes: (a) the per-event error record{:error :event :event-id :frame :time :exception :elapsed-ms} — fanned out by dispatch-on-error! once per catalogued production-reachable per-event runtime :rf.error/* event; (b) the frame-teardown report{:error :rf.error/frame-teardown-failed :frame :hook-failures :reason :recovery :time} — one bounded record per frame destroy whose best-effort cleanup hooks threw (EP-0008 promotion criterion; see 009 §Observability channels and the promotion criterion); and (c) the six EP-0008-promoted SSR non-event categories (:rf.error/ssr-render-failed, :rf.error/ssr-streaming-writer-failed, :rf.error/malformed-hydration-payload — incl. a pre-frame frameless :frame nil sub-path — :rf.error/ssr-head-resolution-failed, :rf.error/sanitised-on-projection, :rf.error/ssr-ring-error-view-failed), flat union records {:error :frame :time …category keys…}. The two non-event arms (b) and (c) ride the general dispatch-error-record! helper and carry no :event / :event-id; the teardown report carries a :hook-failures vector instead of the per-event :exception / :elapsed-ms slots. Listener bodies MUST branch on (:error record) (or otherwise tolerate a record with no top-level :event / :exception) rather than assuming the per-event shape. The per-event record's :event slot is passed through the size-elision wire-boundary walker once before fan-out, so schema-marked :sensitive? paths land as :rf/redacted and :large? paths land as :rf.size/large-elided. This is the single error-observability surface; recovery is the framework's typed per-category default, not app-steerable (the per-frame :on-error recovery policy was removed). Per-listener exceptions are isolated — a buggy listener cannot block siblings or the run.

Sensitive data marking on the error-emit substrate is path-based per the upcoming data-classification mechanism (separate spec doc; in progress). The handler-meta :sensitive? annotation is removed — the per-path elision wire-walker is the sole redaction surface on this path.

The always-on error-emit registry is addressable via re-frame.error-emit + the :error-emit/register-error-listener! late-bind hooks for framework-internal consumers (router fan-out, the SSR error projector, the Fresco server's one-render window) and for tests, which also own between-scenario clears. It carries no public registration verb: an application declares an :errors sink instead.

Observation listeners

One stream-parameterized listener verb registers an observation callback across the two raw dev observation streams — the differentiator is data (which stream), so it rides in a leading required stream keyword (in place of per-channel register-(trace|epoch)-listener! pairs). The closed stream vocabulary is :trace / :epoch; an unknown stream throws :rf.error/unknown-listener-stream (no bare trace default, no compatibility aliases). Both members are raw and DCE-able, so the vocabulary carries its own tier: this verb means raw dev stream.

Production observation is a different verb. The frame-owned observability sink is NOT a :sink stream here — it is declared as frame policy, or once per process via configure!, and delivers an already-PROJECTED record. The always-on :events / :errors streams that used to sit in this vocabulary were a second, fail-open production door — unprojected, raw :exception, no frame policy, fanned across every frame — and they are RETIRED from the public API. Independent corpus observation regardless of a frame's policy is withdrawn as a public primitive; the substrates survive as the implementation-tier registries re-frame.event-emit / re-frame.error-emit for the framework's own capture sites and for tests.

Stream Axis Record
:trace dev-only (DCE'd in production) one trace event per call
:epoch optional artefact (dev-only) one :rf/epoch-record per dequeued event; no-op returning nil when day8/re-frame2-epoch is absent
API M/Fn Signature Status Tier Spec
register-listener! Fn (register-listener! stream id listener-fn) — register listener-fn under id on stream (:trace / :epoch). Re-registering the same id on the same stream replaces. Returns id (or nil on :epoch when the epoch artefact is absent). Unknown stream throws :rf.error/unknown-listener-stream. v1 tooling 009
unregister-listener! Fn (unregister-listener! stream id) → nil. No-op on :epoch when the epoch artefact is absent. v1 tooling 009

There is deliberately no facade clear-listeners! verb: dropping every listener on a stream is a test-isolation concern owned by the fixture layer, not the public facade. re-frame.test-support's reset clears the registries through the lower-level sinks directly (re-frame.trace.tooling/clear-listeners!, re-frame.event-emit/clear-event-listeners!, re-frame.error-emit/clear-error-listeners!, and the :epoch/clear-epoch-listeners! reset hook) — the former per-stream clear-listeners! façade verb was retired in API-shrink #4.

Tracing

All tracing is dev-only (elided in production). See 009 §Tracing for emit semantics and synchronous listener delivery. Trace-listener registration uses the stream-parameterized listener verb with the :trace stream.

API M/Fn Signature Status Tier Spec
emit-trace-event! Fn (emit-trace-event! op-type operation tags) → nil v1 (dev-only) tooling 009
re-frame.interop/debug-enabled? Var ^boolean. CLJS: alias of goog.DEBUG — constant-folded by Closure under :advanced, so :advanced + goog.DEBUG=false builds DCE every (when interop/debug-enabled? ...) branch. JVM: a def read ONCE at ns-load from the Java system property -Dre-frame.debug (winning on conflict) or the environment variable RE_FRAME_DEBUG; defaults true (dev parity). Accepts the conventional false-y vocabulary case-insensitively (false, 0, no, off, empty string) with whitespace trimmed; anything else leaves the flag at true. Set BEFORE re-frame.interop loads. SSR / webhook receivers / long-running JVMs facing untrusted input MUST set the gate false explicitly — per 009 §JVM builds and Security §Production gates. v1 tooling 009
re-frame.performance/enabled? Var ^boolean goog-defined (CLJS) / ^:const false (JVM). Set via :closure-defines {re-frame.performance/enabled? true} to bracket event dispatch / sub recompute / fx walk / view render, emitting a single options-bag performance.measure(name, {start, end}) per bracket (User-Timing measure entries rf:event:*, rf:sub:*, rf:fx:*, rf:render:*; no performance.mark entries are allocated). The measure is cleared by name after emit unless the companion re-frame.performance/retain-entries? goog-define (default false) is set — when set, entries persist in the host User-Timing buffer for one-shot getEntriesByType("measure") readers. Compile-time only — not a (rf/configure! ...) knob; runtime mutation has no effect. Default false; under :advanced + default the bracket DCEs and shipped binaries carry zero User-Timing instrumentation. CLJS-only — JVM is a no-op. See 009 §Performance instrumentation and Tool-Pair §Performance API consumption v1 tooling 009
trace-buffer Fn (trace-buffer frame-id) / (trace-buffer frame-id opts) → the named frame's event-keyed ring, oldest-first (event bundles by default; {:flat true} for raw trace events). [] for a destroyed / never-registered frame and in production v1 (dev-only) tooling 009
clear-trace-buffer! Fn (clear-trace-buffer! frame-id) / (clear-trace-buffer!) → nil — empty the named frame's ring, or every frame's. A data clear: retention policy is preserved (the :trace-buffer process default and each frame's explicit :rf.trace/events-retained override both survive) and the hot-reload dedup table is untouched. The fixture-grade reset that also resets policy is re-frame.trace.tooling's concern, not a public verb. No-op for an unknown frame and in production v1 (dev-only) tooling 009
(rf/configure! {:trace-buffer {:events-retained N}}) See §Configure keys. v1 (dev-only) — (configure key) 009
re-frame.trace.projection/group-by-event Fn (group-by-event events) → vector of event bundles {:dispatch-id :event :handler :fx :effects :subs :renders :other}, sorted by emission order. Pure data; JVM-runnable. Not on the facade — require re-frame.trace.projection directly (see 009 §Event-bundle projection). v1 (dev-only) tooling 009
re-frame.trace.projection/domino-bucket Fn (domino-bucket trace-event)#{:event :handler :fx :effect :sub :render :other}. Classifies a raw trace event into the six-domino slot used by group-by-event. Pure data. Same namespace, not on the facade. v1 (dev-only) tooling 009

Trace-emission opt-out (per-handler metadata)

Event-handler registration accepts a :rf.trace/no-emit? true metadata flag. When set, the runtime suppresses every trace emission and event-emit record within the handler's scope — the handler runs invisibly to the trace surface, the event-emit substrate, and (transitively) the epoch buffer. Used by framework-internal bookkeeping handlers (Xray, Story, re-frame2-pair-mcp, story-mcp) that would otherwise saturate the trace stream. Per Conventions §Reserved namespaces the :rf.trace/* namespace is framework-owned.

Metadata key Where Value Default Effect
:rf.trace/no-emit? reg-event metadata map boolean false When true, suppresses all trace + event-emit emissions inside the handler's scope. Per 009 §Trace-emission opt-out.
:rf.trace/frame-no-emit? frame config map (make-frame / frame-root) boolean false When true, marks the frame a tool / inspector frame: the runtime suppresses every trace emission tagged with that frame, so the inspector's own reactivity does not flood the shared ring it inspects. The frame-scoped sibling of :rf.trace/no-emit?. Per 009 §Frame-level trace-emission opt-out.

Epoch history (per Tool-Pair)

Per-frame epoch snapshots, recorded once per dequeued event (at each event's run-to-completion boundary — not once per drain: a drain that processes a parent event and the :fx [[:dispatch …]] child it queued commits two records) in dev builds. Used by pair-shaped tools for time-travel and post-mortem analysis. Production builds elide entirely.

All rows below are the Tool-Pair time-travel surface — pair-shaped dev tools (Xray, the pair-MCP servers), so they tier tooling (the classification guidance also calls epoch-listener registration "advanced power-user"; either way the surface is back-room and opt-in — never front-porch). The state-injection member is the ONE partial-map mutator replace-frame-state! (rf2-t3lftq — API-shrink #3 consolidated the former replace-app-db! / reset-app-db! / replace-runtime-db! / replace-frame-state! four-mutator family into this): a present partition key replaces that partition, an absent key is preserved — a db-shaped key never silently replaces the other partition.

API M/Fn Signature Status Tier Spec
epoch-history Fn (epoch-history frame-id) → vector of epoch records. Returns [] for an unknown / destroyed frame (per Tool-Pair §Surface behaviour against destroyed frames). v1 (dev-only) tooling Tool-Pair
restore-epoch! Fn (restore-epoch! frame-id epoch-id) → boolean (true on success). Rewinds to the record's canonical :frame-state-after (BOTH partitions — reviving machines / routes / elision / SSR), not just the app-db projection (EP-0001). Returns false and emits a structured trace on any of its seven failure modes — an unknown / destroyed frame (:rf.error/no-such-handler, kind :frame), plus the six :rf.epoch/restore-* preconditions (drain-in-progress, unknown epoch, non-ok record, schema-digest mismatch, missing handler, machine version mismatch) enumerated in the epoch trace-events table below (per Tool-Pair §Surface behaviour against destroyed frames). v1 (dev-only) tooling Tool-Pair
replay-epoch! Fn (replay-epoch! frame-id epoch-id) / (replay-epoch! frame-id epoch-id opts) → structured envelope. Re-drives the named retained epoch's recorded event through the frame's own handlers in ONE call as a strict replay — the raw :trigger-event, the recorded post-generation :rf.cofx under :rf.cofx/mint-policy :strict, and the record's own :fx-overrides / :interceptor-overrides, all resolved in-process (nothing exported or copied by hand). Same frame in and out; no implicit restore; records a new ordinary epoch. {:ok? true … :epoch-id <the new epoch>} on success; {:ok? false :reason …} decided BEFORE dispatch for an unknown / destroyed frame (:rf.error/no-such-handler, kind :frame), a drain in flight (:rf.epoch/replay-during-drain), an unknown / aged-out id (:rf.epoch/replay-unknown-epoch), a halted / synthetic / incomplete record (:rf.epoch/replay-non-replayable-record + :cause), or a recorded :rf/fn-override (:rf.epoch/replay-unreplayable-fx-override + :fx-ids) — refusals ride in the envelope, no trace is emitted; a declared fact absent from the token stays the canonical :rf.error/missing-required-cofx hard error. false when elided / artefact absent (per Tool-Pair §Replay). v1 (dev-only) tooling Tool-Pair
replace-frame-state! Fn (replace-frame-state! frame-id frame-state) → boolean — atomically install a PARTIAL frame-state map (any subset of {:rf.db/app … :rf.db/runtime …}); a present key replaces that partition, an absent key is preserved unchanged. The ONE frame-state write surface: an app-only map ({:rf.db/app v}) is the former replace-app-db!; {:rf.db/app {}} is the former reset-app-db!; a runtime-only map ({:rf.db/runtime v}) is the former replace-runtime-db!; a both-key map is the full-frame atomic install for tool-driven replay / fixture install. Records a synthetic epoch. Rejects a map with no recognized partition key, or an unrecognized key, as :rf.error/replace-frame-state-bad-keys (checked before frame resolution). Emits :rf.error/no-such-handler (kind :frame) / returns false for an unknown / destroyed frame. v1 (dev-only) tooling Tool-Pair
(rf/configure! {:epoch-history {:depth N}}) See §Configure keys. v1 (dev-only) — (configure key) Tool-Pair
epoch-silence-current? Fn (epoch-silence-current? tags)true when a received :rf.epoch.cb/silenced-on-frame-destroy signal still names a CURRENT fact: the carried :observed-gen is still the generation registered under :cb-id, AND that registration is not observing :frame right now. false otherwise — including an absent/nil :observed-gen, and when the artefact is absent. THE supported receiver decision: pass the signal's :tags map straight back. ONE atomic operation, not two composable reads (rf2-uhouu) — registration identity (a same-id replacement or unregister-drop makes a different generation current) and observation continuum (a same-id successor frame re-arms by DELIVERY, which mints no generation, so :observed-gen still matches while the callback is live again) are weighed under a SINGLE ledger snapshot. Composing those two facts from separate reads is not linearizable: a replacement landing between them accepts a silence for an already-superseded registration, an answer no single point in time held — which is why the low-level halves are not published (per Tool-Pair §Surface behaviour against destroyed frames + 009 §The delayed-silence emission linearization law). v1 (dev-only) tooling Tool-Pair
app-db-value / frame-state-value (cross-ref to §Public registrar query API) Fn Partition readers — app-db, and the coherent frame-state projection (the runtime-db-only read is (:rf.db/runtime (frame-state-value frame-id))). Each returns nil for an unknown / destroyed frame (per Tool-Pair §Surface behaviour against destroyed frames). v1 advanced/tooling 002

Epoch-settled listeners are the :epoch stream of the stream-parameterized listener verb(rf/register-listener! :epoch id callback-fn) / (rf/unregister-listener! :epoch id) — not a separate facade fn (the per-channel register-epoch-listener! / unregister-epoch-listener! pair was retired in API-shrink #4, rf2-9flalp; the epoch stream registers through the one verb exactly like :trace). The callback is a record-publication notification, not a once-per-event clock: an ordinary handled event publishes one record when it settles, and the SAME record re-publishes — carrying the same :epoch-id — when a post-settle render / sub-run / unmount back-fills into that already-settled epoch; synthetic records publish too (:rf.epoch/db-replaced per replace-frame-state! write and :halted-depth at the depth ceiling, both ring-retained when depth permits, plus the terminal :halted-destroy — an already-started event interrupted by frame destruction, delivered to listeners only and never retained). Because the listener is process-global while :epoch-id is unique only within one frame, reconcile on the pair [(:frame record) (:epoch-id record)] — cache under that key and replace on re-publication rather than counting callbacks; :outcome is record state (:ok / :halted-depth / :halted-destroy), not identity. A dequeued event rejected before it runs (no handler) publishes nothing. id may be any comparable value and re-registering the same id replaces. Process-global; a callback whose previously-observed frame is destroyed receives a one-shot :rf.epoch.cb/silenced-on-frame-destroy trace (per Tool-Pair §Surface behaviour against destroyed frames) carrying :observed-gen — the generation the silence is attributed to. A consumer decides whether a received signal is current with epoch-silence-current? (row above), the one supported receiver operation: it discards a signal owed to a replaced/dropped registration AND one superseded by a fresh delivery on the same registration, from a single ledger snapshot and without reading private registry state. Returns id, or nil when day8/re-frame2-epoch is absent.

Trace events emitted by epoch-history machinery:

:operation Tags
:rf.epoch/snapshotted :frame, :rf.epoch/id, :rf.trace/event-id, :outcome (the detailed cause enum :ok / :halted-depth / :halted-destroy; :halted-handler-exception is schema-reserved, not currently emitted)
:rf.epoch/outcome :frame, :rf.epoch/id, :rf.trace/event-id, :outcome (the consumer-facing :ok / :blocked / :error projection; fires paired with :rf.epoch/snapshotted per dequeued event)
:rf.epoch/restored :frame, :rf.epoch/id
:rf.epoch/db-replaced :frame, :rf.epoch/id
:rf.epoch/restore-unknown-epoch :frame, :rf.epoch/id, :history-size
:rf.epoch/restore-schema-mismatch :frame, :rf.epoch/id, :schema-digest-recorded, :schema-digest-current, :failing-paths
:rf.epoch/restore-missing-handler :frame, :rf.epoch/id, :missing
:rf.epoch/restore-version-mismatch :frame, :rf.epoch/id, :machine-id, :version-recorded, :version-current
:rf.epoch/restore-during-drain :frame, :rf.epoch/id
:rf.epoch/restore-non-ok-record :frame, :rf.epoch/id, :rf.epoch/outcome, :halt-reason
:rf.epoch/replace-during-drain :frame
:rf.epoch/replace-schema-mismatch :frame, :failing-paths
:rf.epoch/replace-history-disabled :frame (a replace-frame-state! precondition failure when the history ring is disabled)
:rf.error/replace-frame-state-bad-keys :frame, :reason (:no-recognized-keys / :unknown-keys), :keys (a replace-frame-state! precondition failure — no recognized partition key, or an unrecognized key; checked before frame resolution)
:rf.epoch.cb/silenced-on-frame-destroy :frame, :cb-id, :observed-gen (the reserved callback generation the silence is attributed to; a consumer calls epoch-silence-current? on the tags to discard a superseded signal, per 009 §The delayed-silence emission linearization law)
:rf.epoch.cb/listener-exception :frame, :cb-id, :rf.epoch/id, :message (an :epoch-stream listener callback threw)
:rf.warning/restore-quiesce-hook-exception :category, :hook, :frame, :exception (a restore-time async-quiesce hook threw)

Size-elision wire-boundary walker

Cross-reference: see Security.md §Privacy / secret handlingre-frame.elision/elide-wire-value is named there as the single normative emission site for the :rf/redacted sentinel. Every off-box egress (trace forwarders, MCP servers, error monitors) routes through rf/project-egress, which resolves a named boundary and delegates the per-slot walk to this walker; the trust-boundary surfaces catalogued in Security.md compose against that door.

The framework primitive that walks tree-shaped values at the wire boundary and substitutes elision markers for sensitive or large slots. Reached by every tool that emits wire data (the off-box error-monitor forwarders, the Xray-MCP / re-frame2-pair-mcp / story-mcp servers per Tool-Pair.md, the on-box dev panels) through rf/project-egress, never directly. The walker is the single normative emission site for the :rf/redacted sensitive sentinel and the :rf.size/large-elided marker; per-tool reimplementation is prohibited.

Internal mechanism, not a façade door (rf2-kuky.9 ruling A, executed by rf2-kuky.90). re-frame.elision/elide-wire-value carries no re-frame.core re-export and no manifest row. It reads no :rf.egress/profile, so a caller reaching it directly had to hand-assemble the :rf.egress/* floor that a named boundary already carries — two spellings for one boundary, and the weaker of the two silently. rf/project-egress is the single projection door on the façade. The walker stays public at its re-frame.elision home for the framework's own emit-time chokepoints (classification, event / error emit, flows, reply, route-sub egress, the Fresco tool read, and the projector's own slot walk) — every one of which passes {:frame f} and never a profile, because that internal walk is always the maximal floor.

The walker's opts contract — what project-egress resolves a profile into and passes down. (re-frame.elision/elide-wire-value v opts)v or an elision-marker substitution. opts is a closed map — {:frame <frame-id> :path [...] :query-v [...] :as-of-epoch <epoch> :rf.egress/include-sensitive? <bool> :rf.egress/include-large? <bool> :rf.egress/include-digests? <bool> :rf.egress/threshold-bytes <int>} and nothing else. Any other key raises :rf.error/bad-egress-opts naming the offending key(s); in particular a :rf.egress/profile is not accepted here — a profile names a boundary and is resolved by rf/project-egress, which passes the resolved :rf.egress/* opt-set down. Nor are the unqualified include-sensitive? / include-large? spellings: :rf.egress/* is the one egress vocabulary. Closing the map cannot widen egress — an unknown key was previously a silent no-op, which is the defect it removes. The :frame opt is read by key presence, not truthiness: an explicit :frame nil means no governing frame and fails closed; only an ABSENT :frame key falls through to the carried scope. Defaults: both include-* flags false (maximum elision); :rf.egress/threshold-bytes falls back to (rf/configure! {:elision ...}) then 16384. Walks v consulting [:rf.runtime/elision :declarations] and [:rf.runtime/elision :sensitive-declarations] of the named frame's runtime-db; substitutes :rf/redacted for sensitive slots and :rf.size/large-elided markers for large slots. Composition rule (normative): when both predicates match the sensitive drop wins — the size marker is suppressed because it would leak :path / :bytes / :digest. Per 009 §Size elision in traces and Spec-Schemas §:rf/elision-marker.

Commit-plane declaration path (EP-0025). The [:rf.runtime/elision] registry has exactly two slots: :declarations (:large? paths) and :sensitive-declarations (:sensitive? paths). Durable app-db classification rides the four commit-plane classification effects — a reg-event returns :large / :sensitive (or :clear-large / :clear-sensitive) alongside :db, installed by re-frame.elision/apply-classification-effects under :source :effect. A reg-app-schema {:large? true} / {:sensitive? true} slot prop is not a route into this registry: schemas describe shape and validation, not durable app-db egress policy (the schema's :sensitive? still drives schema-validation-failure-trace redaction — the machine [:schemas :data] and resource :params-schema per-slot props validate their owner's value and redact only the validator's own failure trace, and a resource :data-schema is a statically reflected shape fact with no runtime validation consumer). Durable machine and resource :data classification is likewise schema-independent: it rides the subsystem's own projection-relative :sensitive / :large declaration on the reg-machine / reg-resource spec, lowered per instance into the frame's elision registry, per 015 §Subsystem projection-relative classification.. The registry's declaration readers (re-frame.elision/declarations / sensitive-declarations) and the derived-tree value-match arms are reachable through the re-frame.elision home namespace; the user-facing façade exposes only project-egress. Per 015 §Durable app-db — the four commit-plane effects and implementation/core/src/re_frame/elision.cljc L1-16.

Record-level egress projection (EP-0015 / Spec 015)

Cross-reference: 015 §Projection is the normative home; project-egress is the public, record-level boundary primitive layered over the internal re-frame.elision/elide-wire-value walker. The six-member :rf.egress/* profile enum is the named-boundary vocabulary; the boolean :rf.egress/* flags remain the advanced override layer beneath it (EP-0015 §10/§11).

rf/project-egress is the required projection step before any off-box sink. It dispatches on a record's :kind — the three :rf.observe/* kinds plus :rf/epoch-record — to a private per-kind projector — only project-egress is public (EP-0015 issue 2: the name names the boundary, not a record kind) — and delegates every tree-shaped slot to the internal re-frame.elision/elide-wire-value walker. A profile resolves to a :rf.egress/* opt-set; an explicit :rf.egress/* boolean composes on top (the override wins).

API M/Fn Signature Status Tier Spec
project-egress Fn (project-egress record-or-value) / (project-egress record-or-value opts) → a value safe to ship under the resolved profile. opts conforms to :rf/project-egress-opts, which is closed: {:rf.egress/profile <closed six-member enum> :frame <frame-id> :path [...] :query-v [...] :rf.egress/include-sensitive? <bool> :rf.egress/include-large? <bool> :rf.egress/include-digests? <bool> :rf.egress/threshold-bytes <int> :as-of-epoch <epoch> :rf.egress/include-fx-args? <bool> :rf.egress/include-runtime-db? <bool> :rf.egress/include-event-args? <bool>} — the walker's closed key set, the one key this layer owns, and the three trusted-local axes that govern keyspaces only an :rf/epoch-record has (effect :args, the :rf.db/runtime frame-state partition, trigger / trace event args; all default false, and each is stripped before the walker, which knows nothing of them). Those three moved onto this door when rf/projected-record retired (rf2-bv1p); they were spelled bare until rf2-kuky.93 brought the whole opts vocabulary under :rf.egress/*, so all twelve keys of the closed map now read as one namespace. An unrecognised key raises :rf.error/bad-egress-opts naming it, on every record kind and on the kindless value path alike. Dispatches on a record's :kind (:rf.observe/handled-event / :rf.observe/error / :rf.observe/derived-tree / :rf/epoch-record) to a private per-kind projector, falling back to walking a kindless input as a tree-shaped value (the direct-read path); delegates tree-shaped slots to the internal re-frame.elision/elide-wire-value walker. The three :rf.observe/* projectors are private to re-frame.projection; :rf/epoch-record's is late-bound (:epoch/project-record) because core may not require the optional epoch artefact — a recognised :rf/epoch-record whose projector is absent raises :rf.error/epoch-artefact-missing naming the kind rather than bare-walking the record (which would start at :path [] and ship declared-sensitive :db-* slots raw). Frame ownership resolves in three steps, each by key presence, not truthiness: (1) an explicit :frame key in opts wins, nil included; (2) else a recognised record's own top-level :frame slot, nil included — every recognised kind is frame-bearing, carrying the frame whose classification governs its tree-shaped slots; (3) else the carried scope. A record is recognised by its :kind, never by a loose shape test, so a bare value that happens to carry a :frame key is a VALUE and seeds nothing. An explicit nil at step 1 or 2 therefore fails closed rather than borrowing the ambient frame. Profile resolves to a :rf.egress/* opt-set; explicit :rf.egress/* booleans compose on top (override wins). Unknown profile raises :rf.error/unknown-egress-profile (closed enum); an unrecognised opts key raises :rf.error/bad-egress-opts. Fail-closed when no frame is known from any of the three steps — an explicit nil included (no :rf/default synthesis). Per 015 §project-egress. v1 tooling 015

Frame-owned observability sinks (EP-0015 §9 / Spec 015)

Cross-reference: 015 §Frame-owned observability sink policy is the normative home. The normal production observability story (Datadog / Sentry / Honeycomb): an app declares a sink under a frame's :observability config ({:handled-events [{:sink <id> :rf.egress/profile …}] :errors [...]}) and registers the concrete sink fn against that <id> with rf/register-observability-sink!. A sink entry is a closed map — :sink plus the optional :rf.egress/profile, nothing else; any other key fails loud at make-frame with the key named, so no slot is accepted and then dropped. Vendor configuration is the registered fn's own (it closes over it); per-frame branching reads the record's :frame. The runtime routes one :rf.observe/handled-event record per processed event and one :rf.observe/error record per :rf.error/* site through rf/project-egress — under the owning frame's classification and the entry's egress profile (default :rf.egress/off-box-observability) — to the declared sink. Sinks consume already-projected records only (no sink-local redaction). Always-on (survives :advanced + goog.DEBUG=false). This is the only production observation door: the same entry grammar declared once per process with (rf/configure! {:observability …}) covers a cross-frame seat and every record whose frame does not resolve. The lower-level corpus-wide event-emit / error-emit registries survive as implementation tier only — they carry no public registration verb (rf2-kuky.69). Routing is fail-closed: the runtime never synthesizes :rf/default and never borrows another frame's policy, and a throwing sink is isolated from its siblings.

The same policy can be declared once per process with (rf/configure! {:observability …})§Configure keys — and precedence is per stream: a frame that declares a stream uses its own entries for it, a frame that omits the stream inherits the process default's, and {:errors []} on a frame is that frame's opt-out (declaration is read by key presence, not truthiness). Exactly ONE source is consulted per record per stream, so a sink id named by both is invoked once. Inheritance moves the sink list, never the redaction authority: an inheriting frame's records are still projected under its own classification. A record with no frame authority — one of the frameless three, or one whose :frame no longer resolves — reaches the process default alone, projected with the governing frame explicitly nil (tree slots :rf/redacted, summary ids intact, a stale :frame id kept as a diagnostic and never re-resolved). With neither a frame policy nor a process default, nothing routes. 015 §The process default is normative.

API M/Fn Signature Status Tier Spec
register-observability-sink! Fn (register-observability-sink! sink-id f) — register the concrete observability sink fn f under the keyword sink-id the frame's :observability {:sink <sink-id> …} entry names. f receives one already-projected :rf.observe/handled-event / :rf.observe/error record (projected under the owning frame's classification + the entry's egress profile); its return value is ignored. Re-registering the same id replaces. Returns sink-id. The framework ships no Datadog / Sentry client (EP-0015 Non-Goals); the sink fn is an app / integration-library concern. Always-on. Per 015 §Frame-owned observability sink policy. v1 tooling 015
unregister-observability-sink! Fn (unregister-observability-sink! sink-id) → nil v1 tooling 015

DOM source-coord annotations (mandatory)

Per Spec 006 §Source-coord annotation and Tool-Pair §Source-mapping, every adapter whose host has a DOM-attribute concept MUST inject data-rf2-source-coord="<ns>:<sym>:<line>:<col>" on the rendered root DOM element of each registered view. Format and exemptions (Fragments, non-DOM roots) are documented in Spec 006 §Source-coord annotation. Annotation is gated on interop/debug-enabled? (the CLJS mirror of goog.DEBUG); production :advanced builds elide the attribute via dead-code elimination — there is no DOM-bytes cost in shipped bundles. On the JVM the same annotation is stamped at the reg-view* registration boundary — a debug-gated wrapper on the stored :handler-fn (re-frame.views.jvm-source-coord-annotation), the server-side twin of the CLJS substrate wrappers — not by the SSR emitter, which carries no annotation logic and merely serialises the hiccup the registration boundary already annotated; see Spec 011 §Source-coord annotation under SSR.

Error contract

Errors are emitted as structured trace events with :op-type :error (or :warning / :info / :fx / :flow / :frame) and a per-category :operation keyword. The complete normative catalogue — every :rf.error/*, :rf.warning/*, :rf.fx/*, :rf.cofx/*, :rf.ssr/*, :rf.epoch/*, :rf.flow/*, :rf.http/*, :rf.http.interceptor/*, :rf.frame/*, and :rf.route.nav-token/* event the runtime emits — lives at 009 §Error event catalogue (single source of truth for category names, :op-type discriminator, trigger conditions, default :recovery, and :tags payload keys). Per-category Malli :tags schemas are canonicalised at Spec-Schemas §Per-category :tags schemas — one schema per catalogue row.

Recent additions consumers should be aware of: :rf.ssr/version-mismatch, :rf.ssr/schema-digest-mismatch, :rf.ssr/compatibility-check-skipped (the SSR hydration compatibility-check trio,), and :rf.cofx/skipped-on-platform (the platform-gating mirror of :rf.fx/skipped-on-platform). The catalogue at 009 is the single source of truth — do not duplicate the table here.

Per-Spec emit-sites: 002-Frames, 005-StateMachines, 006-ReactiveSubstrate, 010-Schemas, 011-SSR, 012-Routing, 013-Flows, 014-HTTPRequests, Tool-Pair. Each catalogue row's "Per [N]" cross-link names the owning Spec section.

Privacy (Spec 009 §Privacy / sensitive data in traces)

Cross-reference: see Security.md §Privacy / secret handling for the framework-wide pattern-level posture; the trust-boundary catalogue lives in Security.md. The cross-artefact inventory + composition order (every privacy surface in re-frame.core, re-frame.http, re-frame.schemas, re-frame.epoch, tools/mcp-base, with the data-flow from handler exit to off-box wire) lives in Privacy.md. The public classification model is the four commit-plane classification effects (:sensitive / :large / :clear-sensitive / :clear-large, returned by a reg-event alongside :db) for durable app-db (see 015 §Durable app-db — the four commit-plane effects) and registration-owned :sensitive payload classification on reg-event / reg-sub / reg-flow, projected at trust boundaries by project-egress and the :rf.egress/* profiles. The imperative add-marks / set-marks path-marks API, the re-frame.marks namespace, the frame :sensitive {:app-db …} annotation, and the positional redact-interceptor are all removed; the marks projection substrate lives in the marks-free re-frame.classification / re-frame.elision engine.

Per Spec 009 §Privacy the runtime stamps :sensitive? true at the top level of every trace event emitted inside the scope of a handler whose declared path overlap classifies sensitivity. (The legacy handler-meta :sensitive? annotation has been removed; sensitive-data marking is path-based per the data-classification mechanism in Spec 015.) Framework-published trace consumers (Sentry/Honeybadger forwarders, re-frame2-pair server, Xray, Story, story-mcp, re-frame2-pair-mcp) MUST default-drop the stamped events at their egress boundary.

API M/Fn Signature Status Tier Spec
sensitive? Fn (sensitive? trace-event)boolean. True iff trace-event is a map carrying a truthy :sensitive? stamp at the top level (not under :tags). The framework-published predicate every consumer composes against — Xray, Story and the pair-MCP server call it directly rather than reimplementing the same five-token check. Fail-closed: true is sensitive, false / nil / absent are not, and any OTHER truthy value (a string "true", a keyword :yes, a number) counts as SENSITIVE — the schema types the stamp as a boolean, so a non-boolean is a contract violation, and a (true? …) reading would forward the event in exactly the case where the producer has already proved unreliable. Matches the posture re-frame.mcp-base.sensitive/sensitive-stamp? applies on the MCP wire. v1 tooling 009

Spec-internal schemas

Per Spec-Schemas.md, the spec's own runtime shapes are described as Malli schemas registered at runtime. These are the conformance contract an implementation validates against.

Schema Describes Spec
:rf/dispatch-envelope Internal envelope wrapping every dispatch 002
:rf/dispatch-opts The user-facing opts map for dispatch / dispatch-sync / subscribe 002
:rf/registration-metadata Common metadata-map shape across reg-* 001 / 010
:rf/effect-map Return value of reg-event handlers — closed, seven keys: #{:db :rf.db/runtime :fx} plus the four EP-0025 commit-plane classification effects #{:sensitive :large :clear-sensitive :clear-large} (:rf.db/runtime reserved by convention for framework authority; app handlers use only :db / :fx day to day) 002
:rf/trace-event Universal trace event shape 009
:rf/error-event Refinement of :rf/trace-event for :op-type :error / :warning (unified error/warning envelope) 009
:rf/handler-body-dsl Conformance corpus handler-body DSL (host-agnostic event/sub bodies; small-DSL grammar) 008 / Spec-Schemas
:rf/transition-table State-machine transition table grammar 005
:rf/machine-snapshot Runtime snapshot of a machine instance 005
:rf/hydration-payload Wire format for SSR hydration 011
:rf/response HTTP-response accumulator owned by the request frame during SSR 011
:rf.server/cookie Structured-cookie shape for :rf.server/set-cookie / :rf.server/delete-cookie 011
:rf/head-model SSR head/meta data model (title, meta, link, json-ld, html/body attrs) 011
:rf/public-error Sanitised, client-safe projection of an internal error trace event 011
:rf.fx.server/set-status-args / :rf.fx.server/set-header-args / :rf.fx.server/append-header-args / :rf.fx.server/set-cookie-args / :rf.fx.server/delete-cookie-args / :rf.fx.server/redirect-args / :rf.fx.server/safe-redirect-args Args of standard :rf.server/* SSR fx 011
:rf/frame-meta Returned by (frame-meta frame-id) 002
:rf/variant Story-variant artefact contract (post-v1 lib) — variants are data, no fn-valued slots 007
:rf/epoch-record Per-frame epoch snapshot record (Tool-Pair) Tool-Pair
:rf.fx/dispatch-args Args of standard :dispatch fx (and :raise, same shape) 002 / 005
:rf.fx/dispatch-later-args Args of standard :dispatch-later fx 002
:rf.fx/http-args Args of :http fx (user-owned recommendation) Pattern-RemoteData
:rf.fx.nav/push-url-args Args of :rf.nav/push-url fx 012
:rf.fx.nav/replace-url-args Args of :rf.nav/replace-url fx 012
:rf.fx.nav/scroll-args Args of :rf.nav/scroll fx 012
:rf.fx.nav/capture-scroll-args Args of :rf.nav/capture-scroll fx 012
:rf.fx/with-nav-token-args Args of :rf.route/with-nav-token fx wrapper 012
:rf.fx/spawn-args Args of :rf.machine/spawn fx (the canonical actor-lifecycle fx-id; emitted from any event handler's :fx) 005
:rf.fx/managed-args Args of :rf.http/managed fx (request envelope, decode, accept, retry, timeout-ms, on-success/on-failure, request-id, abort-signal) 014
:rf.fx/managed-abort-args Args of :rf.http/managed-abort fx (request-id) 014
:rf.http/reply Canonical reply envelope {:status :ok :value v …} / {:status :error :error {:kind <:rf.http/*> …} …} / {:status :cancelled :error {:kind :rf.http/aborted …} …} appended to the reply target (:reply-to unified, or :on-success/:on-failure split sugar) 014
:rf/route-rank Structural-rank tuple for route-precedence sorting 012
:rf/pending-navigation Pending-navigation slot when :can-leave guard rejects 012
:rf/elision-registry Per-frame size-elision declaration registry in the reserved runtime-db child [:rf.runtime/elision] 009
:rf/elision-marker Wire shape re-frame.elision/elide-wire-value substitutes for an elided large value (:rf.size/large-elided) 009
:rf/project-egress-opts The opts map rf/project-egress accepts (:rf.egress/profile + advanced :rf.egress/* overrides) 015

Schemas are open by default (consumers tolerate unknown keys; producers grow shapes additively); :closed true is opt-in at boundary-validation sites and on the effect-map.


Testing

The testing surface lives across three namespaces. re-frame.core carries the production primitives that double as testing entry points (make-frame, with-frame, with-new-frame, dispatch-sync, with-fx-overrides, app-db-value, compute-sub); the static sub-graph query sub-topology is not among them — it is subscription tooling, reached through re-frame.subs.tooling (see §Public registrar query API). Pure machine simulation is the one exception: machine-transition is owned by re-frame.machines and is not re-exported from re-frame.core (per the front-porch boundary above). re-frame.test-support ships the test-only fixture machinery and test-flavoured helpers. re-frame.test-helpers ships the view-assertion helpers (hiccup-walk + testid authoring). re-frame.test-support does not re-export from re-frame.core — a test file requires both [re-frame.core :as rf] and [re-frame.test-support :as ts], and additionally [re-frame.test-helpers :as th] for view-assertion tests. See 008-Testing.md for fixtures, framework adapters, and re-frame-test compatibility.

API M/Fn Signature Status Tier Spec Notes
assert-path-equals Fn (assert-path-equals path expected-val) / (assert-path-equals path expected-val opts) v1 testing 008 Path-form sync assertion. Mismatch fires clojure.test/is-style failure via do-report. Lives in re-frame.test-support. Mirrors the :rf.assert/path-equals event used inside a Story :script block — same name root so the fn-side and event-side are navigable without a translation table. The wider sibling event family (:rf.assert/sub-equals, :rf.assert/state-is, :rf.assert/dispatched?, :rf.assert/no-warnings, :rf.assert/effect-emitted, :rf.assert/path-matches) lives in 007 §Play functions; runner and reporting channel differ. Choose by test surface: assert-path-equals from a deftest body, :rf.assert/path-equals from a story variant's :script vector.
poll-until Fn (poll-until pred) / (poll-until pred opts) v1 testing 008 Bounded-deadline poll. JVM: synchronous — returns the truthy value, throws ex-info carrying :rf.error/id :rf.error/poll-until-timeout (the canonical discriminator, per Spec 009) on timeout. CLJS: returns a js/Promise resolving with the truthy value or rejecting on timeout. Opts: :timeout-ms (default 2000), :interval-ms (default 5), :label. Lives in re-frame.test-support.
with-fx-overrides M (with-fx-overrides {fx-id -> override, …} body+) v1 testing 002, 008 Lexical-scope :fx-overrides binding. Every dispatch / dispatch-sync inside the body merges the supplied map into its envelope's :fx-overrides. Precedence: per-call opt > lexical with-fx-overrides > per-frame :fx-overrides. Composes with with-frame. Lives in re-frame.core.
compute-sub Fn (compute-sub query-v db) v1 testing 008 Pure sub computation against an app-db value. Lives in re-frame.core.
snapshot-registrar / restore-registrar! / make-reset-runtime-fixture Fn per docstring v1 testing 008 Fixture machinery. make-reset-runtime-fixture builds the :each fixture (registrar snapshot/restore + per-process reset); the raw snapshot-registrar / restore-registrar! pair composes hand-rolled fixtures. Lives in re-frame.test-support.
with-trace-recorder! M (with-trace-recorder! [recs-sym opts?] body+) v1 testing 008 Bracket body with a fresh trace-tooling listener that accumulates matching trace events into an atom bound to recs-sym; unregisters in a finally. Opts: :pred, :shape (:flat / :by-op), :key. Lives in re-frame.test-support.
with-emit-recorder! M (with-emit-recorder! [recs-sym opts?] body+) v1 testing 008 The always-on sibling of the row above. Brackets body with a fresh listener on one of the two IMPLEMENTATION-tier always-on registries — re-frame.error-emit (:stream :errors, the default) or re-frame.event-emit (:stream :events) — accumulating records into an atom bound to recs-sym; unregisters in a finally. Opts: :stream, :pred, :key. Those registries carry no public registration verb (rf2-kuky.69 retired the register-listener! :events / :errors streams); a test is one of the two consumers they survive for, and an application observes production records through an :observability sink instead. Lives in re-frame.test-support.

Testing — view-assertion helpers

re-frame.test-helpers ships the hiccup-walk view-assertion surface — call the view-fn directly, walk the returned hiccup, assert on content or invoke a handler. JVM-runnable; no JSDOM, no React, no act(). Pairs with render-to-string (the HTML-string view-test path per Spec 011): hiccup-walk for structure / handler assertions, render-to-string for HTML-markup assertions. Per 008-Testing §View-assertion helpers.

API M/Fn Signature Status Tier Spec Notes
expand-tree Fn (expand-tree tree) → tree v1 testing 008 Recursively expand fn-components and Form-3 class components inside a hiccup tree. After expansion every vector's first element is a keyword tag or a non-component value. Lives in re-frame.test-helpers.
attrs Fn (attrs node) → map? v1 testing 008 Return the attrs map of a hiccup node, or nil. Lives in re-frame.test-helpers.
children Fn (children node) → vector v1 testing 008 Return the child elements — everything after the tag (and optional attrs map). Lives in re-frame.test-helpers.
text-content Fn (text-content node) → string v1 testing 008 Recursively collect string leaves under node and join. Numbers coerce to strings; nils are skipped. Lives in re-frame.test-helpers.
extract-handler Fn (extract-handler node event-key) → fn? v1 testing 008 Return the value of event-key from node's attrs map, or nil. Lives in re-frame.test-helpers.
find-by-attr Fn (find-by-attr tree attr val) → node? v1 testing 008 First hiccup node whose attrs map carries attr == val, or nil. Generic over the attribute keyword (:data-testid, :data-test, :id, custom). Lives in re-frame.test-helpers.
find-all-by-attr Fn (find-all-by-attr tree attr val) → vector v1 testing 008 Every matching node, in depth-first order. Lives in re-frame.test-helpers.
find-by-attr-prefix Fn (find-by-attr-prefix tree attr prefix) → vector v1 testing 008 Every node whose attr value (a string) STARTS with prefix. Non-string attr values do not match. Lives in re-frame.test-helpers.
find-by-testid Fn (find-by-testid tree test-id) → node? v1 testing 008 Convenience over find-by-attr keyed on :data-testid. Lives in re-frame.test-helpers.
find-all-by-testid Fn (find-all-by-testid tree test-id) → vector v1 testing 008 Convenience over find-all-by-attr keyed on :data-testid. Lives in re-frame.test-helpers.
find-by-testid-prefix Fn (find-by-testid-prefix tree prefix) → vector v1 testing 008 Convenience over find-by-attr-prefix keyed on :data-testid. Lives in re-frame.test-helpers.
invoke-handler Fn (invoke-handler node event-key & args) → any v1 testing 008 Find the handler under event-key on node and call it with args. Returns the handler's return value. THROWS when node is not a hiccup vector, the node has no attrs map, or no handler is registered — the throwing failure mode is deliberate (a missing handler is almost always a test bug). Lives in re-frame.test-helpers.
testid Fn (testid id) / (testid id extra) → map v1 testing 008 Build an attrs map carrying :data-testid id. The 2-arity merges extra into the map; :data-testid always wins on collision. Authoring helper at the view call site. Lives in re-frame.test-helpers.

The single-frame view test is a composition, not a bespoke fixture: re-frame.test-support/make-reset-runtime-fixture (an :adapter + optional :init-fn) seats the ambient :rf/default frame and runs the install thunk; the walkers above call the root view fn directly and assert on the returned hiccup; re-frame.test-support/poll-until covers the async case. Per 008-Testing §Single-frame view test.


Standard interceptors

Under EP-0022 the public interceptor-authoring surface is reg-interceptor (§Registration), and event/frame :interceptors chains carry interceptor references (a bare keyword id or [id arg]), never inline values. The v2 framework-standard interceptor surface is exactly one interceptor — :rf.interceptor/path — referenced as [:rf.interceptor/path <path-vector>]. There is no public rf/path value constructor and no standard unwrap (EP-0022 §No standard unwrap — handler destructuring, or a project-registered interceptor for intentional chain-wide event reshaping, replaces it). The earlier line (keep specific-work helpers, drop trivial (->interceptor :before f) ones) narrows to path-only because path is coupled to app-db commit no-op semantics and justifies the :factory mechanism. Five v1 interceptors removed: debug, trim-v, on-changes, enrich, after (per MIGRATION §M-21).

inject-cofx / inject-cofx* — the v1 coeffect-injection interceptors — are removed (no alias). They are not on the public re-frame.core facade and carry no API-manifest row (a removed surface is not part of the canonical public API): there is no public inject-cofx var to call. Coeffect delivery is no longer a chain member: a handler declares :rf.cofx/requires on its registration metadata and the value-returning supplier's result arrives flat in the coeffects map (§Registration — reg-cofx, 001 §:rf.cofx/requires). The removal alarm survives internally: a stale call still raises the always-on hard error :rf.error/inject-cofx-removed naming :rf.cofx/requires.

Surface Shape Tier Purpose
[:rf.interceptor/path <path-vector>] interceptor reference (the one standard interceptor; the canonical :factory consumer) front-porch Focus a handler on an app-db sub-slice: :before stages the focused slice as :db, :after widens the returned slice back into full app-db. Preserves the frame-commit identical? no-op — an unchanged focused slice widens back to the original app-db object, not an assoc-in allocation (002 §Standard :rf.interceptor/path). A non-vector/malformed path arg is :rf.error/path-interceptor-bad-path.
reg-interceptor M (registrar) front-porch The public application-authoring form for any non-standard interceptor — analytics, logging, validation, ad-hoc context manipulation. The resulting interceptor is named, addressable, queryable, and referenced by id from chains. Rowed in §Registration.

The retired v1 public interceptor-authoring helpers and their replacements:

Removed / retired API Replaced by
path (public value constructor) the standard reference [:rf.interceptor/path <path-vector>] (EP-0022)
unwrap handler destructuring (the M-19 canonical map-payload form), or a project-registered interceptor for intentional chain-wide event reshaping (EP-0022 — no standard unwrap)
->interceptor (public authoring macro) reg-interceptor (the lowering constructor is re-frame.interceptor/->interceptor*, framework-internal)
debug Trace surface (009) + 10x / re-frame-pair
trim-v Canonical map-payload call shape (M-19)
on-changes Flows (Spec 013)
enrich Flows (derived state) / :schema (validation) / a project-registered interceptor (escape hatch)
after Registered fx (:fx [[:my-fx ...]]) for side-effects; a project-registered interceptor for context-shaped work

reg-flow / (clear :flow id) (Spec 013)

reg-flow is rowed canonically in §Registration. Per the canonical 3-slot grammar (rf2-bqstzr) it is (reg-flow flow-id metadata derive-fn): flow-id first, the pure :derive fn last, and metadata carrying :inputs / :output-path (both REQUIRED) plus optional :doc / :schema / EP-0025 classification keys and the :frame mounting key (which selects the owning frame — the metadata middle slot, like every other 3-slot reg-* surface). The inverse is the kind-keyed (rf/clear :flow id) / (rf/clear :flow id {:frame f}):flow is one of the two frame-scoped kinds, so it takes the trailing opts map for the frame override, and that map is EXACT (rf2-kuky.80). It is rowed canonically in §Clearing registrations; it deregisters the flow from the named frame and dissoc-ins its :output-path from that frame's app-db only (per Spec 013 §Frame-scoping).

Frame-destroy teardown. destroy-frame! releases every per-frame piece of flow state (the destroyed frame's slot in the per-frame flow registry, its last-inputs dirty-check rows, and its pending abandoned-output-paths) per Spec 013 §Frame-destroy teardown. Under single-store there is no registrar-slot prune (the old double-store "last owner" unregister / realign is gone — the :flow registrar kind is RESERVED-but-empty). Sibling frames' state is preserved.

Flow-eval failures in production. A throw inside a flow's :derive fn surfaces as :rf.error/flow-eval-exception on the always-on error-emit substrate — registered error-emit callbacks (the :errors stream of register-listener!) fire under CLJS :advanced + goog.DEBUG=false. The error is NOT trace-only. Per Spec 013 §Failure semantics rule 4 and 009 §Production builds.

Reserved fx-ids for runtime flow management via :fx:

Name Kind Signature Status
:rf.fx/reg-flow Reserved fx-id [:rf.fx/reg-flow [flow-id metadata derive-fn]] — register a flow at runtime via :fx (the same 3-slot triple reg-flow takes; the dispatching frame threads through as the :frame metadata key) v1 (optional capability)
:rf.fx/clear-flow Reserved fx-id [:rf.fx/clear-flow id] — clear a registered flow via :fx v1 (optional capability)

Interceptor / context plumbing

The interceptor context accessors get-coeffect / assoc-coeffect / get-effect / assoc-effect are removed from the public re-frame.core façade and carry no API-manifest row (a removed surface is not part of the canonical public API). Post-EP-0017/EP-0022 they lost their audience — the setters had zero callers, the getters one. The intended interceptor model is to author with reg-interceptor and let the :before / :after fns receive and return the context map directly: read coeffects with (get-in ctx [:coeffects k]) and write effects with (assoc-in ctx [:effects k] v). The underlying re-frame.interceptor/{get,assoc}-{coeffect,effect} fns remain in their owning namespace as framework-internal context helpers; they are not a public surface. See §Removed / not shipped.


Lifecycle / utility

API M/Fn Signature Status Tier Spec
init! Fn (init! adapter-map) — boot; idempotent for the seated adapter (same canonical :rf.adapter/* :kind, or the identical custom map), so a ^:dev/after-load re-call is a no-op. A different adapter raises :rf.error/adapter-already-installed and leaves the seated adapter untouched — destroy-adapter! first to swap. Required arg: the adapter spec map. Each adapter ns exports an adapter Var; consumers require the ns and pass the Var, e.g. (rf/init! rf.adapter.reagent/adapter). Calling (init!) with no args raises a language-level ArityException at compile/load time ( — the no-arg arity was cut so the missing-adapter mistake surfaces before runtime). Calling (init! nil) or (init! :reagent) raises :rf.error/no-adapter-specified at runtime. Per 006 §Adapter selection at boot. Installs adapters and runtime capabilities only — it creates no frame (EP-0002: no auto :rf/default); the app mounts its frame at the root with frame-root (ENSURE) — or constructs one programmatically with make-frame v1 front-porch 006
destroy-adapter! Fn (destroy-adapter!) — tear down the exact installed adapter generation. This is a one-way terminal boundary: calls the adapter spec's :dispose-adapter! fn (if present), attempts all owned cleanup, finally clears only the generation it claimed, and leaves adapter-disposed? true even when the primary cleanup failure is rethrown. Later cleanup failures remain secondary diagnostic evidence. A fresh adapter may install afterward; stale finalization never clears a replacement. Per Conventions §Tear-down verb axisdestroy- cluster (lifecycle boundary; symmetric with install-adapter! and with destroy-frame!). The adapter-spec map key :dispose-adapter! (an internal contract slot adapters implement) is unchanged. v1 advanced 006
current-adapter Fn (current-adapter) → the installed adapter SPEC MAP — the exact value passed to (rf/init! …) — or nil when no adapter is installed. Carries the adapter contract fns (:make-state-container, :replace-container!, :make-derived-value, …) plus a :kind discriminator. ONE read, map-shaped (rf2-kuky.4, 2026-09-06 — the former keyword-returning spelling and its current-adapter-spec twin are struck; the keyword form was literally the :kind of this same map). Branch code reads the KEY: (:kind (current-adapter)):rf.adapter/reagent / :rf.adapter/reagent-slim / :rf.adapter/uix / :rf.adapter/fresco / :rf.adapter/plain-atom / :rf.adapter/ssr, or nil for a custom map that picked no canonical kind — nothing is synthesised for it (plus the retired-but-reserved :rf.adapter/freehand / :rf.adapter/ui that nothing produces since 2026-08-16). A PRESENCE check inspects the MAP, never :kind. Per 006 §Adapter introspection. v1 advanced 006
configure! Fn (configure! {key opts, …}) — runtime config from a single nested map; non-map arg fails loud, missing top-level key leaves that subsystem untouched. An unknown bare or rf-namespaced top-level key applies nothing and emits :rf.warning/unknown-configure-key in dev builds (dev-gated, DCE'd in production, :recovery :ignored — observational, never a refusal); an unknown user-namespaced key (e.g. :myapp/thing) passes through silently. Key vocabulary in §Configure keys. One of three orthogonal configuration surfaces per Conventions §Configuration surfaces (configure! for process-level data knobs; set-! / install-! for adapter-pluggable hooks; per-frame metadata for frame-scoped overrides). v1 front-porch
current-config Fn (current-config) → the process-level config values currently in effect, in configure!'s own nested shape — the read twin of configure!, per Conventions §configure! vs current-config. PROCESS values only: no per-frame effective values, no transactional snapshot, and no promise of wire-serialisability. A subsystem key is ABSENT — not nil, not a fabricated default — when its own producer is unavailable, and the two optional keys are independent: :epoch-history is missing without the optional day8/re-frame2-epoch artefact, :trace-buffer is missing from a production bundle that DCEs the dev-only trace-tooling sibling, and neither absence implies the other; (get-in (current-config) [:epoch-history :depth]) reads nil when the epoch artefact is absent, not when trace tooling is. The user-namespaced pass-through carve-out is write-only: configure! accepts :myapp/thing in silence, and current-config does not reflect it back — the vocabulary is closed, so only the keys the runtime READS have live values to report. Key vocabulary in §Configure keys. v1 advanced

Feature inspection

re-frame2's optional capabilities ship as separate Maven artefacts (day8/re-frame2-<feature>) whose implementation namespaces core reaches through the late-bind hook registry at call time (per Conventions §Facade re-export, artefact require). The upside is bundle-isolation — an app that omits a feature does not carry its code. The downside this front-porch closes: the late-binding is otherwise invisible, so a developer who forgets to :require the impl artefact calls a re-exported fn that exists and is met with an opaque artefact-missing error. One surface — features — makes the optional-feature inventory self-explaining; the boolean is a lookup into the map it returns.

The known optional features are :schemas, :machines, :routing, :flows, :http, :ssr, :epoch, :resources (the closed per-feature split set per Conventions §Artefact tiers).

features ships to production. It is a runtime query, not dev-time instrumentation, so — unlike the trace / epoch surfaces — it is NOT gated on interop/debug-enabled? and does NOT elide under :advanced + goog.DEBUG=false. A production caller may legitimately read (get-in (rf/features) [:routing :loaded?]) before taking a routing-dependent path. The feature→coordinate mapping is static data in the always-loaded re-frame.core facade (a plain table of {:feature {:maven … :require … :spec …}} strings), never a live :require reaching into the optional impl namespaces — a live reach-in would create a hard facade→optionals reference that pulls every optional artefact into every production bundle, breaking bundle-isolation. Presence is detected by a pure keyword lookup in the always-loaded late-bind hooks atom against a representative key the impl publishes at ns-load — no reach into the optional namespace.

API M/Fn Signature Status Tier Spec
features Fn (features) → map of every optional feature keyword to its inspection entry: the static coordinate data (:maven / :require / :spec) merged with the live :loaded? boolean. E.g. {:epoch {:maven "day8/re-frame2-epoch" :require "re-frame.epoch" :spec "Tool-Pair (Time-travel / epoch)" :loaded? true} …}. Ships to production (NOT elided). v1 advanced

Reading the boolean, and the boot-time guard. features is the ONE door — data before magic. The per-feature boolean is a lookup into its map, and an unknown feature keyword has no entry, so the lookup reads nil where the removed feature-loaded? read false; the contracts are not identical and the lookup is what is taught.

(get-in (rf/features) [:epoch :loaded?])   ;=> true

;; Want boot-time failure rather than first-call failure? Write the guard.
;; One line, no framework verb, and NOT an `assert` (asserts are elidable):
(when-not (get-in (rf/features) [:epoch :loaded?])
  (throw (ex-info "re-frame.epoch is not on the classpath"
                  (get (rf/features) :epoch))))

That guard carries the same copy-pasteable coordinate data the removed require-feature! threw, because the inventory entry is the throw's data.

Artefact-missing errors carry the require. This front-porch is paired with a hard rule: every artefact-missing error in the framework — including the existing late-bind facade throws (:rf.error/<feature>-artefact-missing, raised via re-frame.late-bind/require-fn! from the re-frame.core-<feature> wrappers) — carries the exact copy-pasteable Maven coordinate and the namespace to require at app boot in its :reason slot. The named pattern is documented once at Conventions §Facade re-export, artefact require.


Configure keys

Runtime configuration is uniformly via (rf/configure! {<key> <opts>, …}) — a single nested map keyed by the top-level keys enumerated here. The argument MUST be a map (a non-map argument fails loudly); a missing top-level key leaves that subsystem untouched; a present key delegates to that subsystem's configurator in table order (:epoch-history, :trace-buffer, :elision, :observability) preserving its slot-merge semantics; an unknown top-level key applies nothing (closed-and-additive). Keys are plural-noun-shaped; opts are an open map of per-key settings.

An unknown top-level key applies nothing, but it is not necessarily silent. The vocabulary above is CLOSED and its keys are BARE, so an unrecognised bare key reads as a typo of a real key rather than as an extension point — the shape Conventions §No silent swallow says MUST signal. A bare (:epoch-histroy) or framework-namespaced (:rf.foo/bar) unknown key therefore emits :rf.warning/unknown-configure-key in dev builds, naming every offending key and the full known set. A user-namespaced key (:myapp/thing) passes in silence — that is the extension-key carve-out the same section reserves. The warning is dev-gated (rf.interop/debug-enabled?, so the whole surface DCEs under :advanced + goog.DEBUG=false) and observational (:recovery :ignored): the call still returns nil and still applies nothing.

(rf/configure! {:epoch-history {:depth 100}
                :trace-buffer  {:events-retained 25}
                :elision       {:rf.egress/threshold-bytes 8192}
                :observability {:errors [{:sink :my-app.sinks/sentry}]}})
Key Opts shape Default Status Spec
:epoch-history {:depth N :trace-events-keep N}:depth non-negative integer (0 disables the ring); :trace-events-keep non-negative integer caps raw :trace-events retention (per Security §Epoch privacy posture). There is no post-projection scrub hook: the in-process ring buffer and every :epoch-stream listener deliver the RAW record (post-EP-0010 causal replay material), and an off-box forwarder that wants a further scrub composes it — (-> r (rf/project-egress opts) scrub) — at the sink. Per Tool-Pair §Time-travel. {:depth 50, :trace-events-keep 50} v1 (dev-only) Tool-Pair
:trace-buffer {:events-retained N} — non-negative integer event-slot count (one slot per event / pipeline run); 0 disables retention (the surface stays live) {:events-retained 50} v1 (dev-only) 009
:elision {:rf.egress/threshold-bytes N} — non-negative integer; 0 disables runtime auto-detect (only declared / schema entries elide) {:rf.egress/threshold-bytes 16384} v1 009
:observability {:handled-events [<sink-entry>…] :errors [<sink-entry>…]} — the PROCESS-DEFAULT production observation sink policy, in the same closed grammar a frame's :observability takes (Spec-Schemas §:rf/frame-meta FrameObservability). Precedence is per stream: a frame declaring a stream uses its own entries for it, a frame omitting the stream inherits this default's, and {:errors []} on a frame is that frame's opt-out — so exactly one source is consulted per record per stream. Inheritance moves the sink list, not the redaction authority: an inheriting frame's records still project under its OWN classification. Records with no frame authority (the frameless producers, and any record whose :frame no longer resolves) reach this default ALONE, projected with the governing frame explicitly nil. Validated against that grammar at CALL time, fail-loud with :rf.error/bad-frame-classification and :where 'rf/configure! — one grammar, two doors. Unlike every other key here an explicit nil CLEARS it (a policy must be removable without knowing what installed it); a second call REPLACES rather than merges. Per 015 §The process default and §Frame-owned observability sinks. none declared v1 015

Reading the values back. (rf/current-config) returns the live values in this same nested shape — the read twin of configure! per Conventions §configure! vs current-config. Three properties are normative. (1) Process values only — a frame carrying its own :rf.trace/events-retained metadata is not reflected; this reads the same slots configure! writes and nothing else. (2) A subsystem's key is ABSENT when its own producer is unavailable, never nil and never a fabricated default. The two optional keys are independent — each is read through its own late-bind hook, so an absence never implies the other's: :epoch-history requires the optional day8/re-frame2-epoch artefact and :trace-buffer the dev-only trace-tooling sibling, so a production bundle that DCEs the latter omits :trace-buffer alone while a loaded epoch artefact still reports :epoch-history beside it. (get-in (rf/current-config) [:epoch-history :depth]) reads nil when the epoch artefact is absent, not when trace tooling is. :observability follows the same absent-not-nil rule for a different reason and the distinction is normative: re-frame.observability is always loaded, so its key is absent when no process default has been declared — a statement about CONFIGURATION, never about the build — and it reports the declared policy verbatim, never any frame's effective policy, which is per-stream and resolved per record. (3) The user-namespaced carve-out is write-only — a :myapp/thing key configure! accepted in silence is not reflected back, because the vocabulary is closed and only keys the runtime READS have live values to report. The result is a key-by-key snapshot rather than a transactional one, and it is not promised to be wire-serialisable.

Retired key. The earlier :sub-cache {:grace-period-ms N} knob is gone. Sub-cache disposal is now synchronous on derefer-count → 0 (per 006 §Reference counting and disposal); there is no deferred-grace timer to configure.

SSR error-projection policy (:public-error-id, :dev-error-detail?) is not a configure key — it is per-frame metadata on the frame's :ssr map (see Conventions §Configuration surfaces bucket 3 and 011 §Server error projection). Different frames in the same process can carry different projector / dev-detail settings, so the natural lifetime is per-frame, not process-global.

Opts-key naming rule

The opts map for any configure key mixes two shapes — it encodes which contract owns the sub-key:

  • Framework-owned semantic sub-keys use a namespaced keyword under a reserved :rf.<area>/* sub-namespace (per Conventions §Reserved namespaces). The namespace identifies the cross-spec policy area the sub-key participates in — the same key shape appears verbatim wherever that policy is consumed, not only inside configure. Example: :elision carries {:rf.egress/threshold-bytes N} because :rf.egress/threshold-bytes is the same per-call policy key consumed by rf/project-egress and the wire-elision walker beneath it — one key, one fact, three readers, so the process-level knob and the per-call opt are literally the same keyword (per Conventions §Reserved namespaces — :rf.egress/*). The namespaced form makes the cross-surface identity grep-visible and prevents collision with adjacent per-knob settings.
  • Ergonomic per-knob sub-keys are unqualified bare keywords (:depth, :trace-events-keep). These sub-keys are local to a single configure key's opts map — they do not appear elsewhere in the framework's vocabulary, so a framework-owned namespace would add noise without adding identity. The bare form is the default at this leaf position; reach for it whenever the knob is unique to one configure key.

The discriminator is whether the sub-key names a cross-surface policy slot or a one-off knob. A sub-key earns a :rf.<area>/* namespace when it names a contract that lives in more than one place (:rf.egress/threshold-bytes is read by :elision, by the wire-elision walker, and by the MCP wire walker). A sub-key stays bare when it is local to its parent configure key (:events-retained under :trace-buffer and :depth under :epoch-history each tune one ring's slot count and live nowhere else in the vocabulary — separate knobs, no shared contract, so each stays bare).

New configure keys MUST apply the same rule: if a sub-key participates in a cross-spec policy area, qualify it under the area's reserved namespace; otherwise leave it bare. The rule is closed — there is no third shape (no :configure/depth, no :rf.configure/* prefix). A sub-key that would want a third shape is evidence the proposed knob is doing two things and should be split.

Fixed-and-additive

The configure-keys vocabulary is fixed-and-additive (Spec-ulation): existing keys cannot be renamed or removed; new keys are added by extending the table. Because configure! takes a single map and ignores unknown top-level keys, user code that wraps configure can pass a composed config value straight through — unknown keys no-op rather than throw. That pass-through is what the user-namespaced carve-out above protects: a wrapper's own :myapp/* keys ride through in silence. A wrapper composing BARE keys it invented will additionally see the dev-only :rf.warning/unknown-configure-key — which is the point, since a bare key is a name this vocabulary is entitled to own.


Machines

Split between the v1 machine-as-event-handler foundation and the post-v1 re-frame.machines scaffolding library — see 005-StateMachines.md §Disposition. The machine is the event handler: reg-machine / defmachine registers a machine into the same event-handler slot an ordinary reg-event writes.

API M/Fn Signature Status Tier Spec
reg-machine M (reg-machine machine-id machine-spec) / (reg-machine machine-id opts machine-spec) — registers a machine as an event handler. Walks the literal spec form at expansion time and co-locates per-element source on each :guards / :actions entry + a reference-site :source-coords on each :states-tree map node. The optional opts metadata map is the canonical Spec 001 MIDDLE slot; it carries an event-vector :schema — the machine + event-vector-schema shape. v1 advanced 005
reg-machine* Fn (reg-machine* machine-id machine-spec) / (reg-machine* machine-id opts machine-spec) — plain-fn surface beneath the macro. No source-coord walking. The optional opts metadata map is the MIDDLE slot (uniform with the macro); it carries the event-vector :schema. v1 advanced 005
defmachine M (defmachine name [docstring] spec)def-shape that walks the literal spec at the definition site and co-locates per-element source onto the def'd value, for the def-then-register shape (defmachine m {…}) / (reg-machine :id m). Does not register. v1 advanced 005
make-machine-handler Fn (make-machine-handler spec) → event-handler fn. Pure factory (registers nothing); the handler reg-machine registers and the :rf.machine/spawn fx installs. For Level-2 tests, not a registration door. v1 advanced 005
machine-transition Fn (machine-transition definition snapshot event) → one plain map: {:status :ok :snapshot … :fx […]} on success, {:status :error :error {:kind …}} when a guard / action / :data fn threw or a depth limit tripped (per 005 §Level 1) v1 advanced 005
Enumerating registered machines has no per-kind accessor (rf2-kuky.31): it is (keys (into {} (filter (fn [[_ m]] (:rf/machine? m))) (rf/registrations {:source :store :kind :event}))) — the generic registrar query plus the :rf/machine? discriminator, rowed in §Public registrar query API. 005
:rf.machine/spawn (fx) Canonical actor-lifecycle fx (registered globally by re-frame.machines). Args per :rf.fx/spawn-args. v1 — (fx-id) 005
:rf.machine/destroy (fx) Canonical actor-destroy fx (registered globally by re-frame.machines). Args: an actor id. v1 — (fx-id) 005
:raise (fx) Reserved fx-id inside a machine action's :fx (machine-internal, routed pre-commit). Args: an event vector. v1 — (fx-id) 005
:final? / :output-key (state-node keys) :final? marks a leaf state as terminal — entering it auto-destroys the machine. :output-key (requires :final?) designates the child's :data slot reported back via the parent's :on-done. Capability axis :fsm/final-states. Per ; see 005 §Final states. v1 — (spec key) 005
:on-done (:spawn spec key) (fn [{:keys [data result]}] new-data) on the parent's :spawn map. Fires synchronously when the spawned child enters a :final? state; result is the child's :data slot named by the final state's :output-key (or nil). Returns the parent's new :data map. Per and. v1 — (spec key) 005
machine->xstate-json Fn (machine->xstate-json definition) → JSON post-v1 lib tooling 005
machine->mermaid Fn (machine->mermaid definition) → string post-v1 lib tooling 005

Canonical descriptions (factory purity, spec keys, snapshot location, registration-time validation, etc.) in 005-StateMachines.md and Spec-Schemas.

v1 transition-table grammar subset is enumerated in 005 §Capability matrix; shape in Spec-Schemas §:rf/transition-table.

Standard registered subs (machines)

Standard sub Returns Spec
[:rf/machine <machine-id>] The machine's snapshot {:state :data :tags} (or nil if not yet initialised) 005
[:rf.machine/has-tag? <machine-id> <tag>] true iff the machine's current snapshot's :tags set contains tag (false for an unknown / not-yet-initialised machine) 005

The canonical machine read is the registered [:rf/machine machine-id] subscription vector — see 005 §Subscribing to machines. It is read like any other subscription — @(rf/subscribe [:rf/machine machine-id]), @(rf/subscribe [:rf.machine/has-tag? machine-id tag]) — and named projections chain off it by declaring it under :inputs. There is no named-read-sugar fn: a runtime-db framework read is a subscription vector, one grammar.


Story / variant / workspace library (post-v1)

See 007-Stories.md.

All Story surfaces are tooling (a Storybook-shaped dev surface — registration, execution, introspection — not application logic). Since the retrospective facade sweep (rf2-i6kh) that statement is exact rather than a summary with an exception: all 121 re-frame.story manifest rows carry :tier :tooling, and the facade holds no implementation carve-out. 116 of the 121 are JVM-introspected; the other five are the #?(:cljs …) arm of a split-host facade (register-substrate! / registered-substrates / mount-shell! / unmount-shell! / active-shell), rowed under :cljs-only with :facade? true per row and held fully-rowed by the CLJS enumeration probe. Absorbs story F-8; see §Tiering of cross-tool surfaces.

API M/Fn Signature Status Tier Spec
reg-story M (reg-story id metadata) post-v1 lib tooling 007
reg-variant M (reg-variant id metadata) post-v1 lib tooling 007
reg-workspace M (reg-workspace id metadata) post-v1 lib tooling 007
reg-tag M (reg-tag id metadata) post-v1 lib tooling 007
reg-decorator M (reg-decorator id metadata) post-v1 lib tooling 007
reg-story-panel M (reg-story-panel id metadata) post-v1 lib tooling 007
run Fn (run target) / (run target opts) → promise/future of the unified run-result. target is a keyword (registered variant) OR a map (inline plan). The single execution verb. post-v1 lib tooling 007
is Fn (is target) / (is target opts) → runs target and reports each assertion to clojure.test / cljs.test. JVM blocks (bounded by :timeout-ms, default 30000) and returns the run-result; CLJS returns the run promise. post-v1 lib tooling 007
explain Fn (explain target) / (explain target opts) → the plan's :explain map (args-validation, sub-overrides, decorators, …) without running. post-v1 lib tooling 007
variants-with-tags Fn (variants-with-tags tag-set) → seq of variant ids post-v1 lib tooling 007
snapshot-identity Fn (snapshot-identity variant-id){:variant-id ... :content-hash "..."} post-v1 lib tooling 007
story-view Fn (story-view variant-id) → hiccup post-v1 lib tooling 007

The recommended execution surface is the three verbs run / is / explain (each accepts a registered-variant keyword OR an inline-plan map) — the vocabulary the Guide and the skills lead with, and what a Story author should reach for first. The lower-level variant lifecyclerun-variant / reset-variant / watch-variant / destroy-variant! / render-variant — is a supported tooling surface too, not implementation vocabulary: custom shells, test fixtures and one-shot screenshot pipelines call it directly, and the Story tutorials teach it. It is not rowed in the table above because this projection rows the slices that surface through re-frame.core or the per-feature artefacts; the complete Story surface is rowed in tools/story/spec/API.md and carried in the api-manifest. The unified run-result is the single execution-record boundary; read its verdict via result-status / result-passed? (:pass / :fail / :cannot-run / :error — there is no :passing? boolean). Canonical execution model: story spec 017 §Public execution API (the re-frame2-story library spec, the normative home for the verbs + run-result shape).


Removed / not shipped

These surfaces are removed or renamed — not part of the public projection and not a tier (the deprecated tier is reserved for surfaces still shipping while on the way out; pre-alpha carries none). This is a migration table: each row names what to use instead.

API What to do Reference
dispatch-with (master) Use (dispatch event {:fx-overrides {...}}) MIGRATION M-4
rf/install-adapter! / rf/adapter-disposed? / rf/current-adapter-spec DELETED from the re-frame.core facade (rf2-kuky.4, 2026-09-06 — rider A-i). Seat the adapter with rf/init!, which since rf2-kuky.1 throws :rf.error/adapter-already-installed on a DIFFERENT adapter, leaving install-adapter! no observable difference. Read the adapter with rf/current-adapter, which now answers the spec map; the discriminator is (:kind (rf/current-adapter)). The two owning-namespace surfaces SURVIVE on re-frame.substrate.adapterinstall-adapter! is the strict primitive (throws on ANY second call, carries the generation / rollback machinery) and adapter-disposed? is the breadcrumb that picks :rf.error/adapter-disposed over :rf.error/no-adapter-installed. Pre-alpha: no shim, no deprecation alias. 006
dispatch-sync-with (master) Use (dispatch-sync event {:fx-overrides {...}}) MIGRATION M-4
dispatch-to (proposed earlier) Use (dispatch event {:frame :todo}) 002
subscribe-to (proposed earlier) Use (subscribe query-v {:frame :todo}) 002
frame-dispatcher / bound-dispatcher / bound-subscriber (proposed earlier) Use (rf/capture-frame) (the keystone OPERATION BUNDLE — captures the frame at creation; safe during render and from async callbacks) 002
bound-fn (CLJS macro) Use (rf/capture-frame) — the keystone OPERATION BUNDLE captures the frame and carries dispatch / subscribe across the boundary. 002
frame-bound-fn (macro) / frame-bound-fn* (fn) DELETED from the facade (API-shrink #1, rf2-csbbwu) — capture-frame is the ONE public carry primitive. The frame-rebinding closure semantics (re-establish *current-frame* around an arbitrary already-held fn) survive internally as re-frame.frame/bind-fn for framework / test / tooling reach; not app-facing. 002
frame-value->id (fn) DELETED from the facade (API-shrink #1, rf2-csbbwu) — every public surface accepts a frame VALUE or its id: the routing ops (dispatch / subscribe / app-db-value / frame-provider / …) normalize a value to its id interchangeably, and destroy-frame! accepts either but reads the value's exact-incarnation lifecycle authority (rf2-moftbs — a stale value no-ops against a same-id successor; a keyword is address-directed). Either way there is no need to unwrap a value to its id. 002
dispatcher Use (:dispatch (rf/capture-frame)) or the dispatch injected in a reg-view body 002
subscriber Use (:subscribe (rf/capture-frame)) or the subscribe injected in a reg-view body 002
current-frame Renamed to current-frame-id (returns a frame-id keyword) 002
get-frame-db Renamed to app-db-value (returns the app-db VALUE, a plain map) 002
(dispatch frame event) / (dispatch-sync frame event) / (subscribe frame-id query-v) / (subscribe-once frame-id query-v) frame-first positional forms DELETED (API-shrink #1, rf2-csbbwu) — every sig is [payload] / [payload opts]; target an explicit frame via the trailing {:frame …} opt (a frame-id keyword or a live frame value). 002
enable-performance-api-tracing! (proposed earlier) Performance-API instrumentation is gated on the compile-time re-frame.performance/enabled? goog-define, not a runtime toggle (see 009 §Performance instrumentation) 009
add-trace-listener / remove-trace-listener (proposed earlier) Use register-listener! / unregister-listener! 009
register-trace-listener / unregister-trace-listener (no-bang, proposed earlier) Renamed to register-listener! / unregister-listener! (bang form matches the side-effecting nature of listener registration) 009
Bare [:my-view "args"] keyword-tagged hiccup Use the Var form [my-view "args"] (canonical) or [(rf/view :my-view) "args"] for late-binding by id Conventions
h macro (proposed earlier) Removed. Use the Var form [my-view "args"] or [(rf/view :my-view) "args"] Conventions
reg-global-interceptor Use the frame-level :interceptors config key (frame-level is the canonical "global within this frame"). For cross-frame observation use register-listener!. MIGRATION M-17
clear-global-interceptor No replacement needed — re-declare the frame (make-frame with an updated :interceptors vector; absent-key semantics clear it). MIGRATION M-17
reg-sub-raw Use reg-sub (app-db reads), Pattern-AsyncEffect (non-app-db sources), state machines (lifecycle), or the 006 adapter contract (bridging external reactivity). MIGRATION M-18
reg-event-db Use reg-event (no alias) — destructure :db from the coeffects map and wrap the return in {:db …}: (reg-event id (fn [{:keys [db]} ev] {:db BODY})). A stale call raises the always-on hard error :rf.error/reg-event-db-removed naming reg-event. The ^:no-doc facade throwing stub carries no API-manifest row. 001 §The retired event-registration names
reg-event-fx Use reg-event (no alias) — reg-event IS the identical shape under the bare name (coeffects in, effects out); just rename the call. A stale call raises :rf.error/reg-event-fx-removed naming reg-event. 001 §The retired event-registration names
reg-event-ctx A framework-internal primitive. Express application full-context work as a registered interceptor (reg-interceptor with :before/:after, referenced by id from a reg-event chain). A stale public call raises :rf.error/reg-event-ctx-removed. 001 §The retired event-registration names
reset-frame! (rf2-lxwpob) Use (destroy-frame! id) then (make-frame config), re-supplying the SAME config (which carries :id, and :images for an image-loaded frame) you already hold. A stale call raises :rf.error/reset-frame-removed naming the composition. The ^:no-doc facade throwing stub carries no API-manifest row. 002 §Resetting a frame — destroy + make-frame
reload-images! (rf2-lxwpob) Image hot-reload is folded into re-construction — re-call make-frame against the SAME :id with a new :images vector; it swaps the generation while preserving frame memory. To read the reload diff, call frame-generation before/after and diff with generation-diff. A stale call raises :rf.error/reload-images-removed. The ^:no-doc facade throwing stub carries no API-manifest row. 002 §Image resolution and composition
get-coeffect / get-effect Removed from the façade (no audience; carry no manifest row). Inside a reg-interceptor :before/:after fn read the context map directly: (get-in ctx [:coeffects k]) / (get-in ctx [:effects k]). The owning-namespace re-frame.interceptor/get-coeffect / get-effect fns remain framework-internal. 001, 002
assoc-coeffect / assoc-effect Removed from the façade (zero callers; carry no manifest row). Inside a reg-interceptor :before/:after fn write the context map directly: (assoc-in ctx [:coeffects k] v) / (assoc-in ctx [:effects k] v). The owning-namespace re-frame.interceptor/assoc-coeffect / assoc-effect fns remain framework-internal. 001, 002
re-frame.alpha/reg The shipped per-kind registrars: reg-event / reg-sub / reg-fx / reg-cofx / reg-flow. (The v1 event trio reg-event-db / reg-event-fx / reg-event-ctx is not a v2 target — those are removed/withdrawn throwing stubs and migration inputs only, see the rows above and EP-0018; reg-event is the single event-registration form.) MIGRATION M-23
re-frame.alpha/sub Vector-form (rf/subscribe [::id arg]). MIGRATION M-23
re-frame.alpha/reg-sub-lifecycle and built-in lifecycle policies (:safe, :no-cache, :reactive, :forever) Sub-cache uses a single algorithm — synchronous ref-counting (dispose on derefer-count → 0), per Spec 006 §Reference counting and disposal. For specific edge cases file a follow-up bead. MIGRATION M-23
feature-loaded? (rf2-kuky.4, 2026-09-08) Use the lookup: (get-in (rf/features) [:epoch :loaded?]). Note the contract difference — an UNKNOWN feature keyword reads nil there, where feature-loaded? read false. §Feature inspection
require-feature! (rf2-kuky.4, 2026-09-08) Write the guard: (when-not (get-in (rf/features) [:epoch :loaded?]) (throw (ex-info "…" (get (rf/features) :epoch)))). Not an assert — asserts are elidable. The inventory entry carries the same Maven coordinate + require ns the throw did. The :rf.error/feature-not-loaded and :rf.error/unknown-feature ids retire with it. §Feature inspection
init-platform (rf2-kuky.77, 2026-09-08) Platform is the HOST DEFAULT — :client on CLJS (including CLJS-on-Node), :server on the JVM — overridden PER FRAME: (rf/make-frame {:id :ssr/request :platform :server}). There is no process-wide marker and no setter; re-frame.interop/active-platform is a per-host constant the frame's :platform key wins over. A CLJS-on-Node SSR host that used to call (rf/init-platform :server) at boot tags its request frames instead, which is what every in-repo SSR host already did. 002, 011
:preset :ssr-server (rf2-kuky.77, 2026-09-08) Write the one key it expanded to: {:platform :server}. The closed preset set is now :default / :test / :story. 002

Cross-references