This page is machine-translated from the Russian original. If something reads oddly, the Russian version is the source of truth — open an issue.
Answers to frequent questions about the multi-platform umbot framework — choosing an editor, platforms, commands and steps, the database, performance and fixing typical errors.
Answer: umbot is a multi-platform TypeScript framework that lets you write the logic once and
run it on 7 platforms: Alice, Sber SmartApp, Marusia, Telegram, VK, Viber, MAX.
Advantages over native SDKs:
umbot is a multi-platform TypeScript framework for building voice platform skills and chatbots.
It lets you write code once and run it on 7 platforms: Alice, Sber SmartApp, Marusia, Telegram, VK, Viber, MAX.
next()), then the platform's middleware chain. If some middleware did not call next(), the dispatcher does not
start. Then the dispatcher itself (controller.run()) processes the request along the chain:
bot.addEvent(...)) — if the adapter recognized the event type (a photo, a voice message,
a button press) and there is a registered handler.oldIntentName, if the user is inside a multi-step flow and the step is registered).platformParams (including the built-in welcome/help).*).
So if you expected an intent but a command fires, first check whether there is an earlier command
that overlaps it by slot.this.userCommand is automatically converted to lower case. Your slots must also be in lower
case.includes(). If you need an exact match, use a regular
expression (for example, /^hello$/; the i flag is needed only if the expression itself contains capital letters —
userCommand is already lowercased).FALLBACK_COMMAND (equivalent to *) fires, if it
is registered.addCommand and an intent from platformParams?addCommand is the main way to register commands. It supports callbacks, async code and specific logic.
It runs before intents are checked.platformParams are used for basic actions (greeting, help) and are processed only if
no command matched.Recommendation: implement all business logic with addCommand, and leave intents for the standard texts (welcome,
help).
Use this.userData or this.state:
// Step 1: save the input
ctx.userData.name = ctx.userCommand;
ctx.thisIntentName = 'step2';
// Step 2: read it
ctx.text = `Hi, ${ctx.userData.name}!`;
Step 2 must be registered with bot.addStep('step2', (ctx) => { ... }), otherwise the transition will not work.
userData is kept between sessions (in the database or local storage).state is the platform's own storage (only Alice, Marusia and SmartApp, with isLocalStorage: true). Its lifetime
is set by the platform: the adapter takes the longest-lived level present in the request. For Alice this is the user storage
(it survives sessions if the user is signed in to Yandex), then the application (device) storage, then
the session; for Marusia — the user, then the session. On Telegram, VK, MAX and Viber state is always null.A plugin in umbot is a module that extends functionality; it is registered in the application context (AppContext)
and lets you add new logic without changing the framework core.
Interface: a plugin can be implemented as a class with an init(appContext, bot) method or as a function with the
isPlugin = true property.
Registration: plugins are connected with bot.use(plugin).
Built-in types: the system reserves slots for system plugins:
i18n — localization;nlu — natural language processing;regExp — a custom regular expression implementation.Adapters (platforms and databases) in version 3.0.0 are also implemented with the plugin architecture.
You connect it at the application entry point with a chain of use() calls.
Example code:
import { Bot, createPlugin } from 'umbot';
import { fullPlatforms, MongoAdapter } from 'umbot/plugins';
const bot = new Bot();
// 1. Connecting ready-made plugins (platforms and the database)
bot.use(fullPlatforms);
bot.use(new MongoAdapter({ host: 'mongodb://localhost:27017', database: 'umbot' }));
// 2. Connecting a custom plugin (an example)
const myPlugin = createPlugin((appContext, bot) => {
appContext.plugins['myPlugin'] = {
getData: (key: string) => `Value: ${key}`,
};
});
bot.use(myPlugin);
bot.start('localhost', 3000);
A plugin is a mechanism for extending the framework's functionality without changing its core. It lets you encapsulate logic in separate modules that you connect only when needed.
Main scenarios:
bot.clearUse() removes all plugins, adapters, middleware and
platforms at once (it is a global operation), so it is better not to use it for fine-grained feature control.Example:
// plugins/game.ts
import { Bot, AppContext, BotController, IUserData, createPlugin } from 'umbot';
interface GameData extends IUserData {
score: number;
}
export const gamePlugin = createPlugin((appContext: AppContext, bot: Bot): void => {
bot.addCommand('game_start', ['play'], (_, bc: BotController<GameData>) => {
bc.userData.score = 0;
bc.text = 'The game has started!';
});
});
// index.ts
import { Bot } from 'umbot';
import { gamePlugin } from './plugins/game';
const bot = new Bot();
bot.use(gamePlugin); // connect the plugin
When to use plugins:
| Situation | Use a plugin |
|---|---|
| A large codebase (>1000 lines) | Yes |
| Several projects with shared logic | Yes |
| You need to turn features on and off | Yes |
| Integration with third-party APIs | Yes |
More about creating plugins — in the Extension architecture section.
npm install umbot
npx umbot create my-bot
cd my-bot
npm install
npm run build
npm run start
npm run startruns the built code fromdist/, sonpm run buildis needed before the first start (and after changes).
Save the tokens to a .env file:
TELEGRAM_TOKEN=your-token
VK_TOKEN=your-token
VK_CONFIRMATION_TOKEN=your-token
VK_SECRET_KEY=your-secret
ALISA_TOKEN=your-token
MAX_TOKEN=your-token
SPEECH_KIT_TOKEN=your-token
# ... and other tokens
Then set the path in the configuration:
bot.setAppConfig({
env: './.env',
});
The problem: version 3.0 moved to a plugin architecture. Platforms are now handled through adapters. Before (2.2.x):
import { Bot } from 'umbot';
const bot = new Bot();
bot.setPlatformParams(params);
bot.start('localhost', 3000);
Now (3.0):
import { Bot } from 'umbot';
import { fullPlatforms, FileAdapter } from 'umbot/plugins';
const bot = new Bot()
.use(fullPlatforms) // Connect the platforms as a plugin
.use(new FileAdapter()) // Connect the database adapter
.setPlatformParams(params);
bot.start('localhost', 3000);
You can read more about the changes here
Important: version 2.1.x has a critical architectural problem. Be sure to update! Update the package:
npm install umbot@2.2
Check the code for deprecated methods (they were removed in 2.2.x)
npm list umbot
umbot can handle any number of commands, but keep in mind that a large number of commands usually
indicates a suboptimal application architecture.
Also, with a large number of commands the application's response time grows.
It is recommended not to use more than 1000 commands in your application.
| Number of commands | Processing time (cold start, worst case) | Processing time (with re2, warm cache) | Recommendation |
|---|---|---|---|
| 50 | up to 0.5 ms | up to 0.5 ms | Excellent |
| 500 | up to 1.2 ms | up to 0.7 ms | Excellent |
| 1000 | up to 30 ms | < 1 ms | Good |
| 10000 | up to 1 s | < 20 ms | Check your server |
| 20000 | up to 1 s | 22.44 ms | Use re2 |
Note: "cold start" means the RegExp cache is empty and the expressions are compiled for the first time. The "up to 30 ms" value for 1000 commands is the worst case (all commands with RegExp, an empty cache). In a typical scenario (500 commands, strings) the time is 0.26 ms. "With re2, warm cache" means
re2is installed and the cache is already filled. Detailed results are in the BENCHMARKS section.
re2 is a regular expression library that:
Installation:
npm install re2
After installation umbot starts using re2 automatically.
Node.js on Windows is less efficient than on Unix systems (Linux/macOS). This can lead to high memory usage (up to 4 GB vs 400 MB on Linux). Recommendation: use a Linux server for production.
It is not recommended. The file database (FileAdapter) keeps data in RAM and is designed for a quick start or databases of up to a few hundred MB — beyond that, Out of Memory and an application crash are possible.
Recommendation: use MongoAdapter or create your own adapter:
import { MongoAdapter } from 'umbot/plugins';
bot.use(
new MongoAdapter({
host: 'mongodb://localhost:27017/my-bot',
database: 'bot_db',
user: 'user',
pass: '***',
}),
);
BaseDbAdapterisConnected, _select, _insert, _update, _removeuse:bot.use(new MyCustomAdapter(config));
Yes, you can store data both in the platform's local storage and in your database at the same time. You can do it like this:
import { Bot } from 'umbot';
import { FileAdapter } from 'umbot/plugins';
const bot = new Bot();
bot.use(new FileAdapter());
bot.addCommand('test', ['save'], (_, cBot) => {
// ⚠️ Do not write `cBot.userData = {}` — it overwrites the reference and breaks change tracking.
// Mutate the object instead:
Object.assign(cBot.userData, { key: 'value' }); // Data for the database
// ⚠️ Do not write `Object.assign(cBot.state, ...)` — on chat platforms
// (Telegram, VK, Viber, Max) state is null, and the call throws a TypeError.
// The safe form: the framework reads state after the command runs,
// so overwriting it via spread is fine ({ ...null } gives an empty object).
cBot.state = { ...cBot.state, key: 'value' }; // Data for the platform's local storage
// Your logic
});
Then, on the next request, the data from the database will be in userData, and the data from the platform in state.
Keep in mind that the local storage logic works only if the platform itself
supports this behavior. Voice platforms (Alice, SmartApp, Marusia) have local storage;
on chat platforms (Telegram, VK, Viber, Max) state is not filled by the platform and stays null until you
initialize it yourself — so write data with the safe form cBot.state = { ...cBot.state, ... }.
This is possible. Write the following code:
import { Bot } from 'umbot';
const bot = new Bot();
bot.setAppConfig({
isLocalStorage: true, // tell the framework that data is stored in the platform's local storage
});
bot.addCommand('test', ['save'], (_, ctx) => {
ctx.userData.myKey = 'value'; // Save by mutating, not reassigning
// Your logic
});
Note that userData is used: this mechanism exists for convenience in scenarios where no
database is intended.
Also keep in mind that isLocalStorage is set; without it the mechanism will not work.
This applies only to the built-in platform adapters. With third-party adapters the behavior may differ.
If you write data to both userData and state, the behavior is as follows:
userData is saved to the database, and state is written to local
storage.state is written.BasePlatformAdapterisPlatformOnQuery, setQueryData, getContentbot.use(new MyPlatformAdapter(token));
By default it is recommended to use fullPlatforms, which connects all platforms. To connect specific
platforms, do the following:
import { TelegramAdapter, VkAdapter } from 'umbot/plugins';
// Connect Telegram and VK
bot.use(new TelegramAdapter(telegramToken)).use(new VkAdapter(vkToken));
Also remember that you can connect only voice platforms (voicePlatforms) or only chatbot
platforms (botPlatforms)
Run npx umbot doctor in the project folder: the command checks the tokens with a request to the platform APIs, shows
the registered webhooks and the last Telegram delivery error. For local checks of Telegram, VK and MAX
a webhook is not required — bot.startPolling() gets updates without a public address.
Check:
addLink, hide: false) are shown as visible
buttons, while interactive ones (addBtn, hide: true) are shown as suggestions and hidden after a press.Yes, and here is why.
umbot is not a "multi-platform add-on" but a full framework that pays off already on the first
project, even if you never plan to add other channels.
What you get by using umbot for a single platform:
An example for a single platform (Alice only):
import { Bot, BotController } from 'umbot';
import { AlisaAdapter } from 'umbot/plugins'; // the Alice adapter
const bot = new Bot();
bot.use(new AlisaAdapter()); // instead of fullPlatforms
// ... all the other logic stays unchanged
No overhead — you use exactly what you need. The framework does not force you to connect extra platforms.
Bottom line: umbot not only works for a single platform but makes single-platform development more structured, safe and ready to scale. Try it — and you will see that the code becomes cleaner and less time goes into routine.
Use the BotTest class:
import { BotTest } from 'umbot/test';
import { fullPlatforms } from 'umbot/plugins';
const bot = new BotTest();
bot.use(fullPlatforms);
bot.test(); // Starts the interactive console mode
Logs are saved to the directory set in error_log:
bot.setAppConfig({
error_log: './logs',
});
In development mode (dev) errors are also printed to the console. In prod and strict_prod without a custom logger, the error
text is duplicated to stderr as an [umbot] ... line (the full data with the stack is in the error.log file).
bot.setAppMode('dev');
The incoming text did not match any command, step or intent. In this case the base controller uses the text from
platformParams.empty_text (an empty string if not set). Check:
The problem: the file database overflows, so the application may crash with an error at some point. The solution:
The practical response limit for Alice is about 3 seconds: the framework logs a warning after 2 seconds of processing already, and an error after 2.9 seconds. Other platforms have different limits that may change. Optimization:
The problem: a potential vulnerability was found in your regular expressions. The solution:
bot.setAppMode('strict_prod')The framework automatically checks regular expressions for vulnerabilities. In the strict_prod mode a dangerous slot is dropped: the command works with the remaining safe slots, and if all of them are dangerous, it is not registered. To fix it:
What exactly the check looks for. It is a heuristic over well-known ReDoS classes (OWASP):
(a+)+, (a*)*, ([a-z]+)*, as well as their bounded variants such as
(a{1,10}){1,10} and (a?){2,8} (a variable-length repetition under a variable outer interval);(a|aa)+, (\d+|\d+)*;(.*a){12}, (\w+\.)+;a*a*a*b, \d+\d+\d+$;The heuristic does not prove safety: an expression it let through can still be slow. Reliable
protection is the re2 package (npm i re2): the framework runs compatible expressions with it without backtracking, regardless of
what the check found.
Check that you connected the adapters you need (bot.use(new TelegramAdapter()), etc.).
Make sure the request arrives at the right URL and contains the correct headers (for example, X-Telegram-Bot-Api-Secret-Token for Telegram).
For local testing use BotTest — it substitutes test data automatically.
Sometimes the framework cannot detect the platform type correctly, or you need to
determine the platform type yourself.
In this case you can use bot.setPlatformResolver(...): it registers a custom handler
for determining the platform type.
How to use it:
bot.setPlatformResolver((query, headers, detect) => {
const platform = detect?.(query, headers);
if (platform === 'telegram' && headers?.['x-force-vk']) {
return 'vk';
}
return platform;
});
The first argument is the platform request itself, the second is the headers, and the third is a handler function with the standard platform detection logic.
If the platform was detected incorrectly, it is recommended to use this mechanism to set the correct platform, and then file a bug report so that we can fix it quickly.
umbot automatically checks regular expressions for vulnerabilities.
In the strict_prod mode dangerous RegExps are rejected; in dev/prod they work but are logged (a warning when
re2 is installed, an error without it).
bot.getAppContext().httpClient = async (url, options) => {
// Your implementation
return fetch(url, options);
};
Yes, via the webhookHandle method:
app.post('/webhook', (req, res) => {
bot.webhookHandle(req, res);
});
This lets you integrate the application into an existing webhook.
// Option 1: an object
class MyI18nPlugin implements IPlugin {
init(appContext: AppContext, bot: Bot) {
appContext.plugins['i18n'] = {
getData(key: string, ...params: unknown[]): string {
return `Translated: ${key}`;
},
};
}
destroy(_bot: Bot) {}
}
// Option 2: a function (via createPlugin — the isPlugin flag is set automatically)
const myI18nPlugin = createPlugin((appContext: AppContext, bot: Bot) => {
appContext.plugins['i18n'] = (key: string, ...params: unknown[]) => {
return `Translation for: ${key}`;
};
});
bot.use(myI18nPlugin);
bot.use(new MyI18nPlugin());
Full reference — API v-3.1 · all versions.