Escape

AI report generator, in JavaScript

You want a model to produce documents. Think of an invoice laid out the way a customer asked, or a summary of last month’s orders nobody had to design by hand. The obvious route is to have it write HTML and print that. The route here has it write a report definition instead: JSON that describes the document, which quario checks and renders. Nothing the model writes can execute, and your code keeps the data.

The approach in one line

The model writes a definition → plan() checks it → your code renders it with your data. A definition that doesn’t compile goes back to the model as a list of problems. Each sits at its own path, so the model can fix it.

Do it with quario

Install the engine and the target you render to:

shell
npm install quario @quario/pdf

Describe the tool. Its input is a report definition, and quario/schema.json says what one may hold, so hand that file over as the input schema. Tell the model the shape of the data it reports on, because it never sees the data itself:

tool.ts
import schema from 'quario/schema.json' with { type: 'json' }

export const draftInvoice = {
  name: 'draft_invoice',
  description:
    'Write a quario report definition for an invoice. It renders against ' +
    '{ invoice: { number, issued, lines: [{ item, qty, unitPrice }] } }.',
  input_schema: schema,
}

The field name belongs to your SDK: Anthropic calls it input_schema, and OpenAI calls it parameters. The schema is the same file.

The schema proves the shape of each value. It can’t prove that two values agree, that a name isn’t reserved, or that an expression compiles. So plan() still decides whether the document renders.

Handle the call. Compile the definition with plan(). While it has problems, return them as the tool result. Once it compiles, render it against the data your own code fetched:

handler.ts
import { writeFile } from 'node:fs/promises'
import { quario } from 'quario'
import { pdf } from '@quario/pdf'

const q = quario({ locale: 'en-IE', currency: 'EUR' })

export async function runDraftInvoice(definition: unknown) {
  const { report, problems } = q.plan(definition)
  if (!report) return { ok: false, problems } // the model reads these and retries

  const bytes = await report.render(pdf(), await getInvoice())
  await writeFile('invoice.pdf', bytes)
  return { ok: true }
}

A problem names where it sits and what’s wrong, which is exactly what a model needs to correct itself:

tool result
{
  "ok": false,
  "problems": [
    {
      "path": "detail.columns[1].style.format",
      "message": "detail.columns[1].style.format: expected number, currency, percent, or date"
    }
  ]
}

Your agent loop does the rest. It sends the tool result back, the model sends a corrected definition, and the handler runs again until ok is true.

What the model can’t do

A definition is untrusted input, the same as data, so it doesn’t matter who wrote it. Whatever the model sends:

  • It can’t execute anything. Expressions parse into closures. There’s no eval, no new Function, and an expression has no loops to spin in.
  • It can’t emit markup. Every target reads a definition’s own text as text, so a <script> in a header reaches the reader as characters.
  • It can’t reach past your configuration. It calls only functions you registered, and it can’t raise the query budgets you set.
  • It doesn’t supply the data. The handler renders against the record your code fetched. The definition only describes the document.

Where the boundary sits →

Where the numbers come from

The model writes the formula for each number, not the number. A total is an aggregate, sum:[email protected] * @.unitPrice, and a line amount is an expression, {{ round(@.qty * @.unitPrice, 2) }}. quario evaluates both against the record your code fetched, so the figures come from your data.

Compare that with a model writing the page itself:

model-written HTML
<tr><td>Total</td><td>$1,760.00</td></tr>
model-written definition
{ "aggregates": { "total": "sum:[email protected] * @.unitPrice" } }

The first total is text the model typed. It can look right and be wrong, and nothing checks it. The second is a formula, and quario computes the total, so the model can’t miscount a line or mistype the total.

What can still go wrong is the formula itself. A model can sum @.unitPrice where it meant @.qty * @.unitPrice, read the wrong field, or drop the wrong rows. It can also type a figure into a text cell. None of that hides in the output: each is a line in the definition, readable in a diff. plan() catches a definition that doesn’t fit the schema, not a formula that’s valid and wrong. So review what the model wrote the way you’d review any change. Keep the definitions it gets right, because a definition is JSON you can store, version, and diff.

The claim holds when the data comes from your systems, as it does in the handler above. If you let the model pass the data itself, quario computes faithfully over whatever it sent.

Prefer MCP?

If your agent runs in a client such as Claude Code or Claude Desktop, you can skip the handler. @quario/mcp exposes the same check and render as two tools, and the host settings stay in its launch configuration. quario over MCP →

FAQ

Can the model get the totals wrong?

It can’t get the arithmetic wrong, because it doesn’t do the arithmetic: quario does, from the data your code supplies. It can declare the wrong formula, or type a figure into a text cell. Both sit in the definition, where you or a reviewer can catch them.

Which models does this work with?

Any model that takes a tool with a JSON Schema input, in the ordinary mode. The strict structured-output modes of OpenAI and Anthropic don’t accept the report schema. OpenAI’s strict mode requires every key, and a report leaves most of them out. The strict mode of Anthropic allows 24 optional keys, and the report document has more than 290. plan() is the check either way, so a model without tools can return the definition as JSON in its reply instead.

Why not let the model write HTML?

HTML from a model is code you have to sandbox, and it’s different every run. A definition is data, and quario validates it before it renders. The same one renders to a page, a PDF, a workbook, or a Word document.

How big is it?

The engine is under 5 kB minified and compressed. Each render target adds a little of its own on top — around 1 kB for HTML, 5 kB for PDF. The PDF target also leans on a file writer. The JSON Schema is about 7.5 kB compressed, and only the code that describes the tool imports it.

Can I use it for free?

Evaluating it’s free and has no time limit — the full engine, no key to request — and evaluation output carries a watermark. Anything past evaluating it, including shipping it in an application or running it in your business, needs a license for each production deployment. Its users, agents, and renders are never counted, and replicas of one service count once. An agent is never licensed on its own: it’s part of the deployment that runs it. See pricing →