Skip to main content

Writing Policies in Stof

Using Stof to author advanced policies.

YAML, JSON, and TOML are fine for defining credits, plans, and limits. They stop working well the moment your policy needs to do something: embed an event handler, define a notification condition, add a capability, or express logic that doesn't fit a static key-value structure.

Stof is the native format of the Limitr policy engine. Everything the engine does internally is Stof. When you write in YAML, it gets parsed into Stof at load time. Writing directly in Stof skips that translation step and gives you the full expressive power of the format — functions, type annotations, attributes, inline logic — without fighting the constraints of a data serialization format.

Stof is optional

Stof is not required to use Limitr. For advanced use cases it enables you to enforce rules and conditions of arbitrary complexity — but most integrations start with YAML and only reach for Stof when they need it. If you're using Limitr Cloud, the dashboard handles policy authoring and you rarely need to write Stof directly.

Try it

Use the online playground to try Stof for yourself. Limitr may be your introduction to the project, but Stof is a general-purpose data runtime you can use anywhere.


The same policy in YAML and Stof

Here's a minimal policy in YAML:

policy:
credits:
token:
description: AI token
overhead_cost: 0.000003
pricing_model: flat
price:
amount: 0.000004
stof_units: int
resets: true

plans:
starter:
label: Starter
default: true
entitlements:
chat_access:
description: Access to AI chat
chat_input:
limit:
credit: token
mode: hard
value: 500000
resets: true
reset_inc: 1day

The same policy in Stof:

policy: {
credits: {
token: {
description: 'AI token'
overhead_cost: 0.000003
pricing_model: 'flat'
price: { amount: 0.000004 }
stof_units: 'int'
resets: true
}
}

plans: {
starter: {
label: 'Starter'
default: true
entitlements: {
chat_access: {
description: 'Access to AI chat'
}
chat_input: {
limit: {
credit: 'token'
mode: 'hard'
value: 500000
resets: true
reset_inc: 1day
}
}
}
}
}
}

Structurally nearly identical at this level. The difference becomes apparent when you add behavior.


Stof syntax basics

Full docs

See the Stof docs for the complete language reference.

Fields — unquoted key-value pairs, no commas required:

name: 'Starter'
value: 500000
resets: true

Objects — curly braces:

price: {
amount: 0.000004
}

Strings — single or double quotes. Field names and type names are unquoted:

description: 'Input tokens for Claude Sonnet 4'
credit: 'token'

Numbers and units — duration and storage units are first-class Stof types:

reset_inc: 1day
value: 2GiB
overhead_cost: 0.000003

Comments:

// This plan is for evaluation customers only
hidden: true

Functions:

fn matches(type: str, event: obj) -> bool {
type == 'meter-changed' && event.remaining < 1000
}

Type annotations:

str description: 'AI token'
float overhead_cost: 0.000003
bool resets: true

Attributes — bracketed decorators placed before a field or function:

#[meter-overage]
fn handle_overage(event: obj) {
// called on every meter-overage event
}

What Stof gives you that YAML doesn't

Inline notification conditions

In YAML, notifications must be loaded separately via setNotifications(). In Stof, they live directly in the policy file:

policy: {
credits: { ... }
plans: { ... }

notifications: {
approaching-limit: {
fn matches(type: str, event: obj) -> bool {
type == 'meter-changed' &&
event.entitlement == 'chat_input' &&
event.remaining < (event.meter.limit * 0.2)
}
}

hard-limit-hit: {
fn matches(type: str, event: obj) -> bool {
type == 'meter-limit'
}
}
}
}

These are indistinguishable from notifications loaded via setNotifications() at runtime — same behavior, same addHandler() integration — but they're part of the versioned policy file.

Inline event handlers with attributes

Attribute-decorated functions fire automatically on matching events. No setNotifications(), no addHandler() — the handler is embedded in the document:

policy: {
credits: { ... }
plans: { ... }

#[meter-overage]
fn on_overage(event: obj) {
// Call a TypeScript-registered library function
?App.meter_overage(stringify('json', event));
}

#[meter-limit]
fn on_limit(event: obj) {
?App.meter_limit(stringify('json', event));
}
}

See Embedded Event Handlers for the full treatment of this pattern.

Inline capabilities

