Skip to main content
Version: Peki

Functions

A Function is a small piece of TypeScript your agent can call as a tool. Use one when a single API call isn't enough: call several APIs, merge the results, calculate prices or indicators, clean up messy JSON. Models are bad at arithmetic and long JSON; code isn't. Functions run in a locked sandbox that can reach only the sites you allow and never holds your keys.

Function or HTTP tool?​

Use an HTTP tool when…Use a Function when…
One API call answers the question.You need two or more calls, or one call depends on another.
The API takes the inputs as query parameters or a flat JSON body.The API needs a nested body, special encoding or a signature.
The response just needs trimming to a few fields.You need to calculate, filter, sort, convert units or combine data.
You want no code at all.You're fine with code, or the Copilot writes it for you.

Both kinds live together on the Tools page and share one set of names: a Function can't have the same Name the AI calls as an HTTP tool.

Add a Function​

  1. Open the agent and go to Tools.
  2. Below the list, click Function. A Function called New function opens with starter code.
  3. On the Details tab, fill in the title, description, inputs and code, and list the Allowed sites it calls.
  4. If it needs an API key, add it under Keys.
  5. Open the Test tab, write an input and click Run.
  6. Publish the agent. Functions are part of the draft: customers get changes after you publish. The Test panel uses the draft right away.

Each Function card has an Enabled switch. When it's off, the agent isn't offered the Function. It stays in the draft and automations can still run it.

tip

The fastest way to get a working Function is to ask the Copilot: "Add a Function that takes an order number, gets the order from ShopX and the tracking from the carrier, and returns both." It writes the code, runs it with realistic input, reads the result and fixes it. You approve the key on a card; the Copilot never sees its value.

Function settings (Details tab)​

SettingWhat it doesLimit / default
TitleThe name people see in Peki.Required, up to 80 characters
Name the AI callsThe tool name for the model, like shipping_quote.Lowercase letters, digits and _, starting with a letter, up to 64 characters. Unique among tools and Functions.
Description the AI seesWhat it does and when to use it.Required, up to 1,000 characters
InputsWhat the model fills in. Same editor as HTTP tool inputs: name, Text / Number / Yes / no, Required, description, Needs the real personal data.Up to 20
CodeTypeScript, in index.ts.Required, up to 20,000 characters, must have a default export
Needs approval / When rejectedThe call waits for your team's approval first. See Approvals.Off / Tell the customer
Allowed sitesThe only hosts the code can reach.Up to 10
KeysThis agent's keys the code may use, and where each one goes.Up to 10
TimeoutThe longest one run may take: 1, 2, 3, 5, 8 or 10 seconds.5 seconds, at most 10

Writing the code​

The shape of a Function​

A Function is a module whose default export is an async function. It gets the input the model filled in and a ctx object, and returns the result:

export default async (input, ctx) => {
// input: the values the model filled in, checked against your Inputs
// ctx: fetch, log, now (and records / store / trigger in some runs)
return { ok: true };
};

You can write plain JavaScript or add TypeScript types. The editor highlights TypeScript but doesn't check types; the code runs either way.

  • Input. Before the code runs, the input is checked against the Inputs you defined. A wrong type or a missing required input stops the run with Invalid input: <name>: <reason>, and the code never starts.
  • Result. Return any JSON value: an object, an array, a string, a number. Returning nothing gives null. The model reads the result as JSON. Return small, clean objects with clear field names; that's what the model reads best.
  • Errors. Throw an Error to fail the run. The model reads {"error": "<your message>"} and can tell the customer or try other input, so write messages that help: throw new Error('No order 48213 for this email').

What ctx gives you​

MemberWhat it doesAvailable
ctx.fetch(url, init?)Makes an HTTP request to one of the Allowed sites.Always
ctx.log(...values)Writes a log line. console.log, info, warn, error and debug do the same.Always
ctx.now()The current time in milliseconds, like Date.now().Always
ctx.records.find(table, { where, sort, limit }) / ctx.records.save(table, data)Read and save the agent's records.Agents with record tables. See Records.
ctx.store.get/set/delete(key)A small memory that lasts between runs.Automation runs only. See Automations.
ctx.triggerWhat started the run.Automation runs only.

ctx.fetch​

