Skip to content
Projects

dev-formata-io/stof has 64 GitHub stars and 5 forks. GitHot measured 0 stars in 24 hours and 0 in 7 days. Last checked 2026-11-15 06:35 UTC. Last push 2026-11-06.

How these counts are measured

Measured star growth

in 24h
0
in 7 days
0
in 15 days
Collecting history
in 1 month
Collecting history

Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.

About

Software that behaves differently for every user: each customer's rules, tools, and state in one document that an agent can edit, a person can review, and your app runs, sandboxed, in Rust, JavaScript, and Python.

AI

README.md

Original

Stof

Software that behaves differently for every user.

Start with your agent. Each customer's rules, tools, and state go in one document: the agent edits it, you review the diff, and your app runs it. Stof is JSON with functions, run in a sandbox inside your app, in Rust, JavaScript, or Python.

One customer, one document

This is how a support agent handles refunds for one customer, Acme. It's a Stof document: the data looks like JSON, and the function next to it is a tool the agent can call.

// acme.stof
auto_approve_under: 50
monthly_cap: 500
refunded_this_month: 0

/// Refund an order, or escalate it to a person.
fn refund(order: str, amount: float) -> str {
    if (amount >= self.auto_approve_under) return "escalated";
    if (self.refunded_this_month + amount > self.monthly_cap) return "escalated";
    Payments::refund(order, amount);
    self.refunded_this_month += amount;
    "refunded"
}

#[test]
fn large_refunds_need_a_person() {
    assert_eq(self.refund("A-1", 120), "escalated");
}

Your app loads Acme's document and hands it the tools Acme's agent may use:

import { StofDoc } from '@formata/stof';

const acme = await StofDoc.new();
acme.lib('Payments', 'refund', (order: string, amount: number) => payments.refund(order, amount));
acme.parse(source);                    // acme.stof

await acme.call('refund', 'A-1', 40);  // 'refunded', and refunded_this_month is 40
await acme.call('refund', 'A-2', 120); // 'escalated': over the auto-approve limit

Then Acme's account manager says Acme's refunds up to $150 can go through without a ticket. The agent proposes a change to Acme's document, the test in the document runs, and you approve a one-line diff:

-auto_approve_under: 50
+auto_approve_under: 150

The same $120 refund now goes through, for Acme only, with no deploy. That's the idea: the agent changed one line in this customer's document, and your app behaves differently for them.

The document can only use what your app hands it. Payments::refund works because you registered it for Acme. A call to anything else (Ex. Payments::export_customers()) fails, and a document has no file system, network, or environment unless you turn them on.

Why a document

Teams building agents end up hand-building each customer's rules out of prompts, JSON, and glue code. The usual options each break somewhere:

Instead of a document What breaks
Let the model write code and run it in a sandbox The diff is hard to review, the sandbox is more infrastructure, and the code doesn't travel with the customer
JSON tool calls Safe, but JSON can't hold logic ("this month", "under $50"), so a rules engine grows around it
Put the rules in the prompt A rule in a prompt is a suggestion the model may ignore
Feature flags and settings The settings are per customer, but the logic is shared and changes with a deploy

A Stof document is all of the things you need at once: a model can write it, a person can read and diff it, it has its own tests, and your app runs it deterministically, in-process, in a sandbox. The same document gives the same result in Rust, Node.js, the browser, and Python.

Why it works: 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. Acme's document is an object, and the function sits on it the same way the fields do:

root (acme.stof)
├── auto_approve_under: 50          field
├── monthly_cap: 500                field
├── refunded_this_month: 0          field: state, updated as refunds go through
├── refund()                        function: data on the object, like a field
└── large_refunds_need_a_person()   test

Fields, functions, types, and docs are all pieces of data attached to objects (an entity-component system, if you know game engines). That's why a document can hold a customer's behavior and not only their settings:

  • 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.

Logic that moves like data

Documents combine like data. Logic and data from different places merge into one document, and every function tagged #[check] runs together:

const doc = await StofDoc.new(teamRules);         // rules every customer gets (Stof)
doc.parse(acmeRules);                             // + rules Acme's agent added (Stof)
doc.parse(order, 'json');                         // + data from your API (JSON)

await doc.run('check');                           // run all the #[check] rules together
doc.stringify('json');                            // and hand back plain JSON
team rules (Stof) ─┐
Acme rules (Stof) ─┼─▶ one document ─▶ run('check') ─▶ JSON out
order 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 isn't an agent framework. Your agent, your model, and your app stay yours; Stof is the document each customer's behavior lives in, and the runtime that runs it. It also works as 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:

  • Each customer (or user, or tenant) needs its own rules, tools, limits, or state, and an agent or a person needs to change them safely.
  • 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 time and memory limits, and you can seal the parts you trust so the rest of the document can call them but not change them.
  • 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. A document travels as text or as binary (bstf), functions and state included: Acme's document is about 600 bytes of text, or 2KB as binary.
  • 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, 2GB to MB exactly), destructuring, async, and tests inside the document (#[test], run with stof test).
  • Proven in production. Stof is the engine behind Limitr, where each customer's pricing (plans, limits, credits, and the rules around them) is a Stof document, 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. The npm package ships the same skill, references included, in node_modules/@formata/stof/skill/stof.

FAQ

Is it safe to run a document an agent wrote or changed? 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). Network access never reaches your machine or private network (localhost, internal services, the cloud metadata service) unless you list those hosts. Each call also has a time limit (2 minutes by default) and a memory limit (256 MB by default), both set by the host, so a runaway document fails its call instead of taking your app down. The stof command-line tool turns access on, since it runs your own files.

Can an agent's function change the rest of the document? By default a document can change itself, including its own functions. Seal the parts you trust (doc.seal('<Engine>')) and they can still be called, but not replaced, removed, or added to, by the document or anyone else, until you unseal them. doc.digest(path) fingerprints a part's functions, and doc.seal(path, { publicKey, signature }) seals it only if that fingerprint was signed by your key, so you know the logic you're running is the logic you shipped.

For code that comes from somewhere else (a plugin, a rule an agent wrote), seal it confined: doc.seal('plugins.weather', { reads: ['inputs'], hosts: ['api.weather.example'] }). While it runs, it sees only itself and what you list, changes only itself, and reaches only those hosts; the rest of the document stays out of its reach. Seal it removable and you can drop it whole when it's no longer wanted.

Can a document check where data came from? Yes: the Key library signs and verifies (Key::sign(secret, 'orders/v1', order), Key::verify(public, 'orders/v1', order, signature)), and your app has the same functions (StofDoc.sign, StofDoc.verify), so a signature made in one place checks in any other. Each signature names what it's for (its context), so it can't be reused for anything else.

Why not let the model write Python and run it in a container? You can, but it means infrastructure, startup time, and a lot of access to lock down, and the change a person reviews is a program, not a one-line diff. Stof runs inside your process, starts instantly, and has no access by default.

Where do the documents live? Wherever you keep customer data: a database row per customer, object storage, or a repo if you want history and reviews. State the document updates (like refunded_this_month) is part of the document: save it with doc.blobify('bstf') and load it back with StofDoc.new(bytes, 'bstf').

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

License

Apache 2.0. See LICENSE.