LiteBot

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

  1. A bot lists tool names in its tools array (Bot configuration reference).
  2. Each turn, LiteBot sends those tools' name, description and parameters to the model.
  3. 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 .js file in config/tools/ is loaded, except index.js, files starting with template and files starting with a dot. A copy such as bookAppointment-old.js is 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 ". Keep constants as plain 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, array and object are checked, along with enum, pattern, format: 'email', minLength and maxLength.
  • Strings may be empty, because models often send "" for optional values.
  • Arguments that are not declared in properties are removed before execute sees them.
  • Every name in required must also be in properties. 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 resolve node_modules for files on disk, so require('axios') in a disk tool fails at load. Use context.modules.axios. Node built-ins such as crypto load, 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, fs or global. Their require accepts only axios, joi, crypto, https, http, url, querystring, uuid and crypto-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 execute returns: { success: true, result: <your object>, toolName, executionTime }. That outer success only means execute returned; your own success inside result is what the model reads.
  • If execute throws, LiteBot calls onError if present, otherwise returns { success: false, error } with the exception text. Thrown errors also count against the tool's circuit breaker.
  • Base success: true on 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 both metadata.timeout and TOOL_EXECUTION_TIMEOUT for every tool. .env.example sets it to 30000. This is a known gap; see LLM providers.
  • The breaker is created on the tool's first run. Changing metadata.timeout later 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 file config/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; 0 or 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_ENV is not production; in production, restart the process after a hand edit. After any change, look for Created new tool, DB tool upserted or Hot-reloaded tools from in the log, and for Invalid tool configuration if 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, metadata and onError at the top level.
  • Every required name exists in properties; the source passes Validate.
  • No require() of npm packages and no process.env; constants at the top of the file.
  • execute handles missing values, string numbers, testMode and a null updateContext.
  • Every branch returns success and a message the model can act on; nothing throws for expected failures.
  • Success is based on proof (an id or reference), not on HTTP 200.
  • metadata.timeout covers the slowest real response, and you have checked how CIRCUIT_BREAKER_TIMEOUT is set.
  • Write tools are not cacheable.
  • No backup copies in config/tools/.
  • After deploying, the log shows no Invalid tool configuration line for the tool, and a test call returns what you expect.

Where to go next