Build on OpenEik. A building is a folder.
Read a building model, check it, keep your app's results beside it and store its versions, with one library that has no dependencies. Fifteen minutes from nothing to a checked model.
In testing The specification, OpenEik 0.4, its schemas and the @openeik/tools library are in testing with early users, and open to the public at a release whose date is not set yet. Everything on this page runs today, with the library installed from openeik.org; npm and the links to the repository come with the public release.
One small JSON record per file, in folders that mean something.
An OpenEik model is a folder of plain files: it works on a disk, in git and in any store. What a compiler can derive (bodies, joins, areas, drawings) is never stored.
one-storey-house/ eik.json the format and the units AGENTS.md the conventions, for agents project/ project.json levels.json order.json types/architecture/wall/ type-wall-exterior-300.json architecture/level-0/ wall/wall-south.json door/door-entrance.json window/window-north.json architecture/level-1/ roof/roof-main.json drawings/views/plan/view-plan-0/view.json
architecture/level-0/wall/wall-south.json{
"category": "wall",
"height": 2700,
"id": "wall-south",
"levelId": "level-0",
"name": "South wall",
"offset": 0,
"profile": [
[10000, 0],
[0, 0]
],
"revision": 1,
"top": {
"attach": {
"face": "underside",
"hostId": "roof-main"
},
"constraint": "unconnected"
},
"typeId": "type-wall-exterior-300",
"width": 300
}
Millimetres, Z up. A wall's level plus its offset is its base, and its two profile points are its centreline. Its layers come from its type, exterior first, on the left of the line's direction. The specification, Parts I to III, fixes the rest.
Read it, check it, hash it: three calls.
@openeik/tools reads, checks, hashes, diffs and stores OpenEik models. It is written from the specification alone and has no dependencies. It needs Node.js 20.16 or later, or any modern browser. Its README is the API, with its functions and every diagnostic code. It reads and writes models of 0.2 and of 0.1, and migrates a model of 0.1 to 0.2.
Install
From openeik.org until the public release. From then on it is on npm, as
npm install @openeik/tools.shellnpm install https://openeik.org/tools/openeik-tools-0.4.tgzGet a building, and check it
Download a sample, unzip it, and check it with the
openeikcommand that comes with the library. In the folder you installed it in,npx @openeik/toolsruns that command and fetches nothing. Exit 0: the folder is valid.shellcurl -O https://openeik.org/samples/one-storey-house.zip unzip one-storey-house.zip npx @openeik/tools check one-storey-house one-storey-house: 34 files (1 agents, 1 eik, 10 element, 1 levels, 1 order, 1 project, 10 record, 6 type, 3 view); 0 errors, 0 warnings; 0 generic elements
Read a folder
A folder is checked whole as it is read, and refused, with the file and the rule, if it fails.
read.mjsimport { readFolder } from "@openeik/tools"; const house = readFolder("one-storey-house"); house.model.elements.length; // 10 house.get("elements", "wall-south").name; // "South wall" house.pathOf("elements", "wall-south"); // "architecture/level-0/wall/wall-south.json"
Check it
Every diagnostic has a code and names the rule of the specification it comes from. The codes are the library's own, and a command refused for a rule the validator checks carries the same code (Part XVII, 10.5).
check.mjsimport { checkFolder } from "@openeik/tools"; const { ok, errors, warnings } = checkFolder("one-storey-house"); // ok: true, 0 errors, 0 warnings // each diagnostic: {severity, code, path, pointer?, message, rule}
Hash it
The root of its version tree. The same folder gives the same root on any machine, in any store.
hash.mjsimport { versionTree } from "@openeik/tools"; versionTree("one-storey-house").root; // "d59785ace3086ce8bb0cb609e68e2ddf64c7edd665d42bc09a25b70abad5491f"
In a browser
Import the same library from openeik.org, as the validator does, and pass the files instead of a path: a Map of relative paths to texts or bytes, from a zip or a file picker. Nothing leaves the page.
browserimport { readFolder } from "https://openeik.org/tools/0.4/index.js"; const files = new Map([ ["eik.json", "…"], ["project/project.json", "…"], /* … */ ]); const house = readFolder(files);
When a folder is refused
readFolder throws an OpenEikError with code: "folder-refused" and the diagnostics. {lenient: true} reads it anyway, with the diagnostics beside the model.
a wall that names a type the model lacksThe folder does not read as OpenEik: 1 error,
first architecture/level-0/wall/wall-south.json:
typeId names type "type-wall-thick", which is
not in the model [ref-missing, I.7]
Work on a building as on a code base: change a file, check, diff.
The openeik command comes with the library: npx @openeik/tools runs it, as in the quick start. An agent maps the folder, reads only the records it will change, and checks after every change.
Read the conventions, then map
Every folder has an
AGENTS.mdwith what cannot be read off the files.openeik treeshows the layout with a hash and a count per folder.openeik tree house architecture --leaves30a3b0429e69… architecture/ 10 leaves 272c17993b79 level-0/ 9 a7434dea68fd door/ 1 026e320443e9 door-entrance 69b374bd16b0 slab/ 1 21bc55f49410 slab-ground 3dbdc20cf0a4 wall/ 5 10bd3e595a8e wall-east e70e185ebf63 wall-north …
Change a record
Keep every member you do not know, add 1 to its
revision, keep the stored form (sorted keys, two spaces, one member a line). Never write derived data.architecture/level-0/wall/wall-south.json- "height": 2700, + "height": 3000, - "revision": 1, + "revision": 2, - "typeId": "type-wall-exterior-300", + "typeId": "type-wall-thick",
Check
Done means no errors. Exit 0 is valid, 1 is errors found.
--jsongives the same as data.openeik check househouse: 34 files (1 agents, 1 eik, 10 element, 1 levels, 1 order, 1 project, 10 record, 6 type, 3 view); 1 error, 0 warnings; 0 generic elements error architecture/level-0/wall/wall-south.json: typeId names type "type-wall-thick", which is not in the model [ref-missing, I.7]
Fix, check again, and say what changed
With the type set back, the check passes.
openeik diffreports by record and field.openeik diff before housechanged elements wall-south (architecture/level-0/wall/wall-south) fields: height 1 record change, 0 kept files
A store's token is never an argument: it is read from OPENEIK_TOKEN. The reference implementation also serves models to agents over the Model Context Protocol; the specification's Part VII fixes what any agent host must offer, whatever the protocol.
Change it with commands, merge two lines, compile what it means.
Beyond reading and checking, the library changes a model the way the specification says a host must, merges two lines of work by record and field, writes a model back as a folder, and derives the bodies and quantities of the class-A rules.
Commands
Typed changes, checked in three stages and applied all or nothing (Part V). A stale revision is refused, so two writers never overwrite each other unseen.
commands.mjsimport { readFolder, applyCommands } from "@openeik/tools"; const house = readFolder("one-storey-house"); const answer = applyCommands(house.model, [{ kind: "update", id: "wall-west", expectedRevision: 1, patch: { name: "West wall (render)" }, }]); // ok: true, affected: ["wall-west"], // verify: {filter: {elementIds: ["wall-west"]}, …} // with expectedRevision: 7 instead: // "Command 0 (update wall-west): Revision conflict: // expected 7, current 1. Read and retry."
Merge
Three-way, against the version both lines started from (Part VI, 5): records matched by id, members merged one by one, the result checked whole.
merge.mjsimport { mergeModels } from "@openeik/tools"; const result = mergeModels(base, ours, theirs); // ours renamed wall-south, theirs raised wall-east: // ok: true, merged: wall-south from ours, // wall-east from theirs // both renamed wall-south: // conflicts: [{key: "elements/wall-south", // kind: "both changed", path: "/name"}]
Compile
Bodies, quantities and members' lines from the class-A rules of Part IV. Nothing derived is written back.
compile.mjsimport { compileModel } from "@openeik/tools"; const { quantities } = compileModel(house.model); quantities["wall-south"].grossArea.value; // 27.0359… m², under the roof's slope quantities["roof-main"].slopedArea.value; // 95.4706… m² quantities["roof-main"].ridge.value; // 10.6 m
Write it back
writeFolder writes a model as its folder: only the files whose bytes change, the files of records that went removed, and nothing else touched, so git sees exactly what changed. Read a folder and write it back unchanged, and every file comes out byte for byte as it was.
after the rename abovewriteFolder("one-storey-house", answer.model); // {written: ["architecture/level-0/wall/wall-west.json"], // removed: [], unchanged: 32}
From the command line
The same, for people, scripts and agents. --out writes a folder, or one file when the path ends in .json.
shellopeneik apply house batch.json --out house
openeik merge base ours theirs --out merged
openeik compile house --quantities
openeik write model.json house
openeik read house --out model.json
Keep your results beside the building, tied to what they were computed from.
An app writes into a top-level folder of its own (analysis/<model>/, schedule/ …) and never into the building's records. A reader keeps the files it does not know byte for byte, through export, save, restore and merge. Each result set carries a manifest that says exactly what it was computed from, so anyone can tell later whether it is still current.
results.mjsimport { readFolder, recordInputs, inputsDigest, intentInputs, runId, resultManifest, resultStatus } from "@openeik/tools"; const house = readFolder("one-storey-house"); const select = { categories: ["wall", "slab"], lists: ["levels", "types"] }; const inputs = inputsDigest(recordInputs(house, select)); // {count: 13, digest: "a35239ab…"} const model = inputsDigest( intentInputs(house, "analysis/daylight")).digest; const engine = { kernel: "my-app 0.1.0" }; const manifest = resultManifest({ app: { name: "my-app", version: "0.1.0" }, kind: "daylight", version: null, // a folder with no version control inputs, model, engine, made: { at: new Date().toISOString(), by: "jane@example.com" }, }); runId({ inputs, model, engine }); // "r-4910fce015dc" // Later: are the results still current? resultStatus(manifest, { inputs: inputsDigest( recordInputs(readFolder("one-storey-house"), select)), model }); // {current: true, inputs: true, model: true}
What a result says about itself
inputs- One hash over every record the app read, and their count. The same on disk, in git and in any store.
model- The same over the app's own intent files, leaving out its results and proposals.
version- The version that was read: a commit in a store, a git commit, or
nullfor a plain folder. upstream- On an alternative line, the main line's version it started from.
The result manifest (openeik-result/0.1) and the proposal file are part of OpenEik since 0.1 (Part VI, 8). The Apps page shows the whole pattern: results in the tree, stale results, alternative lines and proposals.
Store versions anywhere that speaks the API, or nowhere at all.
A version is the same folder stored as content-addressed objects: each record is named by the hash of its canonical JSON, each folder is a tree, each version a commit. A store is optional: git or a plain disk works too.
versions.mjsimport { connectStore, pushVersion, readVersion } from "@openeik/tools"; const store = connectStore({ url: "https://api.example.org", repo: "acme/house", token: process.env.OPENEIK_TOKEN, }); const { version } = await pushVersion(store, "one-storey-house", { author: "Jane Doe", name: "Ground floor" }); const back = await readVersion(store, version); // or "main"
Only what the store lacks is uploaded. A ref moves by compare-and-swap, so two people saving at once never overwrite each other: the second is told the ref moved, with where it is now.
An app that writes into a building it does not own writes its own line, an alternative (ref: "alternatives/my-app-…"), and proposes changes for review, never straight onto main.
The store protocol is the one EikHub speaks (API v0). EikHub is one commercial service that stores OpenEik models; nothing in the standard requires it.
What to read, for what you are building.
An app that reads
Parts I to III of the specification (foundations, records, the folder) and the schemas. A reader of the core needs nothing more.
An app that writes
Add Part V (commands) and the guide Implementing OpenEik as an app's native format: ids, the stored form, revisions, what never to store.
An app with results
The guide Reading and saving an OpenEik model: where a model lives, versions, result manifests, proposed changes and sign-in.
An agent host
Part VII, the agent interface: the map, tiered reads, the filter, budgets, honesty fields and the write loop, whatever the protocol.
A version store
Part VI, history: objects, trees, commits and refs, the diff, and the three-way merge by record and field.
An exporter
Part VIII, bridges: IFC and other formats, and the fidelity report an exporter gives.
In 0.2: electrical installations. Part XIII covers devices and boards, circuits, feed chains, control, cable carriers and modelled cables. The library checks a model of 0.2 by its rules; a model of 0.1, where it was a proposal, only when asked: npx @openeik/tools check --proposal electrical <folder>. Its schemas are in /schemas/0.4/, and its 87 conformance cases are in the suite's zip.
In 0.2: drawings. Part XIV covers sheets, issued drawings as SVG whose lines name their elements, the read set and freshness, the register, issues, reviews and sign-offs. The library checks a model of 0.2 by its rules; a model of 0.1 only when asked: npx @openeik/tools check --proposal drawings <folder>. npx @openeik/tools drawing status <folder> and impact tell which issued drawings a change makes stale, diff compares two revisions element by element, and verify checks sign-offs offline. Its schemas are in /schemas/0.4/, and its 79 conformance cases are in the suite's zip.
The files, on openeik.org
| The specification | OpenEik 0.4, as pages, and as text for agents: llms.txt and llms-full.txt |
| The schemas | each at its $id, or all in one zip: openeik-schemas-0.4.zip |
| The extension registry | the prefixes apps register for what they add to a model, and the names OpenEik reserves; how to register one |
| The samples | complete buildings, each a zip of its folder |
| The conformance suite | cases for readers, validators, command processors and drawing validators, each a small model with what they must do with it, in one zip |
| The library | for npm, as in the quick start, and as ES modules under /tools/0.4/; its README is the API, with its functions and every diagnostic code |
| OpenEik 0.1 | kept as it was: its schemas, its conformance suite and its library |
Conformance
An implementation conforms in one or more classes: reader, validator, writer, compiler, command processor, version store, agent host or bridge, and from 0.2 drawing validator, drawing issuer, host or authoring tool. A claim names the version and the classes, for example "OpenEik 0.4.0 reader and validator". @openeik/tools reads, writes and validates models, merges them, applies the core commands and compiles the class-A rules; the limits below say where it stops short of a class.
Put the OpenEik compatible badge, from the home page, on your site when your tool reads and writes folders that pass openeik check. The conformance suite, cases for readers, validators, command processors and drawing validators, is one zip; Conformance says what each class must do, and where the implementations stand.
Licences
| The specification and the guides | CC-BY-4.0, with a royalty-free patent commitment (OWFa 1.0) |
| The schemas, the examples and the conformance suite | CC0-1.0: copy them into any implementation, open or closed |
| The tools and packages | Apache-2.0 |
The Unicode data in @openeik/tools | Unicode License v3, in the package's NOTICE |
OpenEik is non-profit and vendor-neutral. Proposals and decisions are made in the open, through RFCs, once the repository is public.
Where it stops, for now.
- Reference behaviour is not derived.The compiler derives the class-A rules of Part IV. Wall joins (corners and tees), profiled sheet sections and roof windows are reference behaviour: wall ends are square, and the result's notes say what was left out.
- Core commands only.The processor implements the core record operations of Part V. The documentation profile and the domain operations (moves, arrays, joins, views) are refused; send them as the core operations they amount to. A few operations on placed copies wait until the specification says how a copy's elements are derived.
- A working draft.OpenEik 0.4 is in testing. Until 1.0 a minor version may break compatibility; the changelog says how, and how a model of 0.1 or 0.2 is migrated.
Where OpenEik stands domain by domain, and what comes next: the roadmap. The apps that already work with it: Apps.