Browser runtime SDK for ERPAI custom HTML pages. The contract between agent-generated pages and the iframe they execute in.
Every "custom page" inside an ERPAI app is a standalone HTML document loaded in a sandboxed iframe. Those pages need a way to:
- Query the app's data (records, SQL views)
- Format numbers / dates / cells consistently with the app shell
- Render dropdowns, tables, charts, modals that look like the rest of the product
- Read theme colors and respect light/dark mode
- Open the parent app's record-edit dialog without reinventing it
- Gracefully handle 403s and missing data
This package is the runtime that exposes all of that on window.erpai inside the iframe. Pages are usually generated by an AI agent (Neo's build-page skill); the agent's prompt is built from the same JSDoc that documents this package, so the agent can only call functions that actually exist.
Before this package existed, the runtime lived in three places that drifted independently:
erpai-ui/public/runtime/(what users actually loaded)- Neo's bundled skill (what the agent generated against)
- The build-page skill docs (what the agent thought it was generating against)
When any one of those three forks moved, agent-generated pages crashed in production with erpai.X is not a function. Three deploys in one day, all the same shape.
Now there's one source of truth, with:
- Types —
dist/erpai-pages-runtime.d.tsships with the package. Consumers using TypeScript (Neo's bundler, future SDK users) get compile-time errors for missing methods instead of runtime crashes. - Generated docs —
dist/runtime.mdis regenerated from JSDoc on every build. Neo's agent prompt reads this file directly. The docs cannot lie. - API surface lock —
npm testasserts every public export is present and working. Adding/removing exports requires an explicit edit to the test, which is the point. - Semver — version pinned per release (
v2.2.0,v2.3.0, …). Consumers can lock to a known-good version. Breaking changes get a major bump and a migration path.
| Group | Examples |
|---|---|
| API layer | runSQL, getRecords, createRecord, updateRecord, getTable, … |
| Formatters | compactNumber("1.5M"), fmt$, fmtPct, fmtDate, formatCell |
| Cell decoders | selectName, joinByKey |
| UI helpers | createDropdown, renderRecordTable, renderStatCard, renderPagination, renderPermissionDenied, initTabs |
| Icons | icon('chevron-down'), hasIcon, listIcons (Tabler catalog, ~250 icons) |
| State / loading | cached, sectionLoading, withPrefetch, loadSections |
| Record modals | openRecord, openCreateForm (delegate to parent app shell) |
| Theme | getThemeColors() returns { blue, green, amber, red, surface, ... } |
| Navigation | erpaiUrl, navigateTo (top-frame nav from inside iframe) |
Full reference: dist/runtime.md (regenerated on every build).
This is a private package; consumers install via git URL pinned to a tag:
npm install// vite.config.ts — copy the bundle into public/runtime/ on build
import { copyFileSync } from 'node:fs';
import { resolve } from 'node:path';
export default defineConfig({
plugins: [{
name: 'copy-erpai-runtime',
buildStart() {
const pkg = require.resolve('@erpai/pages-runtime/package.json');
const dir = resolve(pkg, '../dist');
const out = resolve(__dirname, 'public/runtime');
copyFileSync(`${dir}/erpai-pages-runtime.js`, `${out}/erpai-pages-runtime.js`);
copyFileSync(`${dir}/erpai-pages-runtime.css`, `${out}/erpai-pages-runtime.css`);
},
}],
});The pages then load the runtime via <link> + <script> from /runtime/.
// scripts/sync-runtime.mjs (Neo)
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const pkg = require.resolve('@erpai/pages-runtime/package.json');
const dir = dirname(pkg) + '/dist';
const stage = './src/lib/skills/builtin/build-page/stage';
await mkdir(`${stage}/runtime`, { recursive: true });
for (const f of ['erpai-pages-runtime.js', 'erpai-pages-runtime.css']) {
await writeFile(`${stage}/runtime/${f}`, await readFile(`${dir}/${f}`));
}
// runtime.md becomes part of the agent prompt
await writeFile(`${stage}/runtime.md`, await readFile(`${dir}/runtime.md`));/// <reference types="@erpai/pages-runtime" />
const { rows } = await window.erpai.runSQL('SELECT count() FROM accounts');
const label = window.erpai.compactNumber(rows[0].count); // typed → stringThe .d.ts is loaded automatically when the package is installed.
npm install # no runtime deps; tests use Node's built-in vm + fs
npm run build # cp src/* → dist/, write d.ts, generate runtime.md
npm test # API surface + smoke tests for formatters
npm run docs # regenerate dist/runtime.md from JSDoc- Modify
src/erpai-pages-runtime.js(or.css). - Add/update JSDoc above any export — that's what
runtime.mdshows agents. - If you added/removed an export from
window.erpai = {…}, updateEXPECTED_APIintests/api-surface.test.mjsANDtypes/erpai-pages-runtime.d.tsin the same commit. npm run build && npm testmust both pass.- Bump the version in
package.json:- Patch (e.g.
2.2.0→2.2.1) for bug fixes that don't change the API. - Minor (e.g.
2.2.0→2.3.0) for additive changes (new exports). - Major (e.g.
2.2.0→3.0.0) for renames, removals, signature changes.
- Patch (e.g.
- Add a
CHANGELOG.mdentry. - Tag the release:
git tag v2.3.0 && git push --tags. - Bump the version in consumers (
erpai-ui,neo).
| Version | Highlights |
|---|---|
2.2.0 |
First standalone release. Adds compactNumber, branchId export, X-Branch-Id header, --blue theme token, dropdown CSS, dedup. |
See CHANGELOG.md for full history.
UNLICENSED — internal Deskera/ERPAI use only. Do not distribute.