const res = await ctx.fetch('https://api.yourshop.com/v1/orders/48213', {
method: 'GET', // GET (default), POST, PUT, PATCH, DELETE, HEAD
headers: { 'Accept-Language': 'en' },
body: { note: 'hello' }, // a string, or an object that is sent as JSON
secret: { name: 'ShopX API' }, // optional: one of the Function's Keys, by name
});
res.status; // 200
res.ok; // true for 2xx
res.headers.get('content-type');
await res.text(); // the body as text
await res.json(); // the body parsed as JSON
  • When body is an object, it is sent as JSON and content-type: application/json is set for you.
  • A non-2xx answer doesn't throw. Check res.ok or res.status yourself.
  • A refused request throws an Error like ctx.fetch https://… failed: <reason>. See Errors.
  • Redirects are not followed: a 301 or 302 comes back as that status. Call the final URL.
  • The global fetch can't reach anything. Always use ctx.fetch.

Helpers you can import​

Imports are blocked, except for two bundled helpers:

import { sma, ema, rsi } from 'ta';         // moving averages and RSI over number arrays
import { encodeBase64 } from '@std/encoding'; // base64, hex and other encodings
  • sma(values, period) and ema(values, period) return moving averages.
  • rsi(closes, period = 14) returns RSI values; rsi(closes).at(-1) is the latest.
  • @std/encoding gives the standard encoders and decoders (base64, base64url, hex and more).

Any other import fails, including npm packages and URLs. Standard JavaScript built-ins such as JSON, Math, Date, URL, Intl, TextEncoder and structuredClone are available.

What the code can't do​

  • Reach any site that isn't in Allowed sites, or any private network.
  • See a key's value.
  • Read or write files, read environment settings, start other programs, or load code from the internet.
  • Keep anything between runs (except ctx.store in automations). Each run starts fresh.
  • Run longer than its Timeout.

Network rules (egress)​

Every ctx.fetch goes through Peki's network proxy, which enforces these rules. They are security limits and can't be turned off.

  • Allowed sites only. The host must match one of the Function's Allowed sites, or the call fails with api.other.com is not in this function's hosts (…): add it to the function's hosts. Enter host names only, like api.yourshop.com; Peki removes https:// and paths you paste. *.yourshop.com allows every subdomain (api.yourshop.com, cdn.yourshop.com) but not yourshop.com itself; add both if you need both.
  • Only http and https.
  • No private or local addresses. The same ranges as HTTP tools are blocked, checked after DNS, so a host name can't point at an internal service.
  • At most 20 requests per run. The 21st fails with A function run may make at most 20 requests.
  • 1 MB per response and per request body.
  • Time. A request may only use the time left in the run.

Keys in Functions​

The code never holds a key. It only names one; the proxy adds the value on the way out.

  1. Under Keys, click Add key, then choose one of the agent's keys or + Add a new key.
  2. Choose Where the key goes:
    • Header, empty name: sent as Authorization: Bearer <key> (Authorization: Bearer).
    • Header with a name, like X-API-Key: sent as that header with the raw key.
    • Query with a parameter name, like api_key: added to the URL as ?api_key=<key>.
  3. In the code, ask for it by its name: ctx.fetch(url, { secret: { name: 'ShopX API' } }).

The proxy attaches a key only when all of these hold:

  • the key is in the Function's Keys;
  • the URL's host is in the Function's Allowed sites and in the key's own allowed sites;
  • the URL uses https.

More rules that protect your keys:

  • Keys must be at least 8 characters. Shorter keys can't be found reliably in responses, so they are refused: This key is too short to protect; use the HTTP tool instead.
  • A response that contains one of the run's keys is withheld with an error, even if that request didn't carry a key. APIs that echo keys back in errors would otherwise leak them to the code.
  • When a Function uses keys, its responses must be uncompressed UTF-8 text. Peki asks servers not to compress. A response it can't check (compressed or in another charset) is withheld.
  • Some headers can't carry a key or be set by code: host, connection, content-length, transfer-encoding and other connection headers.
  • Webhook signing keys are never sent. They only check incoming signatures.
note

Because the code never receives a key's value, a Function can't compute a signature (like an HMAC) with a key. Use an API that accepts the key in a header or query parameter.

See Variables and secrets for adding keys and their allowed sites.

Limits​

