Extend Limitr
Most of a policy is data: credits, plans, limits, prices. When your pricing needs logic that data can't express, the policy can carry code too. It's written in Stof, lives next to the data it uses, and runs inside your process in a sandbox.
That's how Limitr grows with your product. Instead of working around a billing system's fixed rules, you add the rule you need to the policy, and it runs the same on every server and in Limitr Cloud.
Your own functions
A function on the policy is called like any engine function, from your app or from other functions in the policy. Use one to keep a rule next to the pricing it depends on:
import { Limitr } from '@formata/limitr';
const limitr = await Limitr.new(`
policy: {
credits: {
seat: { label: 'Seat' }
project: { label: 'Project' }
}
plans: {
team: {
default: true
entitlements: {
seats: { scope: 'org', limit: { credit: 'seat', value: 5 } }
projects: { scope: 'org', limit: { credit: 'project', value: 10 } }
}
}
}
// Your own rule, next to the pricing it depends on:
// inviting someone also opens a starter project for them.
fn invite(user: str) -> str {
if (!self.check(user, 'seats', 1)) return 'no seats left';
if (!self.check(user, 'projects', 1)) return 'no projects left';
self.allow(user, 'seats', 1);
self.allow(user, 'projects', 1);
'invited'
}
}`);
await limitr.ensureCustomer('acme', 'team', 'org');
await limitr.ensureCustomer('ada', '', 'user', 'Ada', ['acme']);
// Call it from your app like any engine function.
for (let i = 1; i <= 6; i++) {
console.log(`invite ${i}:`, await limitr.docCall('policy.invite', 'ada'));
}
It prints:
invite 1: invited
invite 2: invited
invite 3: invited
invite 4: invited
invite 5: invited
invite 6: no seats left
docCall(path, ...args) calls any function in the policy document. Inside the policy, self is the policy, so your
functions use the same engine functions your app does.
Where you can add logic
| You want to | Use |
|---|---|
| Price a call from its own data (model, tokens, region) | A cost function on the credit |
| Turn a feature on per customer or per call | A feature rule (enabled) on the entitlement |
| React to usage: alerts, upsells, syncing | Notifications, or a function named for the event (#[meter-overage]) |
| Reach your app from the policy | Host functions your app registers |
| A named operation for your app, your services, or AI agents | A capability |
| Bring outside data in: rates, customers, settings | A capability that fetches from an allowed host |
| Share logic across credits, plans, and features | Functions in a type |
| Ask a question the API doesn't answer | callSource with a function that reads the policy |
Replacing engine functions
To change what an engine function does, extend Limitr with your own type and declare the policy with it. A function
with the same name as an engine function runs in its place, for every call from your app and from inside the engine,
and it can still call the engine's version with self.allow<Limitr>(...). Here, customers on hold are refused
before the engine looks at the call:
import { Limitr } from '@formata/limitr';
const limitr = await Limitr.new(`
#[type]
Limitr MyLimitr: {
// Customers on hold: refused before the engine looks at the call.
list on_hold: ['bo']
fn allow(id: str!, entitlement: str!, value: float | str = 0,
event: bool | str | obj = true, cap: Cap | map = null,
overhead?: float, force: bool = false) -> bool {
if (self.on_hold.contains(id)) return false;
self.allow<Limitr>(id, entitlement, value, event, cap, overhead, force)
}
}
MyLimitr policy: {
credits: { request: { label: 'Request' } }
plans: {
free: {
default: true
entitlements: { api: { limit: { credit: 'request', value: 2 } } }
}
}
}`);
await limitr.ensureCustomer('ada');
await limitr.ensureCustomer('bo');
const ada = [];
for (let i = 0; i < 3; i++) ada.push(await limitr.allow('ada', 'api', 1));
console.log('ada:', ada.join(' '));
console.log('bo:', await limitr.allow('bo', 'api', 1));
It prints:
ada: true true false
bo: false
Ada gets her two requests from the engine as usual; Bo never reaches it.
- Keep the signature. Copy it from Engine functions. The SDK passes arguments
by position, so a missing parameter shifts the rest (
allowtakes acapbeforeoverhead). - Call the original with
self.<name><Limitr>(...)and add your logic before or after it, instead of rewriting what the engine does. - A function on the policy itself (
policy: { fn allow(...) {...} }) works the same way, for a one-off change. A type keeps your changes in one place and can be shared by several policies. - Prefer the hooks above when one fits. They're designed to be changed; engine functions can change between versions.
- Test it like the rest of the policy (Test a policy).
What every extension shares
- The sandbox. Policy code has no network, files, or environment unless your app allows it, and every call has a time limit (Runtime and sandbox).
- It travels with the policy. Versioned, diffed, and reviewed with your pricing, and published with it in Cloud.
- It fails safely. A cost function that throws falls back to
overhead_cost, and a feature rule that throws counts as off. Errors are logged. - Memory stays flat. Keep what a function creates in a field, or drop it (Write policies in Stof).