SCUA

A scripting language made for the problems you actually have.

You’re building a game or an AI system, and the scripting language fights you the whole way. Validating data is a chore. Moving state between machines means writing serialization by hand. Embedding feels half-baked. And an AI agent’s script is either useless or one call away from wiping your disk.

So we built the language we actually wanted. Defining and validating data is genuinely simple, and state saves and moves between machines with no serialization code. An idle session can even hibernate to a blob and wake up where it left off. It embeds cleanly and ships as one small binary instead of a heap of dependencies; the interpreter matches or exceeds Lua's, and the JIT goes toe-to-toe with LuaJIT. And because it sandboxes untrusted scripts, an AI agent only touches what you allow. Early access is open.

Faster than Python Lua-fast interpreter JIT runs with LuaJIT Gradually typed Actor model Sessions hibernate Capability-secure I/O Serves HTTP Parallel fan-out Embeds in C
gate.scua
-- Can this player enter the dungeon? A contract reports
-- everything they still need at once, instead of stopping at the first.
contract CanEnter(p)
  level: p.level >= 10    else "reach level 10"
  key:   p.has_key        else "find the dungeon key"
  quest: p.cleared_intro  else "finish the intro quest"
end

let player = { level = 7, has_key = false, cleared_intro = false }
for c in CanEnter(player).clauses do
  if not c.ok then print(`✗ {c.why}`) end
end
$ scua gate.scua
✗ reach level 10
✗ find the dungeon key
✗ finish the intro quest

Why SCUA

The ideas that set it apart

Most of SCUA feels familiar within minutes. These are the parts that pay you back.

Capability-secure by default

No ambient I/O. A script can't reach the filesystem, network, or environment unless the host hands it a capability, and those grants attenuate as they pass down. Safe to run a plugin, a mod, or an AI agent's script you don't fully trust.

Gradual typing

Write it untyped like any scripting language, or add annotations where you want a compile-time check. Annotations are erased, so adding a type can catch a bug but never changes how the code runs.

State that persists & moves

A partition is just position-independent bytes. A host saves them, moves them between machines, and resumes them later, with no JSON round-trips. For data that outlives your code, a separate typed durable save validates and migrates every level as it loads, even a tree.

Exact numbers

A built-in decimal keeps money exact to the penny, so a price or a ledger never picks up float drift. And big integers count past 64 bits without overflowing, for idle-game economies that run away with themselves.

The actor model

Partitions are isolated: no shared mutable state, no data races. They talk by message, where tell fires and forgets and ask waits for a reply.

Sessions that hibernate

Freeze a session to a blob of about 100 KB, even one parked mid-call on a slow model request. Drop it from memory and wake it later with the answer in hand. While it sleeps, it costs you a file in object storage and nothing else.

First-class game math

Vectors, matrices, and quaternions are built-in value types, along with geometry shapes like rectangles, spheres, rays, and planes. Operations read straight off the value, with no math library to wire up: s.volume(), ray.hits(sphere).

Data tables built in

A columnar frame you query with method chains: filter, group, total, join. Think of it as a spreadsheet you can call from code. Decimal columns total exactly, so a report or an AI tool never drifts a fraction of a cent.

Lua-class speed, JIT included

The interpreter keeps pace with Lua's before you touch a flag. Turn on the optional JIT and hot loops compile to native code on ARM64 and x86-64, where it runs with LuaJIT. The output is byte-for-byte identical either way.

Real tooling, day one

A full LSP and a real step-debugger in VS Code and Zed, with completion, hovers, and breakpoints. The whole compiler also runs in your browser, so the playground is the real thing rather than a sandbox approximation.

One-shot tools for agents

A predictable tool contract: typed args from stdin or CLI flags (sys.parse_args), one JSON result on stdout (sys.emit), diagnostics on stderr, a chosen exit status. Emit a JSON Schema for the same record with scua schema, and teach a model the language with scua pack.

Serves HTTP

SCUA can be the server as well as the client. http.serve runs a handler over incoming requests, gated behind its own --allow-serve capability so a script only listens when you say so. Handlers keep working across a snapshot: restore the world and the server picks up where it left off.

Parallel fan-out

Run many independent operations at once and wait for all of them with wait_all and map_all. There's no async/await coloring: an ordinary function runs in a slot, every result comes back in order, and one failure never cancels the rest.

Agent sessions at zero idle cost

Sessions hibernate. Even mid-call.

