Core concepts
Operations, meta, errors, and the single execute pipeline.
The operation shape
Section titled “The operation shape”An operation is a contract plus a handler. Build the contract with op(), then attach the handler with implement() (or context().op() when you want typed context).
import { agent, http, scopes } from "@prism/agent";import { implement, op } from "@prism/core";import { z } from "zod";
const closeDeal = implement( op() .input(z.object({ dealId: z.string(), outcome: z.enum(["won", "lost"]) })) .output(z.object({ id: z.string(), stage: z.string() })) .errors({ DEAL_ALREADY_CLOSED: { status: 409, title: "That deal is already closed.", suggestion: "Search for open deals before closing one.", }, }) .meta( http({ method: "POST", path: "/deals/{dealId}/close" }), scopes("deal:close"), agent({ description: "Close a deal as won or lost. Requires confirmation.", confirm: true, }), ),).handler(async ({ input, errors }) => { if (input.dealId === "already-closed") { return errors.DEAL_ALREADY_CLOSED(); } return { id: input.dealId, stage: input.outcome };});| Step | Purpose |
|---|---|
.input() / .output() |
Standard Schema (zod) for wire validation |
.errors() |
Declared failure codes with HTTP status + agent-facing copy |
.meta() |
Reserved keys that projections read — never business logic |
.stream() |
Handler returns an AsyncIterable; execute() still resolves to the last value |
.handler() |
({ input, context, errors }) => output |
Streaming
Section titled “Streaming”import { implement, op } from "@prism/core";import { z } from "zod";
const ticks = implement( op() .input(z.object({ n: z.number().int().positive() })) .output(z.object({ i: z.number() })) .stream(),).handler(async function* ({ input }) { for (let i = 0; i < input.n; i++) { yield { i }; }});
void ticks;AI SDK tools surface each yield as a preliminary result; REST, MCP, and CLI collapse to the final value.
Reserved meta keys
Section titled “Reserved meta keys”| Key | Factory | Who reads it |
|---|---|---|
~x/http |
http({ method, path, status }) |
@prism/hono |
~x/agent |
agent({ description, confirm?, suggestions? }) |
MCP, AI SDK, CLI listing |
~x/scopes |
scopes("deal:close") |
Visibility + enforcement |
HTTP defaults (applied at projection time, not stored):
- method:
POST - path: dotted router path (
deals.close→/deals/close) - status:
201for POST,200for GET
Operations without ~x/agent are invisible to MCP and AI SDK tool listing — opt-in, not opt-out.
Two-sided errors
Section titled “Two-sided errors”Declared errors are part of the contract. The handler exits with constructors on errors:
import { implement, op } from "@prism/core";import { z } from "zod";
const getItem = implement( op() .input(z.object({ id: z.string() })) .output(z.object({ id: z.string() })) .errors({ GONE: { status: 410, title: "Gone.", suggestion: "Do not retry." }, }),).handler(async ({ input, errors }) => { if (input.id === "x") { // Must return — a bare final errors.GONE() does not typecheck // (TypeScript#62488: never-through-parameter is a design limitation). return errors.GONE(); } return { id: input.id };});- Human surfaces get Problem Details (
type,title,status, …). - Agent surfaces get the same codes with
suggestiontext the model can act on. - Undeclared throws become opaque 500s — never leak internals to the model.
errors.NOT_FOUND is always available. Prefer return errors.CODE() over a bare call so TypeScript sees the never return.
One execute() pipeline
Section titled “One execute() pipeline”Every projection calls execute(impl, input, context). Projections hold no business logic — they map transport ↔ input/output, attach principal/context, and format results.
import { execute, implement, op } from "@prism/core";import { z } from "zod";
const echo = implement( op() .input(z.object({ n: z.number() })) .output(z.object({ n: z.number() })),).handler(async ({ input }) => ({ n: input.n }));
const result = await execute(echo, { n: 1 }, {});if (result.ok) { console.log(result.value); // { n: 1 }} else { console.error(result.error.code);}That is why four surfaces stay in sync: there is only one handler.