LimitValue
Timeout per run1–10 seconds, default 5
Memory per runAbout 256 MB
Result16 KB (as JSON)
Logs per run8 KB; later lines are dropped
Outbound requests per run20
Response size per request1 MB
Code size20,000 characters
Runs per agent per minute60 by default
Runs at the same time per agent4 by default
Run time per agent per day3,600 seconds by default, reset at midnight UTC
Functions per agent30

How the daily budget works: a run reserves its full Timeout before it starts and gives back what it didn't use when it ends. If the reservation doesn't fit in what's left today, the run is refused. Keep timeouts close to what the Function really needs.

Inside a customer reply, a Function also has to fit in the reply's own time: Peki keeps about 8 seconds for the model to write the answer after the last Function call. If there isn't enough time left, the call fails with Not enough time left in this reply to run the function.

The model reads at most 16 KB of a result and 4 KB of an error.

Personal data​

Functions handle personal data like HTTP tools. With personal-data protection on, the model fills inputs with placeholders like [PHONE_1]. Inputs marked Needs the real personal data get the real value; the others keep the placeholder. Personal data in the result is turned into placeholders before the model reads it, and run records keep placeholders too. See Guardrails and privacy.

Test a Function​

Open the card's Test tab.

  1. Write the Input as a JSON object. The editor starts with every input and an empty value, for example {"weight_kg": 0, "city": ""}.
  2. Click Run or press ⌘ Enter (Ctrl Enter on Windows).
  3. Read the output:
    • Success with the Result, or Failed with the Error.
    • Logs: everything the code logged.
    • Requests: every ctx.fetch with its Host, Status and Time. A refused request shows status 0.
    • The run's duration.

Things to know about test runs:

  • Run saves the draft first, then runs the saved draft. If the draft has problems, fix them first: Fix the problems above so the draft can be saved, then run again.
  • Test runs are real. They use your real keys and may send POST, PUT and DELETE requests, which can change data in your systems. Use test data where you can.
  • ctx.records.save in a test run creates or updates test records only, never live ones.
  • Only editors, admins and owners can run tests: Only editors can run a test.
  • Keys are added by the proxy. Code never sees their values.

Runs and logs​

The Runs tab lists the Last 50 runs of the Function, newest first, from every source: agent (customer conversations), test, Copilot and automation, with who started it. For each run you see the time, the source, ok or error, and the Duration. Open a run to see its input, result or error, logs and requests. Open in Test copies its input to the Test tab so you can run it again.

Use Show → Errors to list only failed runs.

Run records are kept for 30 days. Inputs, results and logs are stored capped, with keys masked and personal data as placeholders.

Errors​

ErrorCauseWhat to do
Invalid input: <name>: …The input doesn't match the Function's Inputs.Fix the input types, or describe the format in the input's description.
<Name>: <message> (your own error)The code threw.Read Logs and the message.
Timed out after 5000 msThe run took longer than Timeout.Raise the timeout (max 10 s) or make fewer or faster calls.
Result is 24 KB; the limit is 16 KBThe result is too large.Return only the fields the model needs.
Memory limit exceeded…The run used too much memory.Process less data at once.
CPU limit exceeded…The code calculated for too long.Simplify the calculation.
ctx.fetch … failed: api.other.com is not in this function's hosts…The host isn't allowed.Add it to Allowed sites.
ctx.fetch … failed: A function run may make at most 20 requestsToo many calls.Fetch in bulk, or split the work.
ctx.fetch … failed: The secret ShopX API may only be sent to …The URL isn't one of the key's allowed sites.Use the right host, or ask an admin to change the key's sites.
ctx.fetch … failed: The secret … is only sent over httpsThe URL uses http.Use https.
ctx.fetch … failed: The secret … is not in this function's secretsThe code names a key that isn't under Keys.Add it under Keys, or fix the name.
ctx.fetch … failed: …response withheld: it contained a keyThe API echoed a key back.Use another endpoint, or report it to the API's owner.
This key is too short to protect; use the HTTP tool insteadA key under Keys has fewer than 8 characters.Use a longer key, or an HTTP tool.
This agent ran its functions 60 times in the last minute…The per-minute limit.Wait a minute.
Too many Functions running for this agent right nowToo many runs at once.Try again shortly.
This agent used its 3600 seconds of function run time for today…The daily budget is used up.Wait until midnight UTC, or lower timeouts.
ctx.store is only available in automation runsctx.store in a conversation or test.Use it only in automations.
ctx.records is only available to agents with tablesThe agent has no record tables.Add a table, or remove the call.
The code must export default an async function…No default export.Start the code with export default async (input, ctx) => ….
Functions are unavailable right now; try again shortlyA temporary problem on our side.Try again.

