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. - Refuse input over the input budget, or not plain data, with
RegisteredViewInputError, whose one error has the codeINPUT_OVER_BUDGETorINPUT_NOT_PLAIN_DATA. What passes continues as the plain copy the check built, so the schema and the builder never read the original. - If the view declared an input schema, parse that copy through it; a view without one receives the copy itself. 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 the copy validation checked alongside the result, so
compositionis whatvalidationdescribes. Only input the budget refuses comes back as built.
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, code? }]
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