Aontu is a JSON-superset language inspired by CUE for describing software systems as data. You write types, defaults, and values in the same notation, and the engine unifies them into one consistent structure, or it refuses the document and names the line that broke. It ships TypeScript and Go implementations, held to the same test suite, and sits under the model layer of the Voxgig SDK generator.
I used it while building a TypeScript client for the Frankfurter exchange-rate API. The model step is where Aontu does its work: OpenAPI is turned into entities, then those entities are unified with target and feature definitions. Once the model is correct, generation works. But getting the model correct was the slow part. Let’s dive deep into my experience with Aontu.
What the design is trying to do
Aontu treats a system definition as a single document that can be checked. Schema and data live together. Defaults are marked with *. Relations can be constrained. The same file can describe what a service needs and what values it currently holds.
A minimal example looks like this:
spec: type({
service: {
owner: string
tier: *1
}
})
services: {
&: $.spec.service
}
services: api: {
owner: "platform"
}
services: auth: {
owner: "identity"
tier: 3
}
Unification fills the default tier on api and keeps the explicit tier on auth. If you set a tier that isn’t an integer, the document fails, and the error points to the offending line.
That design is coherent. For agent-driven workflows, it has clear appeal: an agent can propose a change, Aontu admits or refuses it, and the refusal carries an error code and a source location. The CLI also exposes verbs for validation, path queries, and relation checks. The intent is visible.
Where the friction showed up
The first real failure was path resolution for includes. The top-level model file contained:
@"api/api-info.aontu"
The file existed at model/api/api-info.aontu. The engine still reported:
[aontu/multisource_not_found]: source not found: api/api-info.aontu
and then printed a long list of search paths, most of them under node_modules or parent directories that had nothing to do with the project. The relative path was being resolved from the wrong base. Fixing it required editing the include to match the actual layout the generator had written, and in one case renaming the reference to the generated file (frankfurter-api-info.aontu) because a stub with the expected name was still present and empty.
A second failure was a UTF-8 BOM introduced by PowerShell when creating the required guide entry file. The first character of the file became . Aontu treated it as illegal punctuation in a bare string and refused the document. The error message was precise about the character class, but the root cause was an encoding detail outside the language. Recreating the file without a BOM fixed it.
Windows process spawning also interfered, though that sat one layer above Aontu itself. The generator tried to launch npm directly; Windows exposes it as a command script. That produced spawn npm ENOENT, then spawn EINVAL when forced through npm.cmd. The workaround was a local patch in the npx cache so the call went through a shell. That is not an Aontu bug, but it delayed the first successful model build and made it harder to tell which failures belonged to the language and which belonged to the surrounding toolchain.
None of these problems are theoretical. They appeared while following the documented flow on a current Windows machine with a valid OpenAPI 3.1 spec.
Error messages and mental model
When Aontu refuses a document, the output is detailed. It names the rule, the path, the source line, and often an error code such as multisource_not_found or bare_punct. That is better than a silent failure or a stack trace with no context.
The same detail can become noise. The search-path list for a missing include can run to twenty entries. Most of them are irrelevant. A shorter list that prioritizes the project root and the model directory would be easier to act on. The error is correct; the presentation assumes the reader already knows how the resolver walks the tree.
The mental model also takes time to settle. Unification is not override. Later statements do not win. Compatible statements narrow the result; contradictions fail. That is deliberate and documented, but it is not the default expectation for anyone coming from JSON, YAML, or most config languages. Relative includes (@”…”) are powerful once the base is clear, and opaque until it is. In the Frankfurter project, the includes mixed package paths (@”@voxgig/apidef/model/apidef.aontu”), relative model paths, and generated filenames. Getting them consistent required repeated generate runs and manual inspection of the resulting tree.
What worked once the model was clean
After the include paths and the guide file were fixed, voxgig-apidef produced entities for coverage, currency, provider, and rate. The subsequent npm run generate completed, including the model test stage. The generated TypeScript client exposed those entities as callable surfaces (client.Rate().list(…)), and a live call against the public Frankfurter API returned rates for USD against EUR, GBP, and INR.
The language did what it claims once the surrounding paths and encoding were correct. The entity model stayed small. The operations mapped cleanly. That part of the experience matched the design pitch.
Who this is for, and what still needs work
Aontu is aimed at people who want a single checked definition of a system and who are willing to learn a unification-based language to get it. For that audience, the combination of schema, data, defaults, and relation constraints in one notation is a real advantage. The dual TypeScript/Go implementations and the shared test suite show a seriousness about parity that many small languages skip.
For someone who just wants a working SDK from an OpenAPI file, the model layer is more ceremony than help until the includes resolve and the errors become actionable. Path resolution relative to the wrong base, empty stubs left next to generated files, and BOM sensitivity are not language-design questions; they are day-to-day developer experience questions. They cost time that the design does not need to cost.
A few concrete improvements would reduce that cost:
- Resolve relative includes from the directory of the including file by default, and document the base clearly.
- Prefer project and model directories in the “source not found” search list, and truncate the rest.
- Treat a leading BOM as whitespace or strip it on read, instead of treating it as illegal content.
- Avoid writing both a stub and a generated file with different names for the same conceptual slot; one name, one file, overwritten or merged on each run.
None of these change the core of unification. They would make the first successful evaluation less dependent on knowing the resolver’s internal search order.
Aontu is a focused tool with a clear idea of what a system definition should be. The idea holds up. The path from a fresh project to a clean model still has sharp edges, especially on Windows and around includes. Those edges are fixable without abandoning the design.
Until they are smoother, expect to spend more time on model layout and error diagnosis than the size of the language would suggest.



