The transform is a document.
Prism turns one JSON value into another, and what says how is JSON too. A model writes it and a person can read it. There are no code strings in it and nothing in it is executed, so what a model wrote is safe to run, and because it is a document it can be stored, sent and replaced while the app runs.
pnpm add @niscorp/prism @niscorp/strata zod · 0.3.1 · Apache-2.0
A booking, a config a model wrote, and what comes out
{
id: 'B-2041',
member: { name: 'Grace', pass: 'season' },
show: {
title: 'The Tempest',
starts_at: '2026-10-24T19:30:00+02:00',
},
seats: ['C4', 'C5'],
seat_price_cents: 3800,
currency: 'EUR',
}The config the model wrote
{
"name": { "$ref": "$.member.name" },
"seats": {
"$join": {
"parts": { "$ref": "$.seats" },
"sep": ", "
}
},
"admit": { "$length": { "$ref": "$.seats" } }
}
{ name: 'Grace', seats: 'C4, C5', admit: 2 }Nobody writes the middle by hand. A model is asked for a mapping and answers with a config, and Prism holds it to its schema before any of it runs. The last three are what a model can get wrong: a key that starts with $ names an operator and nothing else, a path that is not there is an error, and an evaluation stops at its budget. No model is on this page: each config here was written ahead of time, and Prism runs it as you choose it.
Written in the shape of its result.
A config is JSON, and reads as what comes out of it. A plain value is itself. A key that starts with $ is an operator, and that is the only thing such a key can be.
A plain object is the object that comes out. Each of its values is worked out.
{
"name": { "$ref": "$.member.name" },
"paid": true
}An object whose one key starts with $ is an operator. What it holds is what it works on.
{
"$length": { "$ref": "$.seats" }
}A list is the list that comes out, each element worked out. A plain value is itself.
[
{ "$ref": "$.id" },
"paid",
2
]75 operators, a page each.
Each has a page with one example, and the example is evaluated in the page. The set only ever grows: a config is stored, so an operator is never removed or reshaped. These are the groups they come in; each leads to its first.
Checked once, run many times.
A config that is stored is compiled. What comes out is JSON too: the config with its sugar resolved, a fingerprint, and what the compiler counted. It can be stored, cached or sent, and it runs without being checked again. This one was compiled as this page opened.
{
name: { $ref: '$.member.name' },
seats: { $join: { parts: { $ref: '$.seats' }, sep: ', ' } },
admit: { $length: { $ref: '$.seats' } },
}{ name: 'Grace', seats: 'C4, C5', admit: 2 }{
fingerprint: 'cd5ed829621d35aa…',
stats: {
nodeCount: 11,
opCount: { $ref: 3, $join: 1, $length: 1 },
maxDepth: 4,
optimizations: {
refsInlined: 3,
handlersAttached: 5,
constantsFolded: 0,
},
},
tables: {
paths: ['$.member.name', '$.seats'],
strings: ['$.member.name', '$.seats', ', '],
},
}Under the other packages.
Where Nisc derives a value, formats one or rewrites a document, that is a Prism config.
Every evaluation has a budget.
A config cannot run forever or fill the memory. Three counts are kept while it runs, and past any of them the evaluation stops with E_BUDGET. A caller may lower them for one call.