A custom action is a short async JavaScript function that the agent can call. Reach for one when the provider you want isn't in the catalogue, or when you want to combine a few API calls into a single named step.
You write the body of the function. Chatzuri wraps it, injects the parameters the agent supplied, decrypts your credential, runs it with a 10-second timeout, and returns the result to the agent.
The five-minute version
Open the action builder
Name it and describe what it does
Declare parameters
Pick a credential (optional)
Write the function body
params, credentials, and config. See the contract below.Test it
The execution contract
The executor wraps your code in roughly this shape:
new Function(
'params', 'credentials', 'config',
`return (async () => { ${YOUR_CODE} })()`,
);That means three variables are always in scope:
params— the input fields the agent extracted from the conversation. Access them asparams.fieldName.credentials— the decrypted credential, ornullif you didn't bind one. Field names depend on the credential type (see below).config— any agent-specific static config you stored when installing the action.
Your code must return a JSON-serialisable value. The convention is { success: boolean, data?: any, error?: string }, but anything serializable works.
Credential field names
Which fields credentials exposes depends on the credential type:
- http_api_key →
credentials.api_key - http_bearer_token →
credentials.bearer_token - http_basic_auth →
credentials.username,credentials.password,credentials.api_url - webhook →
credentials.webhook_url,credentials.secret(optional) - Provider-specific credentials (Notion, Stripe, etc.) → the same field names the built-in tool would receive (e.g.
credentials.access_tokenfor Notion,credentials.secret_keyfor Stripe).
Minimal example: hello-world
return {
success: true,
data: {
message: `Hello, ${params.name || 'world'}!`,
timestamp: new Date().toISOString(),
},
};Realistic example: OpenWeather
Parameters: city (string, required), units (string, optional). Credential: http_api_key.
const apiKey = credentials.api_key;
if (!apiKey) throw new Error('OpenWeather api_key required');
const city = encodeURIComponent(params.city);
const units = params.units || 'metric';
const r = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${units}&appid=${apiKey}`
);
if (!r.ok) throw new Error(`OpenWeather ${r.status}: ${await r.text()}`);
const d = await r.json();
return {
success: true,
data: {
city: d.name,
country: d.sys?.country,
temp: d.main?.temp,
description: d.weather?.[0]?.description,
},
};What's available at runtime
Standard Web/Node APIs are available:
fetch,URL,URLSearchParamsJSON,Date,MathBuffer,setTimeout,setInterval
What's banned
The save-time and run-time guardrails reject code containing:
require(orimport((no module loading)process/child_process(no shell)fs(no file system)eval(ornew Function((no dynamic code generation)
The 10-second timeout
If your code hasn't resolved after 10 seconds the action returns an error to the agent. Long-running work (e.g. video encode, large report generation) belongs in an Agent Task or a background queue, not a live action.
Ready-made templates
The action builder ships with templates you can fork:
- Get Weather (OpenWeather) — http_api_key
- Post to Slack (Incoming Webhook) — webhook
- Convert Currency (Frankfurter) — no credential
- Geocode Address (OpenStreetMap) — no credential
- Shorten URL (TinyURL) — no credential
- Append Note to CRM (Webhook) — webhook
- Track Shipment (AfterShip) — http_api_key
- Create Zendesk Ticket (REST) — http_basic_auth
- Fetch Mixpanel Metric (JQL) — http_basic_auth
- Create Stripe Payment Link — http_bearer_token
How the agent sees your action
The model sees the action's name, description, and the parameters you declared (each one's name, type, required flag, and description). It never sees the code or the credential. Write clear names and descriptions — that's how the agent decides whether to call your action versus a built-in one.
