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.
-- 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 questWhy 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.
-- `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 freezeRun 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.
-- 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 embeddingMade 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.
-- 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.
-- 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.
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.
Pick it up in an afternoon
If you know Lua, you already know most of it. The rest is the good part.