umbot
    Preparing search index...

    umbot FAQ

    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:

    • ✅ A single API for all platforms
    • ✅ Automatic adaptation of responses to the platform
    • ✅ Built-in ReDoS protection and memory optimization
    • ✅ A plugin architecture: connect only what you need

    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.

    • Node.js: 20.19+ (the minimum version)
    • TypeScript: 5.0+

    • The dispatcher order (important!). First the global middleware chain runs (completely, including the code after 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:
      1. Event handlers (bot.addEvent(...)) — if the adapter recognized the event type (a photo, a voice message, a button press) and there is a registered handler.
      2. A step (oldIntentName, if the user is inside a multi-step flow and the step is registered).
      3. Commands — each is checked in registration order, the first matching one wins and runs (the chain stops).
      4. Intents from platformParams (including the built-in welcome/help).
      5. FALLBACK_COMMAND (*). So if you expected an intent but a command fires, first check whether there is an earlier command that overlaps it by slot.
    • Case: this.userCommand is automatically converted to lower case. Your slots must also be in lower case.
    • The lookup type: if a slot is a string, it is looked up with 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).
    • The fallback command: if nothing matched, the command named FALLBACK_COMMAND (equivalent to *) fires, if it is registered.
    • addCommand is the main way to register commands. It supports callbacks, async code and specific logic. It runs before intents are checked.
    • Intents in 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:

    • Modular architecture — move related logic (a game, a shop, statistics) into separate files.
    • Reuse — one plugin works in several projects (a Telegram bot + an Alice skill).
    • Enabling/disabling at build level — 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.
    • Integration with third-party services — encapsulate work with a CRM, payments, etc.

    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 start runs the built code from dist/, so npm run build is 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 re2 is installed and the cache is already filled. Detailed results are in the BENCHMARKS section.

    re2 is a regular expression library that:

    • Speeds up processing 2-15 times
    • Reduces memory usage 3-7 times
    • Protects against ReDoS attacks

    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: '***',
    }),
    );
    1. Create a class that extends BaseDbAdapter
    2. Implement the methods: isConnected, _select, _insert, _update, _remove
    3. Connect it via use:
    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:

    1. If a database adapter is set, the data from userData is saved to the database, and state is written to local storage.
    2. If no database adapter is set, the data from state is written.
    1. Create a class that extends BasePlatformAdapter
    2. Implement the methods: isPlatformOnQuery, setQueryData, getContent
    3. Connect it as a plugin:
    bot.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:

    1. ✅ Whether the token is correct
    2. ✅ Whether your server is reachable from the internet (for webhooks)
    3. ✅ Whether the webhook URL is configured correctly in the platform's developer console
    4. ✅ Whether you use HTTPS (required by most platforms)
    • Use media preloading: Preload uploads images and sounds in advance.
    • Optimize the handler code: avoid heavy synchronous operations, move them to asynchronous tasks.
    • For very large command sets (more than 10 000), reconsider the architecture: use parameterized commands or delegate the logic to an external API.
    • Platform limitations
    • The button type: on voice platforms (Alice, Marusia) link buttons (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:

    • A clean architecture — business logic is separated from the transport layer. The code becomes clearer and easier to maintain.
    • No duplication inside the project — one way to handle commands, state, buttons and cards.
    • Built-in state management — user data is saved automatically (locally or in a database).
    • UI components out of the box — buttons, cards, images, sounds, TTS — a single API for all supported platforms.
    • Security — ReDoS protection, a strict mode for production.
    • Readiness for the future — if a year from now the business asks to add Telegram or Marusia, you will not have to rewrite the core. Just connect one more adapter.

    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:

    1. ✅ Whether the slots in platformParams.intents or in addCommand are written correctly
    2. ✅ Whether you use the right regular expressions
    3. ✅ Whether you added a handler for FALLBACK_COMMAND

    The problem: the file database overflows, so the application may crash with an error at some point. The solution:

    1. Switch to MongoAdapter or another database
    2. Clean up old data if needed

    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:

    1. ✅ Use re2
    2. ✅ Upload resources (images, sounds) in advance with Preload
    3. ✅ Optimize the code in command handlers
    4. ✅ Split complex commands into several simple ones
    5. ✅ Use caching for frequently requested data

    The problem: a potential vulnerability was found in your regular expressions. The solution:

    1. Rewrite the regular expression
    2. Use re2 for protection
    3. Enable the strict mode: 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:

    • Rewrite the expression, avoiding nested quantifiers ((a+)+) and repeated groups (see the list below).
    • Check the expression on regex101.com with the "debugger" flag. If you are sure it is safe, use the prod mode (not recommended).

    What exactly the check looks for. It is a heuristic over well-known ReDoS classes (OWASP):

    • nested quantifiers — (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);
    • overlapping alternatives under a quantifier — (a|aa)+, (\d+|\d+)*;
    • "any character" or an alternative inside a quantified group — (.*a){12}, (\w+\.)+;
    • ladders of adjacent quantifiers with overlapping classes — a*a*a*b, \d+\d+\d+$;
    • quantified backreferences, patterns that are too long (more than 1000 characters) and too deeply nested (more than 5 levels).

    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.

    • Make sure the step is registered with bot.addStep() — you can do it at any time, as long as it happens before the user reaches this step.
    • The previous handler must set this.thisIntentName = 'step_name'.
    • A step fires only if this.oldIntentName (from userData or state) matches the step name, or if nlu.intents has an intent with that name.
    1. ✅ Keep tokens in a .env file
    2. ✅ Add .env to .gitignore
    3. ✅ Use environment variables on the server

    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());