Skip to main content

JSON That Thinks: How We Built a Turing-Complete Language Inside JSON

· 3 min read
Osama Alghanmi
Co-Founder & Technical Lead

Every configuration language eventually hits the same wall: you need logic, but your format only holds data. YAML leads to Helm chart nightmares. HCL and Dhall invent new syntax with new parsers. Jsonnet gets close but breaks JSON compatibility. Almadar took a different route: S-expressions encoded as JSON arrays, giving you a Turing-complete language that every JSON tool already understands.

S-Expressions Are Already JSON

In 1958, John McCarthy built Lisp on S-expressions: (+ 1 2). An S-expression is a nested list with an operator in the first position. JSON arrays are nested lists. The mapping is direct:

["+", 1, 2]
["if", [">", "x", 10], "big", "small"]

No new syntax. No custom parser. Just a convention for interpreting what JSON already provides.

Guards: Logic That Controls Transitions

In .orb, S-expressions appear as guards on state machine transitions. A guard must evaluate to true for the transition to fire:

{
"from": "Pending",
"to": "Approved",
"event": "APPROVE",
"guard": ["and",
[">=", "@user.roleLevel", 3],
["<", "@entity.amount", 10000]
]
}

The evaluator resolves bindings (@user.roleLevel becomes 5, @entity.amount becomes 7500), evaluates inner expressions, then evaluates the outer and. If the result is false, the transition does not exist. There is no "skip" button, no override path.

andgte@user.roleLevel3lt@entity.amount10000

Effects: Actions That Follow Transitions

Effects are also S-expressions. They run after a guard passes:

"effects": [
["set", "@entity.status", "approved"],
["set", "@entity.approvedAt", "@now"],
["emit", "REQUEST_APPROVED"]
]

set writes to entity fields. emit sends cross-orbital events. persist saves to the database. render-ui renders a component. Each effect is a single array with an operator and operands.

Arithmetic, Branching, Recursion

S-expressions handle computed values inside effects:

["set", "@entity.total", ["+", "@entity.subtotal", ["*", "@entity.subtotal", 0.15]]]

Conditional logic works with if:

["if", [">", "@entity.score", 100],
["emit", "ACHIEVEMENT_UNLOCKED"],
["emit", "KEEP_GOING"]
]

Self-transitions with guards give you loops. This transition computes a running sum:

{
"from": "Computing",
"to": "Computing",
"event": "TICK",
"guard": [">", "@entity.counter", 0],
"effects": [
["set", "@entity.counter", ["-", "@entity.counter", 1]],
["set", "@entity.result", ["+", "@entity.result", "@entity.counter"]],
["emit", "TICK"]
]
}

State machine as loop. Entity fields as memory. Guard as termination condition. That combination makes .orb Turing-complete.

The Binding Context

S-expressions reference runtime data through prefixed bindings:

PrefixResolves To
@entity.fieldCurrent entity field value
@payload.fieldEvent payload data
@stateCurrent state name
@nowCurrent timestamp
@config.fieldApplication config

These bindings are validated at compile time. Reference a field that does not exist on the entity, and orbital validate catches it before any code runs.

The Tradeoff: Verbosity for Universality

A hypothetical custom syntax: guard: user.roleLevel >= 3 and entity.amount < 10000 (50 characters).

The .orb version: ["and", [">=", "@user.roleLevel", 3], ["<", "@entity.amount", 10000]] (75 characters).

About 50% more characters. In exchange: no custom parser, no custom LSP, no new syntax to learn, every JSON tool works, and LLMs generate it correctly on the first try. Verbosity is a one-time cost. Tooling compatibility compounds forever.

Extending Without Breaking

New operators are additive. Adding geo-distance to the evaluator does not require a schema version bump:

["geo-distance", "@entity.location", "@payload.target"]

If the evaluator knows the operator, it runs. If not, it returns a clear error. This extensibility model kept Lisp alive for 65 years.

Explore the full operator list in the S-expression standard library.