Skip to main content

Capabilities

Named, callable units of policy logic.

Capabilities are named, callable units of policy logic with defined input parameters and a structured output. They let you expose policy-aware functions as Claude tool definitions — Limitr generates the tool schema from the capability definition, executes the logic when Claude invokes the tool, and returns a formatted tool_result object ready to pass back to the API.

Capabilities are also useful independently of Claude: runCapability() calls any capability directly from TypeScript with a plain argument map.


The core concept

A capability has:

  • A name and description (used in tool definitions)
  • Parameters with types and descriptions (become the tool's input_schema)
  • One or more #[run] Stof pipeline stages that execute
  • A result field name that maps to the output

When claudeToolUse() is called with a tool_use block from Claude, Limitr finds the matching capability by name, injects the inputs, runs the pipeline, and returns a tool_result object with the serialized output.


Defining a capability

Capabilities are defined via setCapabilities() as a Stof string, or inline in a native Stof policy file under capabilities:.

Basic capability with #[run]

await policy.setCapabilities(`
area: {
version: 0.1.0
description: 'Calculate the area of a rectangle'
parameters: [
{ name: 'width', description: 'Width of the rectangle', schema_type: 'number' }
{ name: 'height', description: 'Height of the rectangle', schema_type: 'number' }
]
result: 'area'

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

self.input contains the input parameters injected by claudeToolUse(). self.output is where you write results. The result field names the output field that gets serialized into the tool_result content.

Multi-stage pipeline with #[run(N)]

Stages run in ascending numeric order:

await policy.setCapabilities(`
area: {
version: 0.1.0
description: 'Get the area of a shape from a Stof generator'
parameters: [
{ name: 'shape', description: 'Stof that generates a shape with an area() function' }
{ name: 'units', description: 'Output units for the area (e.g. "m", "ft", "in")' }
]
result: 'area'

#[run(0)]
fn setup() {
const shape = new {} on self.input;
parse(self.input.shape, shape, 'stof');
self.input.shape = shape.gen();
self.input.units = self.input.units as str ?? 'float';
}

#[run(1)]
fn calculate() {
self.output.area = self.input.shape.area().to_units(self.input.units).round(2);
}
}
`);

parse(content, target, format) parses a string into an existing object. to_units() converts a value to the target unit. round(n) rounds to n decimal places.

Typed capability with #[extends]

For capabilities that share common behavior, define a base type and extend it. The GetEndpoint pattern builds a reusable HTTP GET capability:

// Define the base type in the document
policy.doc.parse(`
#[type]
#[extends('Capability')]
GetEndpoint: {
str endpoint: '';
list query: [];

#[run]
fn get_request() {
const query = new {};
for (const q in self.query) {
const v = self.input.get(q);
if (v != null) query.insert(q, v);
}

let endpoint = self.endpoint;
if (query.len() > 0) endpoint += '?' + stringify('urlencoded', query);
drop(query);

const res = await Http.fetch(endpoint);
self.set_result(res);
}
}
`);

// Instantiate capabilities that extend it
await policy.setCapabilities(`
GetEndpoint weather-forecast: {
version: 0.1.0
description: 'Get a weather forecast for a location'
parameters: [
{ name: 'latitude', description: 'Latitude', schema_type: 'number' }
{ name: 'longitude', description: 'Longitude', schema_type: 'number' }
]
endpoint: 'https://api.weather.example.com/forecast'
query: ['latitude', 'longitude']
}

GetEndpoint exchange-rate: {
version: 0.1.0
description: 'Get the current exchange rate between two currencies'
parameters: [
{ name: 'from', description: 'Source currency code', schema_type: 'string' }
{ name: 'to', description: 'Target currency code', schema_type: 'string' }
]
endpoint: 'https://api.fx.example.com/rate'
query: ['from', 'to']
}
`);

GetEndpoint weather-forecast instantiates a GetEndpoint-typed object named weather-forecast. The #[run] logic from GetEndpoint runs for both capabilities — each gets its own endpoint and query configuration.

set_result(v) is a Capability method that writes to the named output field. self.input.get(key) reads an input parameter by name.


Using capabilities with Claude

Getting tool definitions

claudeTools() returns an array of Claude-compatible tool definitions generated from all capabilities in the policy:

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();
const tools = await policy.claudeTools();

const response = await client.messages.create({
model: 'claude-opus-4-5',
max_tokens: 1024,
tools,
messages: [{ role: 'user', content: 'What is the area of a 3ft by 4ft rectangle?' }],
});

To get tools for a specific customer — respecting plans and customers access filters on the capability:

const tools = await policy.claudeTools('user_abc');

Handling tool use

When Claude responds with a tool_use block, pass it to claudeToolUse():

for (const block of response.content) {
if (block.type === 'tool_use') {
const toolResult = await policy.claudeToolUse(block);
// { type: 'tool_result', tool_use_id: '...', content: '12' }
}
}

Full conversation loop

async function runWithTools(userMessage: string, customerId?: string) {
const tools = await policy.claudeTools(customerId);
const messages: Anthropic.MessageParam[] = [
{ role: 'user', content: userMessage }
];

while (true) {
const response = await client.messages.create({
model: 'claude-opus-4-5',
max_tokens: 1024,
tools,
messages,
});

const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === 'tool_use') {
const result = await policy.claudeToolUse(block, customerId);
if (result) toolResults.push(result as Anthropic.ToolResultBlockParam);
}
}

if (toolResults.length === 0 || response.stop_reason === 'end_turn') {
const text = response.content.find(b => b.type === 'text');
return text?.type === 'text' ? text.text : '';
}

messages.push({ role: 'assistant', content: response.content });
messages.push({ role: 'user', content: toolResults });
}
}

const answer = await runWithTools('What is the area of a 3ft by 4ft rectangle in square meters?');
console.log(answer);

Calling capabilities directly

runCapability() executes a capability without going through Claude — useful for testing, batch processing, or any case where you want the capability logic without an LLM in the loop:

const result = await policy.runCapability('area', { width: 3, height: 4 });
console.log(result); // { area: 12 }

// With a customer ID — respects access filters
const result = await policy.runCapability('weather-forecast', {
latitude: 42.36,
longitude: -71.06,
}, 'user_abc');

Capability access control

Capabilities can be restricted to specific plans or customer IDs using plans and customers fields:

await policy.setCapabilities(`
premium-analysis: {
version: 0.1.0
description: 'Advanced data analysis — Growth and Enterprise only'
plans: ['growth', 'enterprise']
parameters: [
{ name: 'dataset', description: 'Dataset to analyze', schema_type: 'string' }
]
result: 'analysis'

#[run]
fn analyze() {
self.output.analysis = 'Analysis result for: ' + self.input.dataset;
}
}
`);

// claudeTools() and claudeToolUse() both respect plan/customer filters
// when a customerId is passed
const tools = await policy.claudeTools('user_abc');

Registering host functions for capabilities

Capabilities run inside the Stof engine. To call TypeScript from within a #[run] function, register a library function with doc.lib():

// Register fetch for HTTP capabilities
policy.doc.lib('Http', 'fetch', async (url: string) => {
const response = await fetch(url);
return await response.text();
});

// Register standard output
policy.doc.lib('Std', 'pln', (...args: unknown[]) => console.log(...args));

These are available to all capabilities and Stof functions in the document. Http.fetch is what makes GetEndpoint-style capabilities actually execute HTTP requests — without it, Http.fetch is an optional call that silently no-ops.

See Embedded Event Handlers for more on doc.lib().


Capabilities in Limitr Cloud

In Limitr Cloud, capabilities defined in your policy are available as part of Limitr Network — callable by authorized agents and other Limitr-connected services without direct SDK integration. The same capability definition works both locally (via claudeToolUse()) and over the network (via Cloud routing).

The local SDK is the right place to develop and test capabilities. Once they're working, publishing the policy to Cloud makes them available network-wide.