Will ChenWill Chen
← Writingsystem design

Choosing a notation for a model to write in

I spent three months designing a rich notation for a model to write in, then deleted all of it. The syntax was for humans.

Will ChenWill Chen6 min

For three months in 2025 I designed a rich notation for an AI to emit, and then I deleted the design and used function calls and JSON. The deletion was the result, and the argument for it took about a minute to write down once I had it.

Motivation

I was building a system where a person describes an agent in ordinary language and something has to come out the other end that a machine can run. That intermediate thing needs a written form. It has to be generated reliably by a language model, read back by a program, edited by a person when the model gets it wrong, and checked for errors before anything executes.

The obvious candidate is JSON, which models emit reliably and every mainstream language parses. Looking back at the origin later, I put the problem plainly: XJSN came out of needing to represent agent behavior and have AI build the agent for you, and JSON was not going to cut it.

JSON is a data format. It has objects, arrays, strings, numbers and a couple of literals, and it has no way to say that this thing here is a call, or a condition, or a reference to that other thing over there. You can encode all of it as nested objects with a field naming what each one is, and then you have invented a language and written it in the least readable available syntax.

First attempt: markup outside, JSON inside

In late June my answer was to keep JSON on the inside and put a markup layer on the outside:

I need JSON internal, but XML external repr (superset) such that LLM can easily generate

The reasoning is sound as far as it goes. Markup handles nesting and mixed content well, models have read enormous quantities of it, and it has a mature validation story, which matters when the thing generating your program is probabilistic and you want to reject bad output before it runs. I spent real time in that direction, including on schema validation and on whether the validator could run in the browser without an unacceptable cost.

Two months later I abandoned all of it.

Deleting the syntax

The whole reversal is one paragraph, written on 6 September:

i deliberately chose function calls because i realized with AI, we don't need the syntactic richness built for human usability. function calls + json are alrady ergonomic we dont need dhall. like function calls are already same semantics as s exprs. the key point is decoupling syntax from semantics

Three claims are stacked in there, and they come apart cleanly.

The first is that syntactic richness exists for people. Every convenience in a programming language, the infix operators, the significant whitespace, the shorthand for common shapes, exists because a human being has to type it and read it back later. A model has neither constraint. It does not get tired of typing and it does not lose its place in a nested structure. Designing ergonomics for a reader who has none of the problems ergonomics solve is designing for a user who is not there.

The second is that function calls are already the thing. f(a, b) and (f a b) are the same structure written two ways, one with the operator inside the parentheses and one outside. The Lisp family gets a reputation for strangeness on the strength of that difference, and the difference is punctuation. Whatever expressive power people attribute to symbolic expressions is available in a notation everyone already types.

The third is the one that generalises past this project. Syntax and semantics are separable, and once you accept the first two claims there is nothing left to design in the syntax, so all the design effort moves to the meaning. What operations exist, what they take, what they return, how they compose. Those questions were always the real ones and the notation had been absorbing the attention.

The notation and the parse result

The package that came out of it is called XJSN, for eXtensible JavaScript Notation, and its README states the whole design in one line: it is JSON with function calls as a primitive type.

The problem it replaces is what you get if you try to represent a program in plain JSON. Every construct needs a field naming what it is, and the nesting compounds:

{
  "$type": "conditional",
  "$condition": {
    "$type": "function_call",
    "$name": "user_has_permission",
    "$args": [{ "$type": "variable", "$ref": "current_user" }]
  }
}

That is four levels deep to say one thing, and a model has to get every bracket right to produce it. In XJSN the call sits where a value would go, and the tagging disappears:

{
  "workflow": checkPermission(currentUser),
  "actions": [sendNotification(), returnResponse("success")]
}

The call is not evaluated. It parses into data, which is the property the whole design rests on:

{ "$type": "call", "$fn": "checkPermission", "$args": [currentUser] }

So the verbose form is still there, and the model no longer has to type it. Writing the tagged object was the model's job in the first version and is the parser's job in this one.

What that buys is a substrate rather than a language. The parsing stays fixed and each domain supplies its own functions, so a workflow tool and a game both write f(a, b) and mean entirely different things by it, while the tree underneath has one shape.

By September the design was fixed enough to describe by comparison:

XJSN is basically Clojure.spec / EDN / Racket in JSON clothing

Anyone who knows those three languages has the specification already. For anyone who does not, the shared idea is that a program is written in the same form as the data it works on, so one program can read and build another as easily as it can read a list. XJSN takes that idea, adds a way of stating what counts as valid, and writes both in the notation that models and web programs already have in common.

The design goal underneath it had been stated a year or more earlier in a note on the same theme, which is that rather than building a new toolchain for every small language you want, you build one extensible notation that can be repurposed into any of them. And the practical form it took was a subset of one language and a superset of another, arranged so that the people using it can attach automatic checking to the parts they care about.

Adjacent work: projectional editors

Systems that let you edit a program's structure directly, rather than editing text that gets parsed into structure, are called projectional editors, and the best known one is JetBrains MPS. In September I described what I was building as a system in that family, written in TypeScript, driven by AI, and aimed at people who would never install a Java IDE, and I wanted the editing to be flow based as well as projectional.

I spent part of the same afternoon asking why MPS had not spread further, because a good idea that did not travel is more informative than one that never had the chance. Projectional editing is a genuinely good idea and MPS implements it seriously; what it also asks of you is a Java toolchain and a commitment to building inside its world before you get anything back. That is a steep first step for someone who wants a small language for one domain, and it is the specific step I was trying to remove rather than a verdict on the tool.

The strategy call came in the same conversation and it has its reasoning attached: the technology was going to be open sourced anyway, so treating it as a moat was not an option worth defending. That is a clearer basis for open sourcing than most, because it starts from an honest assessment of what is actually defensible rather than from a position on openness.

What it cost

Three months went into a notation that did not ship. What came out is a shorter specification, a generation path that models already handle without special instruction, and the attention that had been going into syntax now going into the operations. The part I would carry forward is the question rather than the answer: before designing a representation for a machine to write, work out which of its properties exist for human readers, and check whether your reader is human.