LiteBot

Bots

This chapter explains where LiteBot loads bots from, how to create one, where its system prompt comes from, and how changes reach a running server. Read it before you write your first bot, and again when a bot does not appear or does not pick up an edit. Every key is listed with its type and default in Bot configuration reference.

Where bots come from

At start-up LiteBot loads bots from three places, in this order. A later source replaces an earlier bot with the same id.

Order Source Notes
1 config/bots/*.json under the process working directory ("bundled") Every .json file is loaded
2 config/bots/*.json next to the litebot binary ("external") Files whose name starts with template are skipped
3 The Mongo bots collection Only documents with isActive: true. Loaded last, so the database copy wins

When pm2 starts the binary from its own folder, as in Quick start, the working directory and the binary folder are the same, so both passes read the same config/bots. A file named template*.json is then still loaded by the first pass, so keep templates out of config/bots.

A bot in the database always wins over a file with the same id. Bots created or edited from the dashboard or the API are stored in Mongo, so once you edit a file-based bot through the API, later edits to its file no longer take effect after a restart.

The log shows each load: Loaded bot configuration: <name> from <file> (bundled) for files and Loaded bot from database: <name> for Mongo, then Initialized <n> bots.

The unknown-key rule

Every bot is checked against the Joi schema in src/core/bot-manager.js. The schema rejects any key it does not list. A bot with an unlisted key, a value of the wrong type or a missing required key is skipped, and the server starts without it. Look for these log lines:

Where the bot came from Log line
Bundled file Failed to load bundled bot from <file>: followed by the reason, for example "greeting" is not allowed
External file Failed to load external bot from <file>:
Mongo Invalid bot config in database for <id>:
File hot reload Failed to reload bot config from <file>: <reason>
POST /api/bots or PUT HTTP 400 with the reason in error

Two keys the loader or voice handler reads are not in the schema, so a bot that sets them is rejected: promptFile and voice.thinkingBudget. See Keys the loader reads but the schema rejects.

A minimal bot

Three keys are required: id, name and systemPrompt. Everything else has a default. Save this as config/bots/clinic-reception.json beside the binary:

{
  "id": "clinic-reception",
  "name": "Clinic Reception",
  "description": "Answers questions about clinic hours and services",
  "provider": "gemini",
  "model": "gemini-2.5-flash",
  "temperature": 0.3,
  "maxTokens": 600,
  "tools": ["getSessionContext"],
  "systemPrompt": "You are the receptionist for {{clinicName}}. Keep answers under three sentences. Opening hours: {{hours}}.",
  "variables": {
    "clinicName": "Example Clinic",
    "hours": "Monday to Saturday, 9 am to 6 pm"
  }
}

Set provider to a provider you have configured. The schema default is groq, which fails if you have no Groq key. Provider names and models are in LLM providers.

Creating a bot

From the dashboard

Open the Bots page and choose Create Bot. The Form Builder writes a normal bot. When the visual editor is enabled, a Visual Builder button appears next to it for flow bots; see Visual flows. The dashboard saves through the API below, so the bot is stored in Mongo and is live as soon as you save.

From the API

POST /api/bots takes the bot JSON as the body. It needs a user or API key with the bots create permission.

curl -s -X POST http://127.0.0.1:3345/api/bots \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"id":"pizza-orders","name":"Pizza Orders","provider":"gemini","systemPrompt":"You take pizza orders for Example Pizza."}'
{ "success": true, "message": "Bot created successfully", "data": { "id": "pizza-orders", "tenantId": "default", "temperature": 0.7, "maxTokens": 1000, "...": "..." } }

The response carries the bot with every default filled in. An id that already exists in memory or in Mongo returns 409 Bot already exists. A tenant user's bot is always created in that user's tenant; only a global admin can set tenantId in the body (Users, roles and tenants).

POST /api/bots/generate-with-ai with {"requirements": "..."} drafts a bot config with the Groq provider and returns it without saving. It fails when no Groq key is configured.

The system prompt

A file bot gets its prompt from the first of these that applies:

  1. systemPrompt in the JSON.
  2. config/prompts/<id>.txt, read only when the JSON has no systemPrompt. LiteBot looks in config/prompts next to the binary first, then under the working directory.

If neither exists the bot is skipped with No system prompt found for bot <id>.

The loader also reads a promptFile key and would prefer it over systemPrompt, but the schema rejects promptFile, so a bot that uses it is skipped. Use the <id>.txt convention instead.

Bots created through the dashboard or API must carry systemPrompt inline. Mongo bots never read prompt files.

Each text-chat turn also gets a separate system message with today's date, for example Today is Saturday 24 October 2026 — 24/10/2026 in DD/MM/YYYY, timezone Asia/Kolkata. The zone is BOT_TZ, then ANALYTICS_TZ, then Asia/Kolkata. It is kept out of the prompt so a cached prompt stays the same across days. You do not need a date variable.

Variables

variables is a flat object of string, number or boolean values. {{name}} in the prompt is replaced with the value. Names start with a letter or underscore and are up to 30 characters.

Placeholder Replaced with
{{name}} variables.name
{{env.NAME}} The environment variable NAME, only if it is listed in ALLOWED_TEMPLATE_ENV_VARS (default NODE_ENV,APP_NAME,SITE_URL,SUPPORT_EMAIL)

A name with no value is left in the text unchanged and logged as Template variables not found: <names>. An env name not on the allow-list is left unchanged and logged as Template env vars rejected (not in allowlist).

When replacement happens matters:

Bot and channel Variables replaced
File bot, any channel Once, when the file is loaded or hot-reloaded. The stored prompt is the resolved text
Mongo or API bot, text chat Never. {{name}} reaches the model as written
Any bot, voice session (/voice) At session start, from variables merged with the variables sent by the caller in the init message (Voice)

For a bot you manage through the dashboard, write values straight into the prompt, or use voice-session variables.

Language

language (default en) is stored and shown in bot lists, but no runtime code reads it: it does not reach the model. Put language instructions in the prompt. For voice bots the spoken language is voice.languageCode (Bot configuration reference).

Tools

tools is a list of tool names. Each name is resolved for the bot's tenant: tenant tools first, then global tools in Mongo, then disk tools (Tools).

tools value Text chat Voice session
Names listed Those tools; unknown names are dropped silently Those tools; unknown names log Voice tool-bridge: tool not in registry, skipping
[] or missing (the default) Every tool visible to the tenant is offered to the model No tools

List the tools a bot needs explicitly. An empty list in text chat hands the model every tool on the server.

Escalation rules

escalationRules sets when a reply carries "shouldEscalate": true. LiteBot only sets the flag; the client (for example a voice gateway) decides what to do with it.

Trigger When
Tool A tool in this turn returned escalated: true in its result
Keyword The user message contains one of escalationRules.keywords (case-insensitive substring)
Message count The session holds at least maxAttempts × 2 stored messages

maxAttempts defaults to 3, but only when the bot has an escalationRules object. A bot that leaves escalationRules out gets {} and never escalates on message count. With the default of 3, every reply after the first few turns is flagged. Raise maxAttempts (for example to 100) if your client acts on the flag. sentimentThreshold is accepted but not used.

Routing and bot switching

A chat request may name a bot with botId, or leave it out.

  • No botId on a new session: LiteBot scores every active bot against the message and picks the best one whose score reaches its intentThreshold (default 0.3). If none qualifies it uses the bot with id default, then DEFAULT_BOT_ID, and otherwise fails with No default bot configured.
  • botId on a new session: that bot starts the session. On an existing session botId is ignored; the session keeps its own bot.

Scoring uses only metadata.keywords (phrase match 0.8, word match 0.3), metadata.patterns (case-insensitive regex, 0.7), a 0.2 boost for keywords in the last three user messages, and metadata.priority / 10. The total is divided by the number of matches and capped at 1. A bot with no keywords or patterns is never picked or switched to.

From the second turn on, every text-chat turn also checks whether to switch bots. It switches when another bot scores at least the current bot's switchingThreshold (default 0.4) and beats the current bot's score by more than 0.2. The new bot's prompt is added, the session moves to it, and a handoff message is added: handoffMessages[<newBotId>] from the current bot, or a generic Let me connect you with our <department or name> team....

Known gaps:

  • The switch check runs even when the request named botId. A pinned bot can still be switched away from.
  • Scoring looks at every active bot on the server, not only bots in the same tenant.
  • intentThreshold: 0 and switchingThreshold: 0 are treated as the defaults.

To stop switching away from a bot, set its switchingThreshold to 1, and keep metadata.keywords and metadata.patterns off bots that should never be switched to. Flow bots built in the visual editor get keywords from their node labels, so they are switch targets. Test with POST /api/bots/intent-detection ({"message": "..."}) and POST /api/bots/switching-test ({"currentBotId": "...", "message": "..."}); neither changes a session.

Voice sessions on /voice never switch bots.

Editing a live bot

PUT /api/bots/:botId/config (or PUT /api/bots/:botId) applies at once to new turns, with no restart, and writes the bot to Mongo.

curl -s -X PUT http://127.0.0.1:3345/api/bots/clinic-reception/config \
  -H "X-API-Key: lbk_..." -H "Content-Type: application/json" \
  -d '{"temperature": 0.2, "tools": ["getSessionContext", "bookAppointment"]}'

The body is merged into the bot one level deep. A nested object you send replaces the whole stored object: sending {"voice": {"voiceName": "Puck"}} resets every other voice key to its default. Send the full object. id cannot be changed (400 Bot id cannot be changed), and tenant users cannot move a bot to another tenant. The merged bot is checked against the schema, so an unknown key fails the update with 400.

GET /api/bots/:botId/config returns the stored bot. DELETE /api/bots/:botId removes it from memory and Mongo. A bot that also has a JSON file comes back on the next restart.

Export and import

GET /api/bots/:botId/export?format= downloads a bot:

format Result
files (default) JSON with config (no prompt), systemPrompt and exportMetadata
json The config with the prompt inline and an exportMetadata block
zip config.json, systemPrompt.txt, manifest.json, README.md and, unless includeTools=false, the bot's tool sources under tools/

Tenant users get global tool names in the zip but not their source. POST /api/bots/export/bulk exports several bots.

POST /api/bots/import takes one single-bot zip as multipart field file (25 MB limit):

curl -s -X POST http://127.0.0.1:3345/api/bots/import \
  -H "Authorization: Bearer <token>" \
  -F file=@clinic-reception.zip -F includeTools=false

The import fails with 409 if the id exists; delete the old bot first or change the id. Bulk zips are refused. Tool sources are imported only with includeTools=true, because that runs uploaded code. Tenant users get them as tenant tools, and a global admin's disk-origin tools are written to config/tools/. The response lists importedTools and skippedTools.

Reloading and hot reload

Change When it takes effect
Dashboard or API edit At once
config/bots/*.json file edit At once when hot reload is on; otherwise at pm2 restart
config/prompts/<id>.txt edit When the bot's JSON file is next reloaded (see below), or at restart
Direct edit in Mongo After a restart, or POST /api/bots/reload when NODE_ENV is not production

With NODE_ENV=production, file hot reload is off unless you set ENABLE_HOT_RELOAD=true in .env. (It is always on when NODE_ENV is anything else.) The log at start-up shows Hot-reload enabled for bot configs and prompts or Hot-reload disabled in production mode. It watches config/bots and config/prompts next to the binary:

  • A changed JSON file is re-validated and replaces the bot in memory. If the bot already existed, it is also written to Mongo, so from then on the Mongo copy wins at start-up.
  • A new JSON file loads a new bot. A deleted file unloads the bot whose id equals the file name.
  • A changed prompt file is logged as Prompt <file> updated but no active bots are using it, because a loaded bot already holds its prompt text. Save the bot's JSON file again to reload the prompt.

POST /api/bots/reload (admin only) clears every bot and reloads files and Mongo. It returns 403 Hot-reload not available in production mode when NODE_ENV=production, even with ENABLE_HOT_RELOAD=true; run pm2 restart litebot (or your process name) there instead.

curl -s -X POST http://127.0.0.1:3345/api/bots/reload -H "Authorization: Bearer <token>"
{ "success": true, "data": { "beforeCount": 4, "afterCount": 5, "reloadedAt": "2026-10-24T06:30:00.000Z" } }

Where to go next