Examples​

Shipping quote (no network)​

Inputs: weight_kg (Number, required), zone (Text, required: "domestic, eu or world"). No allowed sites needed.

const RATES = { domestic: 4.5, eu: 9, world: 18 }; // USD for the first kg
const PER_EXTRA_KG = { domestic: 1, eu: 2.5, world: 5 };

export default async ({ weight_kg, zone }: { weight_kg: number; zone: string }) => {
if (!(zone in RATES)) throw new Error('zone must be domestic, eu or world');
if (weight_kg <= 0 || weight_kg > 30) throw new Error('We ship parcels from 0.1 to 30 kg');
const extra = Math.max(0, Math.ceil(weight_kg) - 1);
const price = RATES[zone] + extra * PER_EXTRA_KG[zone];
return { zone, weight_kg, price_usd: Math.round(price * 100) / 100, free_over_usd: 75 };
};

Order with tracking (two APIs, keys)​

Inputs: order_id (Text, required). Allowed sites: api.yourshop.com, api.carrier.example. Keys: "ShopX API" (Header, empty name → Bearer), "Carrier API" (Query, api_key).

export default async ({ order_id }: { order_id: string }, ctx) => {
const orderRes = await ctx.fetch(`https://api.yourshop.com/v1/orders/${encodeURIComponent(order_id)}`, {
secret: { name: 'ShopX API' },
});
if (orderRes.status === 404) throw new Error(`No order ${order_id}`);
if (!orderRes.ok) throw new Error(`Shop API answered ${orderRes.status}`);
const order = await orderRes.json();
ctx.log('order status', order.status);

if (!order.tracking_number) return { order_id, status: order.status, tracking: null };

const trackRes = await ctx.fetch(`https://api.carrier.example/track/${order.tracking_number}`, {
secret: { name: 'Carrier API' },
});
const track = trackRes.ok ? await trackRes.json() : null;
return {
order_id,
status: order.status,
items: order.items.map((i: { name: string; qty: number }) => `${i.qty} × ${i.name}`),
carrier_status: track?.status ?? 'unknown',
last_scan: track?.events?.[0]?.location ?? null,
estimated_delivery: track?.eta ?? null,
};
};

Crypto signal with RSI (helpers)​

Inputs: symbol (Text, required: "a trading pair like BTCUSDT"). Allowed sites: api.binance.com.

import { rsi, sma } from 'ta';

export default async ({ symbol }: { symbol: string }, ctx) => {
const url = `https://api.binance.com/api/v3/klines?symbol=${encodeURIComponent(symbol.toUpperCase())}&interval=1h&limit=100`;
const res = await ctx.fetch(url);
if (!res.ok) throw new Error(`Unknown symbol ${symbol}`);
const closes = (await res.json()).map((k: string[]) => Number(k[4]));
const latest = rsi(closes).at(-1)!;
return {
symbol,
price: closes.at(-1),
rsi14_1h: Math.round(latest * 10) / 10,
sma20: Math.round(sma(closes, 20).at(-1)! * 100) / 100,
signal: latest < 30 ? 'oversold' : latest > 70 ? 'overbought' : 'neutral',
};
};

Find a customer's open records (records)​

For an agent with a leads table. Inputs: phone (Text, required, Needs the real personal data). No allowed sites needed.

export default async ({ phone }: { phone: string }, ctx) => {
const { records, more } = await ctx.records.find('leads', {
where: [{ field: 'phone', op: 'eq', value: phone }],
limit: 5,
});
ctx.log('found', records.length, more ? '(more)' : '');
return records.map((r: { id: string; title: string; data: Record<string, unknown> }) => ({
id: r.id,
title: r.title,
status: r.data.status ?? 'new',
}));
};

where is a list of up to 10 conditions (field, op: eq, in, range or contains, with value, values or min/max); sort takes a field and asc or desc. Use your table's own field keys. ctx.records.find returns { records: [{ id, title, data }], more } with at most 20 rows (10 by default); ctx.records.save(table, data) returns { saved, created }. A run may make up to 20 finds and 50 saves. See Records.