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:
systemPromptin the JSON.config/prompts/<id>.txt, read only when the JSON has nosystemPrompt. LiteBot looks inconfig/promptsnext 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
botIdon a new session: LiteBot scores every active bot against the message and picks the best one whose score reaches itsintentThreshold(default 0.3). If none qualifies it uses the bot with iddefault, thenDEFAULT_BOT_ID, and otherwise fails withNo default bot configured. botIdon a new session: that bot starts the session. On an existing sessionbotIdis 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: 0andswitchingThreshold: 0are 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
- Every key, type and default: Bot configuration reference
- Giving a bot actions: Tools and Session-end hooks
- Flow bots: Visual flows
- Talking to a bot: Chat API and Voice