policy: {
credits: { ... }
plans: { ... }

capabilities: {
area: {
version: 0.1.0
description: 'Calculate the area of a shape'
parameters: [
{ name: 'width', description: 'Width in any unit', schema_type: 'string' }
{ name: 'height', description: 'Height in any unit', schema_type: 'string' }
]
result: 'area'

#[run]
fn calculate() {
self.output.area = (self.input.width as float * self.input.height as float).round(2);
}
}
}
}

Type annotations

Stof lets you annotate field types in the policy. The validator uses these at load time:

credits: {
token: {
str! description: 'AI input token' // required (never null) string
float overhead_cost: 0.000003
bool resets: true
}
}

Loading a Stof policy

import { Limitr } from '@formata/limitr';
import { readFileSync } from 'fs';

// From a .stof file
const policy = await Limitr.new(readFileSync('./policy.stof', 'utf-8'));

// Inline (format defaults to 'stof' when omitted)
const policy = await Limitr.new(`
policy: {
credits: {
token: { overhead_cost: 0.000003 }
}
}
`);

// Empty policy
const policy = await Limitr.new();

A complete policy in Stof

The AI token metering pattern, written natively in Stof with inline notifications:

policy: {
credits: {
sonnet_input: {
description: 'Claude Sonnet 4 input tokens'
overhead_cost: 0.000003
pricing_model: 'flat'
price: { amount: 0.000004 }
stof_units: 'int'
resets: true
}
sonnet_output: {
description: 'Claude Sonnet 4 output tokens'
overhead_cost: 0.000015
pricing_model: 'flat'
price: { amount: 0.00002 }
stof_units: 'int'
resets: true
}
ai_credit: {
description: 'AI Credits'
label: 'AI Credit'
unit: 'credit'
}
}

exchange: {
rune: { value: 1, currency: 'usd' }
ai_credit: { value: 1.25, currency: 'rune' }
sonnet_input: { value: 0.000004, currency: 'ai_credit' }
sonnet_output: { value: 0.00002, currency: 'ai_credit' }
}

plans: {
starter: {
label: 'Starter'
default: true
entitlements: {
chat_access: { description: 'Access to AI chat' }
chat_input: {
limit: { credit: 'sonnet_input', mode: 'hard', value: 500000, resets: true, reset_inc: 1day }
}
chat_output: {
limit: { credit: 'sonnet_output', mode: 'hard', value: 200000, resets: true, reset_inc: 1day }
}
}
}

growth: {
label: 'Growth'
entitlements: {
chat_access: { description: 'Access to AI chat' }
chat_input: {
limit: { credit: 'sonnet_input', mode: 'soft', value: 2000000, resets: true, reset_inc: 1day }
}
chat_output: {
limit: { credit: 'sonnet_output', mode: 'soft', value: 800000, resets: true, reset_inc: 1day }
}
}
topups: {
monthly_credits: {
description: '50 AI credits included monthly'
credit: 'ai_credit'
value: 50
included: true
resets: true
reset_inc: 30days
reset_mode: 'hard'
}
}
}
}

// Notifications inline — no setNotifications() call needed
notifications: {
approaching-limit: {
fn matches(type: str, event: obj) -> bool {
type == 'meter-changed' &&
event.entitlement == 'chat_input' &&
event.remaining < (event.meter.limit * 0.2)
}
}

overage-started: {
fn matches(type: str, event: obj) -> bool {
type == 'meter-overage' &&
event.remaining >= -1 &&
event.remaining < 0
}
}
}
}

Augmenting an existing policy with doc.parse()

You don't have to write the entire policy in Stof to use Stof features. If your base policy is YAML, you can parse additional Stof into the document at runtime:

const policy = await Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml');

// Add Stof-only features after loading the YAML base
policy.doc.parse(`
#[meter-overage]
fn on_overage(event: obj) {
?App.meter_overage(stringify('json', event));
}
`);

doc.parse() merges the Stof into the existing document — fields are added or replaced, existing fields not mentioned are untouched. This is how setNotifications() works internally.


When to use Stof vs. YAML

YAML / JSON / TOMLStof
Credits, plans, limits, exchange
Inline notifications
Embedded event handlers
Capabilities
Type annotations
Logic and expressions
Familiar to most developersRequires learning

Start with YAML. Reach for Stof when you want notifications or event handlers embedded in the policy file, need type annotations for clarity, or are building tooling that generates policies programmatically.