Run AI-generated logic safely, inside your app.
Stof is a sandboxed runtime for logic that lives in data. Let a model write a rule, a transform, or a tool, then run it in-process in under a millisecond, with access to nothing but the data and functions you give it. Stof documents combine and split like JSON, and run like code, in Rust, JavaScript, and Python.
Your agent can write the logic. Where do you run it?
Models are good at writing small pieces of logic: a routing rule, a validation check, a data transform, a tool. Running that logic is the hard part, because the code came from a model, not from your team.
| Option | The catch |
|---|---|
| Run generated Python or JS in a container or microVM | Heavy, slow to start, and more infrastructure to run and secure |
eval it in your app |
The code can reach everything your app can |
| Function calling / JSON tool calls | The model can only call functions you already wrote; it can't add new logic |
| Rule DSLs (logic encoded as JSON) | Limited, and awkward to read and write, for people and models alike |
| WebAssembly | Needs a compiler toolchain, binaries are large to send, and the logic lives apart from the data |
Stof is the missing option: a small language made to be written by models, sent as data, and run in-process in a sandbox.
How it works
A model writes this. Stof is JSON plus functions, and the tag #[triage] is just a label the model and your app agree to:
#[triage]
fn route() {
if (self.ticket.text.lower().contains('refund')) self.ticket.team = 'billing';
else if (self.ticket.priority > 2) self.ticket.team = 'urgent';
else self.ticket.team = 'general';
}
#[triage]
fn flag() {
self.ticket.needs_human = self.ticket.priority > 2;
}
Your app runs everything tagged #[triage], without requiring what the model named its functions or how many it wrote:
import { StofDoc } from '@formata/stof';
const doc = await StofDoc.new(modelOutput);
doc.parse({ ticket: { text: 'Where is my refund?', priority: 1 } });
await doc.run('triage');
doc.get('ticket.team'); // 'billing'
And the document can't reach anything you didn't give it:
const doc = await StofDoc.new(`fn run() -> str { fs::read_string('/etc/passwd') }`);
await doc.call('run'); // throws: this host hasn't given documents file system access
To let it do more, you hand it specific functions: doc.lib('App', 'notify', notify) makes App::notify(...) available, and nothing else. File system, environment, and network access stay off unless you turn them on (doc.allowHttp() here, allow_system() and allow_http() in Rust and Python).
Logic that moves like data
Documents combine like data. Logic and data from different places merge into one document, and everything tagged #[triage] runs together:
const doc = await StofDoc.new(teamPolicy); // your team's rules (Stof)
doc.parse(modelOutput); // + rules a model wrote (Stof)
doc.parse(apiResponse, 'json'); // + data from an API (JSON)
await doc.run('triage'); // run all the rules together
doc.stringify('json'); // and hand back plain JSON
team policy (Stof) ─┐
model rules (Stof) ─┼─▶ one document ─▶ run('triage') ─▶ JSON out
API data (JSON) ─┘
They split like data, too. Any part of a document can be sent on its own, functions included, as compact binary: doc.blobify('bstf', part). Store it, version it, diff it, send it over HTTP or a WebSocket, and run it wherever it lands. The same document gives the same result in Rust, Node.js, the browser, and Python.
Where it fits
Stof is a glue layer between the formats you already have and your code:
where things come from the glue (often temporary) your app
────────────────────── ────────────────────────── ────────
model output Stof ─┐
API responses JSON ─┤
config files YAML ─┼─▶ Stof document: data + logic ─▶ run it, read the results
settings TOML ─┤ ─▶ export JSON / YAML / TOML
user input Stof ─┘
Load data from anywhere, add logic, run it, and keep the document or throw it away. The runtime is a library (npm, crate, or pip) inside your app: no server, container, or extra process, and documents can only call the functions you expose.
Reach for Stof when:
- A model, a user, or a customer gives you logic you need to run: rules, filters, transforms, scoring, tool behavior.
- The same logic has to run in more than one place (browser, server, worker) and give the same answer.
- You want to change behavior by sending or storing a new document, not by shipping a deploy.
- Logic should travel with the data it belongs to, through APIs, queues, and databases.
Use something else when the code is your own application logic, it's compute-heavy, or it's a long-running service. Write that in your main language and expose it to Stof documents as functions.
Why Stof
- Safe by default. A document can only read and change itself and call the functions you give it.
- Fast and in-process. No containers or cold starts: parsing and running a small document takes under a millisecond, and each call after that takes microseconds.
- Small on the wire. Documents, functions included, serialize to compact binary: the rules above are under 1KB.
- It starts as JSON. Any JSON document is valid Stof, and Stof imports and exports JSON, YAML, TOML, and more.
- Models already write it. Stof reads like JSON, so models write it without a lot of special training, and errors give the exact line and a plain explanation, so a model can fix its own mistakes.
- Proven in production. Stof is the policy engine behind Limitr, where documents are pushed to running apps and evaluated in-process.
A few things you'll like
- Units built in:
5min + 30s==330s, and2GBconverts to2000MBexactly, when needed for comparisons, etc. - Types and schemas: objects can have types with inheritance, and schemas that validate other objects.
- Documents that grow: a running document can parse new fields and functions into itself.
- Tests inside the document: mark functions with
#[test]and run them withstof test. - Clear errors: parse and runtime errors point to the file, line, and column, so a model can fix its own mistakes.
Quickstart
Try it in your browser at the Stof Playground, or install it:
npm i @formata/stof # JavaScript / TypeScript
cargo add stof # Rust
pip install stof # Python
cargo install stof-cli # command line: stof run, stof test
TypeScript
import { stofAsync } from '@formata/stof';
const doc = await stofAsync`
"name": "Stof"
fn hello() -> str { \`Hello, \${self.name}!\` }
`;
console.log(await doc.call('hello')); // Hello, Stof!
Python
from pystof import Doc
doc = Doc()
doc.parse('rate: 2\nfn total(units: int) -> float { units * self.rate }')
print(doc.call('total', 21)) # 42.0
Rust
use stof::model::Graph;
use stof::runtime::{Runtime, Val};
let mut graph = Graph::default();
graph.parse_stof_src("rate: 2\nfn total(units: int) -> float { units * self.rate }", None, Default::default())?;
let total = Runtime::call(&mut graph, "total", vec![Val::from(21i64)])?; // 42
Teaching a model Stof: give it content/SKILL.md, a short guide to the language with the library references in content/stof.zip.
FAQ
Is it really safe to run Stof a model wrote?
A document runs in a sandbox: it can see itself and the functions your app provides, nothing else. File system, environment variables, file imports, and network access are all off by default, in every language; a host turns them on explicitly with allow_system() or allow_http(). The stof command-line tool turns them on, since it runs your own files.
Why not just run the model's Python in a container? You can, but it means infrastructure, startup time, and a lot of access to lock down. Stof runs inside your process, starts instantly, and has no access by default.
Can models actually write Stof? Yes. It reads like JSON and JavaScript, which every model already knows. For better results, give your agent the Stof skill: a short guide to the language and its common mistakes. When something's wrong, errors give the line and a plain explanation the model can act on.
Why not WebAssembly? WebAssembly needs a compiler toolchain, and its binaries are large to send. Stof is readable, small, and travels with the data it works on. (The Stof runtime itself is compiled to WebAssembly for browsers.)
Do I have to replace JSON, YAML, or TOML? No. Stof imports and exports them, so you can add logic to the data you already have.
Learn more
- stof.dev: docs, the standard library, and examples
- Playground: try Stof in the browser
- Limitr: Stof in production
- Discord: questions and discussion
- Changelog and issues
License
Apache 2.0. See LICENSE.
Metadata
Release files for stof 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| stof-0.10.0.tar.gz | 423.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| stof-0.10.0-cp314-cp314-win_amd64.whl | CPython 3.14 | CPython 3.14 | Windows x86-64 | Details |
Total release size: 7.6 MB
Release files / stof-0.10.0.tar.gz
| Download URL | stof-0.10.0.tar.gz |
|---|---|
| Size | 423.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bbc91683447c7219a0c914e865cee700390141d05e32a81ae8b9377c01f25ea3
|
|
BLAKE2b-256 checksum How to use checksums |
35975ceaac6b514b9924345e57c7fa1196a717d6f8a9e11b3284307384e3bd2f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.13.1
|
Release files / stof-0.10.0-cp314-cp314-win_amd64.whl
| Download URL | stof-0.10.0-cp314-cp314-win_amd64.whl |
|---|---|
| Size | 7.2 MB |
| Tags | CPython 3.14 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
d3a3980eed45d296410a2c6c19603c27373595f7762b1d1dc288413e836363de
|
|
BLAKE2b-256 checksum How to use checksums |
7a434dfd5c86770f7c4323a4df472a830a5ff2bd487716581df564c6ec0eb088
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.13.1
|