Skip to content
Prism

Core concepts

Operations, meta, errors, and the single execute pipeline.

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
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.

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: 201 for POST, 200 for GET

Operations without ~x/agent are invisible to MCP and AI SDK tool listing — opt-in, not opt-out.

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 suggestion text 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.

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.