Tools
A tool is a small JavaScript module the model can call during a conversation: book an appointment, create a lead, look up an order, search the knowledge base. This chapter covers where tools come from, the exact shape a tool file must have, what LiteBot hands to a tool when it runs, timeouts and caching, and how to create and test tools through the dashboard and the API. Read it before you write your first tool, and keep the checklist at hand for every tool you ship.
Tools that run automatically when a conversation ends are covered in Session-end hooks.
How a tool is used
- A bot lists tool names in its
toolsarray (Bot configuration reference). - Each turn, LiteBot sends those tools'
name,descriptionandparametersto the model. - When the model calls one, LiteBot validates the arguments, runs
execute(params, context)and gives the result back to the model, which writes the reply or calls another tool.
The model decides when to call a tool from the description alone, so say plainly when to call it and when not to.
An empty tools array does not mean "no tools": the bot is then offered every tool visible to its tenant. List tools explicitly on every bot.
Where tools come from
| Source | Location | Who writes it |
|---|---|---|
| Tenant DB tool | Mongo tools collection, tenantId = the tenant |
Tenant users with tool permissions |
| Global DB tool | Mongo tools collection, tenantId = * |
Global admin |
| Disk tool | config/tools/*.js beside the binary |
Global admin, by file or API |
| Bundled tool | src/tools/*.js beside the binary, if that folder exists |
Shipped with a release |
When a bot calls a tool by name, LiteBot looks in this order and uses the first match: the bot's tenant DB tool, then the global DB tool, then the disk tool. A tenant can therefore override a global tool by creating its own with the same name. A tenant sees global tools (disk and * DB) only when they are on its shared-tools allowlist (Users, roles and tenants).
Disk loading rules:
- Every
.jsfile inconfig/tools/is loaded, exceptindex.js, files starting withtemplateand files starting with a dot. A copy such asbookAppointment-old.jsis live and can shadow the real tool, so keep backups outside the folder. - Bundled tools load first; a disk tool with the same name replaces it.
- A file may export one tool, or
{ tools: [ ... ] }with several.
The tool file
A tool exports exactly these keys and nothing else:
| Export | Required | Meaning |
|---|---|---|
name |
yes | Tool name the model calls. Letters, digits and _, starting with a letter |
description |
yes | When to call the tool, for the model |
parameters |
yes | OpenAI-style JSON Schema: { type: 'object', properties, required } |
execute |
yes | async (params, context) => result |
metadata |
no | Free-form object; LiteBot reads timeout, cacheable, cacheTTL and category |
onError |
no | async (error, params, context) => result, called when validation fails or execute throws |
Any other top-level key, such as WEBHOOK_URL: '...', fails validation and the tool is not registered. The only trace is an error line in the log, "Invalid tool configuration for const lines above module.exports, and put display-only values in metadata.
parameters is the single schema. LiteBot converts it to a validator automatically, so you do not write one yourself:
- Types
string,number,integer,boolean,arrayandobjectare checked, along withenum,pattern,format: 'email',minLengthandmaxLength. - Strings may be empty, because models often send
""for optional values. - Arguments that are not declared in
propertiesare removed beforeexecutesees them. - Every name in
requiredmust also be inproperties. Gemini rejects a schema that breaks this, and every request from that bot fails.
What execute receives
params holds the validated arguments. context holds the following.
| Field | Content |
|---|---|
context.sessionId |
Conversation id |
context.botId |
Calling bot |
context.tenantId |
Bot's tenant |
context.userId |
User id sent with the chat request (on WhatsApp-style integrations, often the sender's number) |
context.iteration |
Which tool round this is within the turn (chat only) |
context.sessionContext |
The session's context object (values from the chat request and earlier updateContext calls) |
context.updateContext(updates) |
Merges updates into the session context. Can be null, so check before calling |
context.testMode |
true when run from the test endpoint or the dashboard Test button |
context.modules |
Libraries, listed below |
Session-end hooks add transcript, messageCount and sessionEndReason (Session-end hooks).
context.modules depends on where the tool lives.
| Module | Disk and global DB tools | Tenant DB tools |
|---|---|---|
axios |
yes | yes |
crypto |
yes | yes |
logger |
yes | yes |
llm (llm.chat(messages, options)) |
yes | yes |
knowledgeBase |
yes | yes, limited to the tenant's own and global collections |
db, embeddings, vectorDb |
yes | no |
llm.chat calls the provider manager with the bot's tenant key (if any) and no response cache. Use it for summaries or extraction. knowledgeBase.searchDocuments(query, { collection, limit }) searches a collection (Knowledge base). When the calling bot belongs to a tenant, a disk tool's knowledgeBase is also limited to that tenant's collections.
Rules for the binary
LiteBot runs as a single pkg binary. That shapes what tool code may do.
- No
require()of npm packages. The binary cannot resolvenode_modulesfor files on disk, sorequire('axios')in a disk tool fails at load. Usecontext.modules.axios. Node built-ins such ascryptoload, but prefer the injected modules. - No
process.env. Put URLs, sheet ids and secrets as constants at the top of the file. A changed constant takes effect when the tool is reloaded; a changed environment needs a restart and leaks deployment settings into tool logic. - Tenant DB tools run in a sandbox with no
process,fsorglobal. Theirrequireaccepts onlyaxios,joi,crypto,https,http,url,querystring,uuidandcrypto-js; anything else fails with "require('') is not allowed in DB tools". Loading the file must finish within 1 second. The sandbox protects against mistakes, not hostile code, so only trusted people should get tool permissions.
Return values
Return an object with success and a message from every branch, and do not throw for expected failures.
return { success: true, message: 'Lead saved', leadId: 'L-1042' };
return { success: false, message: 'The CRM did not confirm the lead. Offer to take a message instead.' };
- LiteBot wraps whatever
executereturns:{ success: true, result: <your object>, toolName, executionTime }. That outersuccessonly meansexecutereturned; your ownsuccessinsideresultis what the model reads. - If
executethrows, LiteBot callsonErrorif present, otherwise returns{ success: false, error }with the exception text. Thrown errors also count against the tool's circuit breaker. - Base
success: trueon proof the action happened: a booking number, a row id, a reference. An HTTP 200 with an error page in the body is not a booking, and the model will confirm it to the caller. - Keep results small. Everything you return goes back to the model as input tokens.
Timeouts and caching
| Setting | Default | Meaning |
|---|---|---|
metadata.timeout |
none | Milliseconds allowed for this tool |
TOOL_EXECUTION_TIMEOUT |
5000 | Timeout for tools without metadata.timeout |
metadata.cacheable |
false | true caches results per tool and arguments |
metadata.cacheTTL |
TOOL_CACHE_DEFAULT_TTL |
Cache lifetime in seconds |
TOOL_CACHE_DEFAULT_TTL |
300 | Default cache lifetime |
ENABLE_TOOL_CACHE |
true | false turns the tool cache off |
TOOL_CACHE_MAX_SIZE |
500 | Entries kept in memory |
The timeout is enforced by the tool's circuit breaker, so three conditions apply:
- It only exists when
ENABLE_CIRCUIT_BREAKER=true. Without it, a tool runs until its own HTTP calls give up. CIRCUIT_BREAKER_TIMEOUT, when set, overrides bothmetadata.timeoutandTOOL_EXECUTION_TIMEOUTfor every tool..env.examplesets it to 30000. This is a known gap; see LLM providers.- The breaker is created on the tool's first run. Changing
metadata.timeoutlater takes effect after a restart.
When a tool times out, the model is told it failed, but the webhook may still complete in the background. Set the timeout above the slowest real response; a tool that calls llm.chat and then a webhook needs 20 to 25 seconds.
Only results with metadata.cacheable: true are cached, in Redis when available and in memory always. Editing the tool's code or metadata changes the cache key, so stale results are not served after an edit. Never cache tools that write (bookings, leads).
Creating and changing tools
In the dashboard, open Tools, write or paste the source, validate, save, then use Test. Where a new tool is stored depends on who saves it:
- A tenant user: a DB tool in that tenant.
- A global admin who sends
tenantId(a tenant id, or*for global): a DB tool in that scope. - A global admin without
tenantId: a fileconfig/tools/<name>.js.
Run the binary with its own folder as the working directory. The API writes new disk tools under the working directory, while the binary loads them from the config/tools folder beside itself; if the two differ, a saved tool does not appear.
| Method and path | What it does |
|---|---|
GET /api/tools |
Tools in your scope |
GET /api/tools/:name |
One tool, with source when you may edit it |
POST /api/tools/validate-source |
Check { "sourceCode": "..." } without saving |
POST /api/tools |
Create from sourceCode |
PUT /api/tools/:name |
Replace the source |
DELETE /api/tools/:name |
Remove the DB tool or disk file |
POST /api/tools/:name/test |
Run execute with { "parameters": {...}, "context": {...} } and testMode: true |
POST /api/tools/reload |
Re-scan tools (refused when NODE_ENV=production) |
curl -s -X POST http://127.0.0.1:3345/api/tools/createLead/test \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"parameters":{"name":"Asha","phone":"9800000000","interest":"demo"}}'
{ "success": true, "data": { "success": true, "toolName": "createLead", "executionTime": 412,
"result": { "success": true, "message": "Test mode: lead not saved", "testMode": true } } }
Tenant users may test only their own DB tools; testing a global tool needs a global admin. The test runs execute for real, so check context.testMode before writing to production systems.
When changes take effect
- DB tools are loaded at start and polled every
TOOLS_DB_POLL_MS(default 15000;0or less turns polling off). Saves through the API apply at once on that node; other nodes pick them up on the next poll. Every fourth poll is a full reload, which also catches deletions. - Disk tools saved through the API or dashboard load at once. A file you copy or edit by hand is picked up by the file watcher only when
NODE_ENVis notproduction; in production, restart the process after a hand edit. After any change, look forCreated new tool,DB tool upsertedorHot-reloaded tools fromin the log, and forInvalid tool configurationif the tool does not appear.
A complete example
// config/tools/createLead.js
const CRM_URL = 'https://crm.example.com/api/leads';
const CRM_TOKEN = 'replace-me';
module.exports = {
name: 'createLead',
description: 'Save a sales lead once the caller has given their name, phone number and what they are interested in. Do not call it for support questions.',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: 'Caller name' },
phone: { type: 'string', description: 'Ten-digit phone number' },
interest: { type: 'string', description: 'Product or service of interest' }
},
required: ['name', 'phone']
},
metadata: { category: 'sales', timeout: 10000 },
async execute(params, context) {
const { axios, logger } = context.modules;
const phone = String(params.phone || '').replace(/\D/g, '').slice(-10);
if (phone.length !== 10) {
return { success: false, message: 'The phone number is incomplete. Ask the caller to repeat it.' };
}
if (context.testMode) {
return { success: true, message: 'Test mode: lead not saved', testMode: true };
}
try {
const res = await axios.post(CRM_URL,
{ name: params.name, phone, interest: params.interest || '', source: context.botId },
{ headers: { Authorization: `Bearer ${CRM_TOKEN}` }, timeout: 8000 });
const leadId = res.data && res.data.id;
if (!leadId) {
logger.warn('createLead: no id in CRM reply', { sessionId: context.sessionId, status: res.status });
return { success: false, message: 'The CRM did not confirm the lead. Offer to take a message instead.' };
}
if (context.updateContext) await context.updateContext({ leadId });
return { success: true, message: 'Lead saved', leadId };
} catch (error) {
logger.error('createLead failed', { sessionId: context.sessionId, error: error.message });
return { success: false, message: 'The CRM could not be reached. Offer to take a message instead.' };
}
}
};
Add "createLead" to the bot's tools array, test it with the endpoint above, then make a real conversation that triggers it.
Checklist
- Only
name,description,parameters,execute,metadataandonErrorat the top level. - Every
requiredname exists inproperties; the source passes Validate. - No
require()of npm packages and noprocess.env; constants at the top of the file. executehandles missing values, string numbers,testModeand anullupdateContext.- Every branch returns
successand amessagethe model can act on; nothing throws for expected failures. - Success is based on proof (an id or reference), not on HTTP 200.
metadata.timeoutcovers the slowest real response, and you have checked howCIRCUIT_BREAKER_TIMEOUTis set.- Write tools are not
cacheable. - No backup copies in
config/tools/. - After deploying, the log shows no
Invalid tool configurationline for the tool, and a test call returns what you expect.
Where to go next
- Run tools when a conversation ends: Session-end hooks
- Search documents from a tool: Knowledge base
- Attach tools to a bot: Bot configuration reference
- Call tools from a flow node: Visual flows
- Inspect tool calls in a finished conversation: Conversations and analytics