Skip to main content

Call your app from a policy

Policy functions run in a sandbox with no network, files, or environment. When a policy needs to reach your app (flag a customer, record usage, page someone), your app gives it functions to call.

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

const limitr = await Limitr.new(`
policy: {
credits: {
ai_token: { label: 'AI token', price: { amount: 0.00002 } }
}
plans: {
pro: {
default: true
entitlements: {
chat: {
limit: {
credit: 'ai_token', mode: 'soft', value: 100_000
resets: true
}
}
}
}
}
notifications: {
heavy_user: {
fn matches(type: str, event: obj) -> bool {
type == 'meter-overage' && (event.meter.value ?? 0) > 150_000
}
// Calls your app. App?:: does nothing if the app didn't register the function.
fn fire(event: obj) {
App?::flag_customer(event.customer.id, 'heavy_user', event.meter.value);
}
}
}
}

// Runs for every meter-overage event, wherever it is in the document.
#[meter-overage]
fn report_overage(event: obj) {
App?::report_usage(event.customer.id, event.entitlement, event.overage_price);
}
`);

// Host functions the policy can call. Arguments arrive as plain values.
limitr.doc.lib('App', 'flag_customer', (id: string, flag: string, used: number) => {
console.log(`flag ${id} as ${flag} (${used} tokens)`);
});
limitr.doc.lib('App', 'report_usage', (id: string, entitlement: string, amount: number) => {
console.log(`usage for ${id}: ${entitlement} $${amount}`);
});

await limitr.ensureCustomer('ada');
for (const tokens of [90_000, 40_000, 40_000]) await limitr.allow('ada', 'chat', tokens);

It prints:

usage for ada: chat $0.6
flag ada as heavy_user (170000 tokens)
usage for ada: chat $0.8

Host functions​

limitr.doc.lib(library, name, fn) registers a function the policy can call as Library::name(...). Use the App library for your app's functions, or any name you like.

  • App?::name(...) calls the function if it's registered and does nothing if it isn't, so the same policy runs with or without your app (in tests, on another server, in Cloud).
  • Arguments are plain values. Objects arrive as objects; pass stringify('json', obj) for JSON.
  • Async functions work. A notification's fire can be async and await them.
  • Keep them out of the hot path. Call host functions from notifications and event handlers, not from cost functions or feature rules, which run on every call and should stay pure.

Where to call them​

FromRuns
A notification's fireWhen its matches returns true. See Notifications.
A function with an event attribute (#[meter-overage])For every event of that name.
A capabilityWhen it's run.

App hooks​

Besides the general event handler the SDK registers (behind addHandler), the engine calls these App functions for you when they're registered, each with the event as JSON:

App::meter_changed, App::meter_overage, App::meter_limit, App::meter_governed, App::cap_limit, App::customer_set, App::customer_removing, App::plan_set, and App::plan_removed.

limitr.doc.lib('App', 'meter_overage', (json: string) => queue.push(JSON.parse(json)));

addHandler is simpler for most apps; the hooks suit apps that want one function per event.

HTTP from a policy​

A policy can call HTTP APIs with Stof's Http library once your app allows the hosts:

limitr.doc.allowHttp(['hooks.example.com']);
async fn fire(event: obj) {
using body = new { customer: event.customer.id, used: event.meter.value };
await Http::fetch('https://hooks.example.com/limitr', 'post', stringify('json', body));
}

A Limitr.new policy has no network access until you allow it, and a Cloud policy can only reach Limitr Cloud unless you pass httpHosts. Prefer host functions when the code is yours: they're faster and easier to test.

See Runtime and sandbox.