View registry
A registered view is a product-owned golden path: a stable id, the questions it answers, a typed input, and a builder that turns host data into a composition. It is the trusted way to get a Composition — host code writes it, and the input is schema-gated before that code runs. The untrusted way is an agent composing one, which arrives through runtime.parse instead.
import { defineView } from '@elastic/isomer-runtime';
import { z } from 'zod';
export const serviceHealthView = defineView({
id: 'observability.serviceHealth',
title: 'Production service health',
description: 'Summarizes service health, latency, and recent deploy risk.',
answers: [
'Are my services healthy right now?',
'Did the latest deploy affect latency?',
],
input: z.object({
environment: z.string().optional().describe('Environment, e.g. "prod".'),
range: z.string().optional().describe('Time range, e.g. "last 24h".'),
}),
build: async ({ context, input }) => buildComposition(await load(context, input)),
});
Views reach a runtime two equivalent ways — at construction, or afterwards:
createIsomerRuntime({ packs, views: [serviceHealthView] });
runtime.viewRegistry.register(serviceHealthView);
Registering the same id twice throws. Registering after construction is expected, not a workaround: a host may build views per request or load them from a store, and the authoring context reads the list live for exactly that reason.
defineView takes build({ context, input }) and defaults description to title. Declaring a Zod input lets TypeScript derive build's input type from the schema, so the two cannot disagree.
const { view, composition, validation } = await runtime.viewRegistry.request(
'observability.serviceHealth',
hostContext,
{ environment: 'prod', range: 'last 24h' }
);
The sequence is short and the order matters:
- Look up the id — an unknown one throws
IsomerErrorwithUNKNOWN_VIEW. - If the view declared an input schema, parse the raw input through it. This happens before the builder runs, so bad input fails as a typed error rather than as something opaque from inside composition-building code.
- Await the builder. A view may fetch;
contextis whatever the host passes — a session, a request, a services bundle — and this package never inspects it. - Validate the built composition and return it alongside the result.
Validation here is reported, not enforced. request hands back a ValidationResult and lets the caller decide; enforcement is the surfaces' job, and doing it in both places would mean doing it inconsistently.
await runtime.viewRegistry.request('checkout.snapshot', ctx, { range: 42 });
// throws RegisteredViewInputError
// .code → 'VIEW_INPUT_INVALID'
// .viewId → 'checkout.snapshot'
// .errors → [{ path, message }]
Catch it by name and code, or by import: RegisteredViewInputError is exported from the package entry.
list() and get(id) return summaries — id, title, description, answers, and an inputSchema projected from the Zod input to draft-2020-12 JSON Schema. The projection runs once, at register; every read hands back that summary. That projection is why a view declares its input once: the same schema gates the request and describes it to a model, so the two can never drift.
A view with no input schema is legal; its input passes through unvalidated and its summary carries no inputSchema.
Authoring context for the other path into a composition · Surfaces for what happens after