Prism · data mapping

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

A model was asked
In: the booking
{
  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" } }
}
Out
{ 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.

First steps →
A config

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.

An object
{
  "name": { "$ref": "$.member.name" },
  "paid": true
}

An object whose one key starts with $ is an operator. What it holds is what it works on.

An operator
{
  "$length": { "$ref": "$.seats" }
}

A list is the list that comes out, each element worked out. A plain value is itself.

A list
[
  { "$ref": "$.id" },
  "paid",
  2
]
Compiled

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.

The config
{
  name: { $ref: '$.member.name' },
  seats: { $join: { parts: { $ref: '$.seats' }, sep: ', ' } },
  admit: { $length: { $ref: '$.seats' } },
}
Run against the booking
{ name: 'Grace', seats: 'C4, C5', admit: 2 }
What compiling it gives
{
  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', ', '],
  },
}
In Nisc

Under the other packages.

Where Nisc derives a value, formats one or rewrites a document, that is a Prism config.

NovaWhat an action sends to an endpoint and what it keeps of the answer are configs, and a layout works a value out with one.
VexWhat Postgres answers is mapped into the shape you asked for. The mapping is compiled once and replayed.
StrataA format change ships as a config over one document, so what you stored still opens.
TideWhat an automation passes from one step to the next is worked out by a config.
FormattingMoney, dates and numbers are written for a named locale in a transform. Components do not format.
For a modelThe grammar is published as JSON Schema, and a profile teaches a model only the operators a task uses.
Limits

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.

LimitStops atCounts
maxSteps1,000,000nodes evaluated in one evaluation
maxStringLength1,000,000characters in any one string a node makes
maxValues1,000,000values in an accumulator, a binding, or the result