Run AI-generated logic safely, inside your app.
Stof is JSON with functions. One document holds data and the logic that works on it, and your app runs it in a sandbox, in-process, in Rust, JavaScript, or Python.
What it looks like
This is a Stof document. The data looks like JSON, and the function next to it works on that data:
ticket: {
text: 'Where is my refund?'
priority: 1
}
#[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';
}
Your app loads it and works with it as needed:
import { StofDoc } from '@formata/stof';
const doc = await StofDoc.new(source);
await doc.run('triage'); // runs every function tagged #[triage]
doc.get('ticket.team'); // 'billing'
A Stof document is data you can store, send, and merge like JSON, and code you can run, safely:
- It runs in a sandbox. A document can read and change itself and call the functions you give it. Nothing else: no file system, network, or environment unless you turn them on.
- It runs in your process. No server, container, or cold start. A small document parses and runs in under a millisecond.
- It runs the same everywhere. Rust, Node.js, the browser, and Python give the same result.
Everything is data
Stof has functions, but it isn't a language for writing apps. It's a document, and every part of it is data. The document above is a tree of objects, and the function sits on its object the same way the fields do:
root
├── ticket object
│ ├── text: 'Where is my refund?' field
│ └── priority: 1 field
└── route() function: data on the object, like a field
Fields, functions, types, and docs are all pieces of data attached to objects (an entity-component system, if you know game engines). That's what makes the rest possible:
- Anything you can do with data, you can do with logic. Merge two documents, pull one object out with its functions, diff two versions, store it in a database, or send it over the wire.
- A document can change while it runs. It can parse new fields and functions into itself, or drop them, with no build step.
- It grows to fit your domain. Your app adds its own host functions to safely bridge contexts, types and schemas live in the document, and in Rust you can add your own formats and kinds of data.
- There's very little to learn. If you can read JSON and a bit of JavaScript, you can read Stof, and so can a model.
Why it matters now: models write logic
Models are good at writing small pieces of logic: a routing rule like the one above, 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 |
With Stof, the model writes a document, and your app runs it like any other data it receives. The tag #[triage] is just a label the model and your app agree on, so your app doesn't depend on what the model named its functions or how many it wrote:
const doc = await StofDoc.new(modelOutput); // rules a model wrote
doc.parse({ ticket: { text: 'Where is my refund?', priority: 1 } });
await doc.run('triage');
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(['api.example.com']) 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 (very good for distributed systems):
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.
- You're about to invent a config format, rules engine, or DSL for your product.
- 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. Each call has a time limit, too.
- Fast and in-process. 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. It reads like JSON and JavaScript, and errors give the exact line and a plain explanation, so a model can fix its own mistakes.
- More than a rules engine. Types and schemas, units (
5min + 30s == 330s,2GBtoMBexactly), destructuring, async, and tests inside the document (#[test], run withstof test). - Proven in production. Stof is the policy engine behind Limitr, where documents are pushed to running apps and evaluated in-process.
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() (or allows only some hosts with allow_http_hosts). Each call also has a time limit (2 minutes by default, set by the host). 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.6
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.6.tar.gz | 467.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| stof-0.10.6-cp314-cp314-win_amd64.whl | CPython 3.14 | CPython 3.14 | Windows x86-64 | Details |
Total release size: 7.8 MB
Release files / stof-0.10.6.tar.gz
| Download URL | stof-0.10.6.tar.gz |
|---|---|
| Size | 467.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9818f91d0c589a4a6dfd7e558216505c900a0701f857fc4b01a535aba553ecb5
|
|
BLAKE2b-256 checksum How to use checksums |
606a4634359c73dd5c69712a1ab7e49346aeec6355410c2eee38f932f7de201d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.13.1
|
Release files / stof-0.10.6-cp314-cp314-win_amd64.whl
| Download URL | stof-0.10.6-cp314-cp314-win_amd64.whl |
|---|---|
| Size | 7.3 MB |
| Tags | CPython 3.14 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
bc334a1d034281194f0274ce2f844b2b1003b832e8f44d3fc9b1a56e4fc5198d
|
|
BLAKE2b-256 checksum How to use checksums |
1f739d47bee2af159669b2192856c1129e0e9cc89213c305b05fa641177d6b0a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.13.1
|