Tests Custom Reports

How Custom Reports Work

Each session's data is passed to your JavaScript function, which returns a plain object; your Nunjucks template renders that object to HTML. On servers the function runs in an isolated sandbox limited to 5 seconds and 128 MB.

Published 2026/10/15

Every custom report is produced by the same four-step pipeline, run separately for each session:

Session data  ──►  Function (optional)  ──►  output object  ──►  Template  ──►  HTML
(Report Input)       JavaScript              plain JSON          Nunjucks
  1. Report Input. TestInvite assembles one JSON object for the session: candidate, dates, task and step, the full scored result, the benchmark (norm) comparison, and the test's reporting settings. Every field is listed in the Report Input Reference.
  2. Function. Your JavaScript receives that object as its only argument and returns a plain object. This is where calculations belong: percentages, bands, rankings, text lookups, sorting and filtering. See Writing the Function.
  3. Template. The template is rendered with the function's return value as its variables — every key of the returned object becomes a variable. If the handler has no function, the template receives the Report Input itself. See Writing the Template.
  4. Display. The resulting HTML is shown inside an isolated container: the app's styles don't leak into your report and your styles don't leak into the app.

Where your code runs

The same handler runs in two places, and it must behave identically in both:

PlaceUsed forEnvironment
TestInvite serversPDF export, REST API, result postbacksAn isolated sandbox: no network, no require/import, no file system, no environment variables. Limits: 5 seconds and 128 MB per run. console.log is allowed but its output is discarded.
The viewer's browserReport view, handler editor previewRuns locally in the page.

Write the function as a pure calculation over its input: no network calls, no timers, no reliance on anything but data. A function that follows this rule produces the same report everywhere.

Why separate the function from the template

  • Testability — the handler editor shows the function's output as JSON (Output: Data), so you can check every number before any HTML is involved.
  • Readable templates — the template only prints and loops over names you chose (overall.percent, dimensions[].band.label) instead of digging through raw result paths.
  • Reuse — the function's output is also returned by the REST API, so integrations get your computed values, not just HTML.
Reports are rendered when they are viewed. Editing a handler changes the report of every past and future session at once — there is nothing to recalculate, and nothing to roll back except the handler itself.
Good to know