Skip to main content

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 toUse
Price a call from its own data (model, tokens, region)A cost function on the credit
Turn a feature on per customer or per callA feature rule (enabled) on the entitlement
React to usage: alerts, upsells, syncingNotifications, or a function named for the event (#[meter-overage])
Reach your app from the policyHost functions your app registers
A named operation for your app, your services, or AI agentsA capability
Bring outside data in: rates, customers, settingsA capability that fetches from an allowed host
Share logic across credits, plans, and featuresFunctions in a type
Ask a question the API doesn't answercallSource 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 (allow takes a cap before overhead).
  • 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).