An agent conversation spends almost all of its life waiting: for the next message, or for a model to finish thinking. SCUA freezes the whole session into a blob around a hundred kilobytes: state, history, even a call still in flight. Drop it from memory, keep the blob in object storage, and when something finally happens, rehydrate it and hand over the answer. The script resumes inside its Ok(answer) arm as if it was never gone. A thousand idle sessions is a thousand blobs, not a thousand warm processes.

Read the hibernation how-to →

agent_session.scua
-- `model` is a module the host registers and serves. The session just
-- asks; the HOST decides whether to wait resident, or freeze
-- the session while the model thinks, and wake it with the answer.
fn handler(state, question)
  match model.ask(question)
    Ok(answer) -> state.push({ q = question, a = answer })
    Error(e) when e == "interrupted" -> reconcile(state, question)
    Error(e) -> state.push({ q = question, failed = e })
  end
  return state
end
[host] parked on model.ask; frozen to store (95 KB), dropped
[host] 34 s later: the answer landed; rehydrating, waking…
resumed in Ok(answer), across the freeze

Run untrusted & AI scripts safely

A script touches nothing you don’t hand it

SCUA has no ambient I/O. The filesystem, network, and environment stay off until the host grants a capability, so a plugin, a mod, or an AI agent’s script only reaches what you allow. Grants narrow as they pass down, so you can hand out a sliver of access without handing over everything.

Write tools for AI agents →

untrusted.scua
-- SCUA has full filesystem and network access, but only
-- when the host grants it. So a script from an AI agent
-- can't touch the disk or network unless you let it.
import fs

let key = fs.read("id_rsa")
print(key)
$ scua untrusted.scua
scua: untrusted.scua:1: module `fs` needs the `fs`
      capability, which this run was not granted — grant it
      with `--allow-fs` on the CLI, or the host's capability
      API when embedding

Made for tools & live-ops

Answer a data question in two lines

A frame is a built-in data table you query with method chains, like a tiny spreadsheet that lives in the language. A decimal column totals to the exact penny, where a float-based tool would drift a fraction of a cent.

Read the data tables how-to →

sales.scua
-- A `frame` is a built-in data table: named columns you
-- query with method chains. A `decimal` column totals
-- exactly, where a float-based spreadsheet drifts.
let sales = frame({
  region = ["EU", "US", "EU", "US"],
  item   = ["sword", "sword", "shield", "potion"],
  net    = [10.50d, 20.00d, 5.25d, 3.00d],
})

print(`EU revenue = {sales.filter(region == "EU").total("net")}`)
print(sales.group_sum("region", "net").to_table())
$ scua sales.scua
EU revenue = 15.75
┌────────┬───────┐
│ region │ total │
├────────┼───────┤
│ EU     │ 15.75 │
│ US     │ 23.00 │
└────────┴───────┘

Fast when you need it

Lua-fast out of the box. LuaJIT-fast on demand.

SCUA was already friendly to write and safe to embed, and now it's quick too. The interpreter on its own matches or exceeds Lua's, the bar scripting interpreters are measured against, and it comprehensively beats Python. Turn on --jit=on and hot loops compile straight to native code, on both ARM64 and x86-64, while the rest of the program keeps interpreting. Against LuaJIT, it's a similar story.

What the JIT won't do is change an answer: its output is byte-for-byte identical to the interpreter, it's entirely opt-in, and it compiles out of a client build for the platforms that forbid runtime code generation. Snapshot the live world mid-run and the bytes match either way.

entities.scua
-- Move every entity one frame: position += velocity * dt.
-- A vec3 is register-shaped, so with --jit=on this loop
-- runs in CPU registers and allocates nothing per frame.
for i in 0:n do
  pos[i] = pos[i] + vel[i] * dt
end
$ scua entities.scua           # interpreter tier
233 ms
$ scua --jit=on entities.scua  # same program, JIT on
7 ms  (identical output, far faster)

Runs where you do

Native today, more on the way

SCUA runs natively on macOS and Linux, and anywhere WebAssembly does. Windows, iOS, and Android are next. And since it's built to embed, you can drop it into just about anything that speaks C.

macOS Linux WebAssembly Windows soon iOS soon Android soon

Try it now

The compiler runs in your browser

SCUA compiles to WebAssembly, so the playground runs the real thing right here. Your code never leaves the page.

Open the playground →

Pick it up in an afternoon

If you know Lua, you already know most of it. The rest is the good part.