Authoring context
The authoring context is what a host hands an agent so it can answer a question with a composition: the composition contract, what it may compose from, and what it can request by id. It is the untrusted path's other half — the agent writes JSON, and runtime.parse decides whether that JSON is a composition this runtime can render.
const { schema, primitives, views } = runtime.getAuthoringContext();
| Field | What it is | Freshness |
|---|---|---|
schema |
Authoring JSON Schema for a Composition built from this runtime's primitives |
cached |
bodySchema |
The body of schema as a schema of its own, with the $defs it reaches, for an editor that validates body nodes |
cached |
declarations |
.d.ts source for the registered primitives; see Editor declarations |
cached |
primitives |
One catalog entry per primitive: purpose, useWhen, avoidWhen, example |
cached |
views |
Registered-view summaries, each with its input JSON Schema, read from this runtime's own viewRegistry — the views option to createIsomerRuntime or runtime.viewRegistry.register, never a separately constructed registry |
live |
groups |
Every pack's primitive groups, in pack order, for an index catalog | cached |
schemaFor(types) |
Authoring JSON Schema restricted to types, e.g. a registered view's narrower input |
uncached |
describePrimitives(types) |
The catalog entries of types, the $defs of schema they reach with the body-node union stubbed, and props: each type's properties as PropDescriptors |
uncached |
Both halves arrive from one call on purpose. An agent choosing between routing to a registered view and composing from primitives needs to see both options at once, and a host should not have to stitch that catalog together from two sources.
The caching split is equally deliberate. The schema and catalog cannot change for the runtime's lifetime, and projecting a discriminated union is expensive, so they are computed once via the SDK's buildAuthoringJsonSchema, on the first getAuthoringContext() call rather than in createIsomerRuntime: a runtime that only validates and renders never builds them. The view list is read on every call, because a host may register views after boot and an agent asking now should see what is registered now; each view's summary is projected once at registration, so the read is cheap. schemaFor is not cached: it exists for a handful of subsets, not one call per render.
A runtime with many primitives can hand an agent an index built from primitives and groups instead of the whole catalog and schema, then answer each lookup with describePrimitives. Its $defs keep the ids schema uses, so two lookups never disagree about a shared def. Both lookups throw UNKNOWN_PRIMITIVE_TYPE for a type this runtime does not register.
A host that documents props (a docs site, Storybook controls, a props table) reads describePrimitives(types).props instead of walking $defs, $ref, anyOf, and enum itself. props[type] lists one PropDescriptor per property in schema order: name, a display type (string, tone, primary | warning, string[]), a kind (string, number, boolean, enum, array, object, or other), values when kind is enum, required, and description. A property that refers to a named def displays the def's name and still reports the kind and values it resolves to; a discriminator such as type is a one-value enum; a union that is not all enum values is other. values holds string choices only: a nullable prop shows | null in its type but not in values, and a number or boolean choice is shown as written in type (1 | 2) with kind number or boolean.
Pass authoring on createIsomerRuntime to name pack-owned $defs (actionItem, badgeItem) or attach refine descriptions. The validator schema parse uses is unchanged. A pack can contribute its own describe too, via authoring on its PrimitivePackInput — see Packs — and the runtime merges every composed pack's contribution with this option, which wins on conflict.
declarations contains an ambient module with <Type>Node types, the BodyNode union, and each primitive's catalog guidance and prop descriptions as JSDoc. Load it as a Monaco extra lib, then import the names into the editor model:
const runtime = createIsomerRuntime({
packs: [pack],
authoring: { jsx: true, moduleName: 'isomer:editor' },
});
const { declarations, bodySchema } = runtime.getAuthoringContext();
monaco.languages.typescript.typescriptDefaults.addExtraLib(
declarations,
'isomer.d.ts'
);
For classic JSX, configure the editor with jsx: Preserve and jsxFactory: 'authorJsx'. The editor's TypeScript source can then use:
import { authorJsx, Composition, Note, type BodyNode } from 'isomer:editor';
const node: BodyNode = { type: 'note', text: 'Hello' };
const view = (
<Composition>
<Note text="Hello" />
</Composition>
);
The default module identity is @elastic/isomer-authoring. Use a distinct moduleName for each runtime loaded in one TypeScript service. Imports belong to the editor's type-checking environment; a host that executes author snippets must bind the imported components to its buildJsxShim result and authorJsx to React's createElement. The companion JSX runtime is a type declaration; execution uses the host's React JSX transform and runtime. A host can add those imports in an editor-only wrapper when its snippet syntax has no imports.
With jsx: true, the module exports the root Composition, one component per primitive with its branded children as members (SlideList.Item), and their props types, each member's props named after its primitive. It also exports components, keyed exactly like the shim. Non-identifier keys are available through that object: const { 'Code-block': CodeBlock } = components. Names such as Image and Record are local to the module and remain available with DOM or ES libraries. Generated type names are numbered only when names within the module collide.
The shim and declarations share component discovery, collision checks, child traversal, and slot inference. Authored fields can be supplied as props or filled by children. The JSX namespace is scoped to authorJsx, so it does not augment the host's global or React JSX types. For automatic JSX, use jsxImportSource: moduleName; the generated companion modules at <moduleName>/jsx-runtime and <moduleName>/jsx-dev-runtime supply the namespace for production and development mode. Children accept editor elements and structural React elements and reject ordinary objects. A custom toItem child uses its propsSchema when supplied; without it the declaration accepts loose props because the callback's TypeScript annotation is unavailable at runtime. See Custom child props.
Type declarations provide structural checking, not full schema validation. JSX calls check schema-constrained extra props. Plain node types widen an index signature with the named properties' types because TypeScript cannot express separate value constraints for known keys and every other string key. Refinements, formats, and value bounds remain schema checks. bodySchema validates an array of body nodes; a host that also accepts a single node must wrap it in an array before using that schema.
const parsed = runtime.parse(JSON.parse(modelOutput));
if (!parsed.valid) {
return retryWith(parsed.errors);
}
render(parsed.composition!);
parse answers one question — is this a Composition for this runtime? — and stops there. It runs the input budget and the schema but not the semantic passes, so a composition with two nodes sharing an id parses as valid. If you want those checks, run validate on the parsed composition:
import { warningsForSurface } from '@elastic/isomer-sdk';
const result = runtime.validate(parsed.composition!);
result.errors;
result.warnings;
- duplicate ids
- per-surface findings — narrow with warningsForSurface
Errors from either are path-prefixed and single-purpose, which is the shape a model can retry from.
The runtime supplies the structural half. Most hosts send more, because a vocabulary has copy the runtime does not own: a pack's authoring guide and rules, golden composition fixtures, and runtime.getCapabilities() so an agent stops proposing primitives the host would only degrade.
runtime.getCapabilities();
// { primitives: […],
// surfaces: ['react','html','text','markdown','slack','snapshot'],
// formats: ['react','html','text','markdown','slack','png','svg','pdf'],
// support: { kpi: { react: 'native', …, slack: 'native' }, sparkline: { react: 'native', …, slack: 'fallback' } },
// enhancements: { tableSort: true, clipboard: true } }
- request them with
enhancements: ['tableSort']
surfaces reports the surfaces that were actually built — a runtime with no frame truthfully omits snapshot. formats is what the host can ship: every surface but snapshot, then whatever the host registered with createIsomerRuntime({ formats }), here a takumi backend's png, svg, and pdf. An agent deciding what its output becomes reads formats; see Surfaces and formats.
support says how each of those surfaces renders each primitive. slack is native when the primitive has its own slack renderer, from its definition or a renderer override, and fallback when the dispatcher converts its markdown to Block Kit instead; a pack's surfaces declaration does not change it. Every other surface is native, since it renders from the react, text, or markdown renderer every primitive must have.
View registry for the trusted path · Surfaces for rendering what comes back