This page is machine-translated from the Russian original. If something reads oddly, the Russian version is the source of truth — open an issue.
This reference describes the main public classes, methods and interfaces of the umbot framework. To get started, see the "Quick start" section.
The main class for managing the application logic. It provides the base functionality for handling user requests, managing state and interacting with different platforms.
| Property | Type | Description |
|---|---|---|
| text | string | The response text for the user |
| tts | string | null | Text for speech (on voice platforms, if null, it can be filled automatically from text) |
| buttons | Buttons | The buttons component (initialized lazily through a getter) |
| card | Card | The cards/galleries component (initialized lazily through a getter) |
| nlu | Nlu | NLU data (initialized lazily through a getter) |
| sound | Sound | Sound effects (initialized lazily through a getter) |
| userId | string | number | null | The user identifier |
| userToken | string | null | The user authorization token (if the platform provides it) |
| userMeta | unknown | null | Additional user information (platform-dependent) |
| messageId | number | string | null | The message number. 0 is the start of a new session on voice platforms (welcome is used for it) |
| userCommand | string | null | The user command in lower case |
| originalUserCommand | string | null | The original user command |
| payload | Record<string, unknown> | string | null | undefined | Additional request parameters (payload) |
| eventType | TEventType | The universal event type ('message', 'photo', 'callback', 'start', …). Filled by the platform adapter; the basis of bot.addEvent routing |
| match | RegExpExecArray | null | The command's regex match (lazy): groups in match[1], match.groups. null for string commands |
| api | IControllerApi | null | The active platform's API facade: sendPhoto/sendDocument/sendAudio/sendVideo/answerCallback/can. A lazy object; null on voice platforms |
| userData | TUserData | User data (the database or local storage, depending on setAppConfig) |
| state | TPlatformState | null | The platform's local storage (if the platform supports it and isLocalStorage is enabled) |
| isAuth | boolean | Request user authorization (account linking; Alice) |
| userEvents | IUserEvent | null | User events (authorization/rating), if the platform sends them |
| isScreen | boolean | Whether the user has a screen (if the platform reports it) |
| isEnd | boolean | End the session. Read by voice platforms; chat platforms ignore the flag |
| skipAutoReply | boolean | If true, the framework does not try to "auto-send" the response (relevant for platforms where you send messages via the API yourself) |
| requestObject | Record<string, unknown> | string | unknown | null | The original request object from the platform |
| thisIntentName | string | null | The step/intent name to save as the "next step" |
| oldIntentName | string | null | The name of the previous step/intent (from userData.oldIntentName or state.oldIntentName) |
| emotion | string | null | The assistant's response emotion (SmartApp: 'radost', 'pechal', …) |
| appeal | 'official' | 'no_official' | null | The form of address (SmartApp; arrives in the request) |
| isSendRating | boolean | Request a skill rating (the special response is sent only on SmartApp) |
| appContext | AppContext | The application context (config, registries, logger) |
| appType | TAppType | null | The platform the request came from (filled by the framework during processing) |
| platformOptions | IPlatformOptions | Service request data from the adapter, the core and middleware: clientIp, requestId, rateLimitOverflow, etc. Read it; write only in your own adapter/middleware |
| Method | Parameters | Return value | Description |
|---|---|---|---|
| action | intentName: string | null, isCommand?: boolean, isStep?: boolean | void | Promise<void> | Your main handler. Called by the framework (overridden in a subclass); can be async — the framework awaits the promise |
| run | - | void | Promise |
Starts request processing (called by the framework; usually not called manually) |
| setAppContext | appContext: AppContext | this | Sets the application context (updates the context in the already created buttons and card components) |
| clearStoreData | - | void | Fully resets the controller state: text, tts, user data, flags and the NLU cache |
| isButtonsInit | - | boolean | true if the buttons component was initialized (through the buttons getter) |
| isCardInit | - | boolean | true if the cards component was initialized (through the card getter) |
| isSoundInit | - | boolean | true if the sounds component was initialized (through the sound getter) |
| isNluInit | - | boolean | true if the NLU component was initialized (through the nlu getter) |
The main orchestrator class. It manages the lifecycle, middleware, command registration and starting the server.
class Bot<
TUserData extends IUserData = IUserData,
TPlatformState extends IPlatformData = IPlatformData,
> {
constructor(type?: TAppType, botController?: TBotControllerClass<TUserData, TPlatformState>);
}
type is the default platform (usually not needed: the platform is detected from the request), botController is the controller
class (the same as initBotController). All methods except those listed below with a different return value
return this — calls can be chained.
| Method | Parameters | Return value | Description |
|---|---|---|---|
| setAppConfig | config: Partial<IAppConfig> | Bot | Sets the application configuration |
| setAppMode | mode: TAppMode ('dev' | 'prod' | 'strict_prod') |
Bot | Sets the operating mode |
| setPlatformParams | params: IAppParam | Bot | Sets the platform parameters |
| initBotController | controller: TBotControllerClass | Bot | Connects the controller class |
| addCommand | commandName: string, slots: TSlots, cb: (userCommand: string, bc: BotController) => void | string | Promise<void | string>, isPattern?: boolean | Bot | Registers a command |
| addAction | actionName: string, cb: (userCommand: string, bc: BotController) => void | string | Promise<void | string> | Bot | A callback button press handler by payload (like bot.action() in Telegram frameworks). The payload 'buy' / {"command":"buy"} calls the buy command (Telegram, VK, MAX) |
| addEvent | eventType: TEventType, cb: (bc: BotController) => void | string | Promise<void | string> | false | Bot | A platform event handler (a photo, a voice message, callback, start, etc.). Called before steps and commands; false passes the request on to the regular pipeline |
| removeEvent | eventType: TEventType | Bot | Removes all handlers of an event |
| clearEvents | - | Bot | Removes all event handlers |
| removeCommand | commandName: string | Bot | Removes a command by name |
| clearCommands | - | Bot | Removes all commands |
| addStep | stepName: string, handler: IStepParam['cb'] | Bot | Registers a step (a dialog chain) |
| removeStep | stepName: string | Bot | Removes a step by name |
| clearSteps | - | Bot | Removes all steps |
| addForm | formName: string, options: IAddFormOptions | Bot | Registers a multi-step form with field validation |
| removeForm | formName: string | Bot | Removes a form and all its steps |
| use | fn: MiddlewareFn | platform: TAppType, fn: MiddlewareFn | plugin: TPlugin | Bot | Connects middleware or a plugin |
| clearUse | - | Bot | Removes all platforms, plugins and middleware |
| setCustomCommandResolver | resolver: TCommandResolver | Bot | Sets a custom command resolver |
| setCommandGroupMode | mode: TCommandGroupMode | Bot | The RegExp grouping mode |
| setPlatformResolver | resolver: TPlatformResolver | Bot | Sets the platform detection function |
| setLogger | logger: ILogger | null | Bot | Sets a custom logger (null disables it) |
| getAppContext | - | AppContext | Gets the application context |
| setContent | content: TBotContent (object | string | null) |
void | Sets the request content (for testing) |
| run | appType?: TAppType | null, content?: string | object | null, auth?: TBotAuth, clientIp?: string | Promise<TRunResult> | Processes an incoming request. All parameters have defaults (null), so a call without arguments is valid. clientIp is available to middleware via controller.platformOptions.clientIp (for example, ipFilter) |
| webhookHandle | req: IncomingMessage, res: ServerResponse, responseCb?: TBotResponseCb | Promise<void> | An HTTP request handler (for Express/Fastify integration) |
| webhookEvent | data: string | object | null, headers?: Record<string, unknown>, clientIp?: string | Promise<IWebhookEventResult> | Processes a serverless platform event (Yandex Cloud Functions, AWS Lambda) with webhook signature verification. Returns { statusCode, body } to return from the cloud function |
| start | hostname?: string, port?: number, responseCb?: TBotResponseCb | Server | Starts the HTTP server (returns a Server instance) |
| startPolling | options?: IPollingOptions ({ platforms?: TAppType[] }) |
Promise<void> | Starts long polling (Telegram, VK, MAX) instead of a webhook. The promise resolves after stopping; it rejects if no adapter supports polling |
| stopPolling | - | Promise<void> | Stops long polling: aborts the current update requests and waits for the already received ones to be processed |
| close | - | Promise<void> | Stops the HTTP server and long polling, cleans up resources |
| send | userId: string | number, controllerOrText: BotController | string, platform: TAppType | Promise<unknown | boolean> | Sends a message to a user (for platforms that support it) |
import { Bot } from 'umbot';
import { fullPlatforms, FileAdapter } from 'umbot/plugins';
const bot = new Bot();
bot.setAppMode('strict_prod'); // 0. The production mode — before registering commands and parameters
bot.use(fullPlatforms); // 1. Register the platforms
bot.use(new FileAdapter()); // 2. Register the database (or isLocalStorage: true)
bot.setAppConfig({
// 3. Pass the config
json: './data',
error_log: './errors',
isLocalStorage: false,
});
bot.setPlatformParams({
intents: [{ name: 'bye', slots: ['bye'] }], // a required field (can be [])
welcome_text: 'Hi!',
});
bot.start('0.0.0.0', 3000); // 4. Start
The controller (
initBotController) and commands (addCommand) are optional for starting. Without them the bot replies only withwelcome_text/help_text/empty_text. This is handy for the very first start — to make sure the webhook works, and then add logic gradually.
run() from umbot/buildIf you want it even shorter, there is the run utility:
import { run } from 'umbot/build'; // TMode = 'dev' | 'dev-online' | 'prod'
import { fullPlatforms, FileAdapter } from 'umbot/plugins';
import { MyController } from './controller/MyController';
run(
{
appConfig: { isLocalStorage: true },
appParam: { intents: [{ name: 'bye', slots: ['bye'] }] },
controller: MyController,
plugins: [fullPlatforms, new FileAdapter()],
logic: (bot) => {
bot.addCommand('ping', ['ping'], (_, bc) => {
bc.text = 'pong';
});
},
},
'prod',
'0.0.0.0',
8080,
);
// run(config, mode: TMode = 'prod', hostname = 'localhost', port = 3000)
// → 'dev' (runs BotTest.test()), 'dev-online' (a server in the dev mode), 'prod' (a server in strict_prod)
If plugins is not passed, run connects all platforms (fullPlatforms) and a database adapter: MongoAdapter when
a database address is set (appConfig.db.host or DB_HOST), otherwise FileAdapter.
The component for working with interface buttons.
| Method | Parameters | Return value | Description |
|---|---|---|---|
| addBtn | title: string | null, url?: string | null, payload?: TButtonPayload, options?: IButtonOptions | this | Adds a button |
| addLink | title: string | null, url?: string, payload?: TButtonPayload, options?: IButtonOptions | this | Adds a link button |
| row | - | this | Ends a row: the next buttons go to a new line (Telegram, VK, MAX, Viber). The buttons before the first call form the first row; without a call each button is on its own line |
| getButtons | buttonProcessing: TButtonProcessing | T | null | Gets the array of buttons adapted to the platform |
| getButtonJson | buttonProcessing: TButtonProcessing | string | null | The JSON representation of the buttons for the platform |
| clear | - | void | Clears all buttons (starts the list over; an already shown keyboard is not removed) |
| remove | - | this | Asks the platform to remove a previously shown keyboard. Relevant for Telegram (the reply keyboard) and VK, where the keyboard "sticks" to the dialog; on other platforms the call is safe and changes nothing |
| isRemove | - | boolean | (a getter) true if remove() was called and the keyboard needs to be removed |
The component for working with cards and galleries.
| Method | Parameters | Return value | Description |
|---|---|---|---|
| addImage | image: string | null, title?: string, desc?: string, button?: TButton | null | this | Adds an image/element (the 4th parameter is the element's button) |
| addOneImage | image: string | null, title?: string, desc?: string, button?: TButton | null | this | Replaces the current card with a single image |
| setTitle | text: string | this | Sets the title (overwrites the previous one) |
| setDescription | text: string | this | Sets the description (overwrites the previous one) |
| addButton | button: TButton | this | Adds a button to a card element |
| clear | - | void | Clears the card: images, title, description and template |
The component for working with sounds. It supports the platforms' standard sounds (Alice, Marusia) and custom audio files.
| Property | Type | Description |
|---|---|---|
| sounds | ISound[] | An array of custom sounds |
| isUsedStandardSound | boolean | Use the platform's standard sounds (true by default) |
| Method | Parameters | Return value | Description |
|---|---|---|---|
| getSounds | text: string | null, soundProcessing: TSoundProcessing<TResult>, controller: BotController | Promise<TResult> | Gets the text with embedded sounds for the platform |
The component for working with entities the platform (or a plugin) extracted from the user's text.
It is available in the controller as this.nlu (initialized lazily). The data is filled by the platform
adapter from the request (for example, Alice sends it in request.nlu); if the platform does not
provide NLU, the data can be filled manually with setNlu().
All entity extraction methods return an object of the same shape:
interface INluResult<T = object> {
status: boolean; // whether at least one value was found
result: T | null; // the found values (null if nothing was found)
}
| Constant | Value | Description |
|---|---|---|
Nlu.T_FIO |
'YANDEX.FIO' |
Full name |
Nlu.T_GEO |
'YANDEX.GEO' |
Geolocation |
Nlu.T_DATETIME |
'YANDEX.DATETIME' |
Date and time |
Nlu.T_NUMBER |
'YANDEX.NUMBER' |
A number |
Nlu.T_INTENT_CONFIRM |
'YANDEX.CONFIRM' |
The consent intent |
Nlu.T_INTENT_REJECT |
'YANDEX.REJECT' |
The refusal intent |
Nlu.T_INTENT_HELP |
'YANDEX.HELP' |
The help intent |
Nlu.T_INTENT_REPEAT |
'YANDEX.REPEAT' |
The repeat intent |
| Method | Parameters | Return value | Description |
|---|---|---|---|
| getFio | - | INluResult<INluFIO[]> | The full name from the text (first_name, last_name, patronymic_name) |
| getGeo | - | INluResult<INluGeo[]> | Geolocation (country, city, street, house_number, airport, etc.) |
| getDateTime | - | INluResult<INluDateTime[]> | Date and time (year, month, day, hour, minute + the *_is_relative flags) |
| getNumber | - | INluResult<number[]> | Numbers from the text |
| getUserName | - | INluThisUser | null | Information about the current user (first_name, last_name, username), if the platform sent it |
| isIntentConfirm | userCommand?: string | boolean | Checks the consent intent; if there is no intent and a text is passed, an extra check by consent words (Russian "yes", "of course", etc.) |
| isIntentReject | userCommand?: string | boolean | Checks the refusal intent; a similar fallback check by refusal words (Russian "no", "I don't want to", etc.) |
| isIntentHelp | - | boolean | Checks the help intent |
| isIntentRepeat | - | boolean | Checks the repeat intent |
| getIntents | - | INluIntents | null | All intents of the request |
| getIntent | intentName: string | INluIntent | null | A specific intent by name (for example, 'YANDEX.CONFIRM') |
| getNluValue | - | INlu | The raw NLU object |
| setNlu | nlu: INlu, isClearCache?: boolean | void | Sets the NLU data; isClearCache: true resets the cache of extracted entities |
They work with arbitrary text and need no data from the platform:
| Method | Parameters | Return value | Description |
|---|---|---|---|
| Nlu.getLink | query: string | INluResult<string[] | null> | Extracts links from the text |
| Nlu.getPhone | query: string | INluResult<string[] | null> | Extracts phone numbers |
| Nlu.getEMail | query: string | INluResult<string[] | null> | Extracts email addresses |
const fio = this.nlu.getFio();
if (fio.status) {
this.text = `Nice to meet you, ${fio.result?.[0]?.first_name}!`;
}
// Static methods — for text without platform data
const phones = Nlu.getPhone(this.userCommand || '');
if (phones.status) {
this.userData.phone = phones.result?.[0];
}
The application configuration.
interface IAppConfig {
error_log?: string; // The path to the logs directory
json?: string; // The path to the JSON directory
db?: IAppDB; // The database configuration
isLocalStorage?: boolean; // Use local storage
memorySession?: IMemorySessionConfig | false; // An in-process userData session (platforms without localStorage, without a database)
env?: string; // The path to the .env file or 'local' for process.env
tokens?: ITokenPlatform; // Platform tokens (telegram, vk, etc.)
}
The application parameters.
interface IAppParam {
isAuthUser?: boolean; // Whether user authorization is required
welcome_text?: string | string[]; // The greeting text
help_text?: string | string[]; // The help text
empty_text?: string | string[]; // The text when no command matches
intents: IAppIntent[] | null; // The array of intents
utm_text?: string | null; // The UTM tag for links
}
The interface for storing user data.
interface IUserData {
oldIntentName?: string | null; // The name of the previous intent (null on reset)
[key: string]: unknown; // Additional data
}
const WELCOME_INTENT_NAME = 'welcome'; // The greeting intent
const HELP_INTENT_NAME = 'help'; // The help intent
const FALLBACK_COMMAND = '*'; // The fallback command (called when nothing matches)
⚠️
bot.addCommand(FALLBACK_COMMAND, [], cb)works because the pipeline looks up the fallback separately: after the commands and the intents fromsetPlatformParams, if none of them matched. A regular command with an empty slots array is silently not registered —addCommand('myCmd', [], cb)creates no triggers. The exception iswelcome/help, for which the framework uses default slots when the list is empty.
// The function that runs the next step in the middleware chain
type MiddlewareNext = () => Promise<void>;
// A middleware function
type MiddlewareFn = (ctx: BotController, next: MiddlewareNext) => void | Promise<void>;
// The parameters of a registered command
interface ICommandParam<TBotController extends BotController = BotController> {
slots?: TSlots; // Activation triggers (strings or RegExp)
isPattern?: boolean; // Interpret slots as RegExp
cb: (
userCommand: string,
botController: TBotController,
) => void | string | Promise<void | string>;
regExp?: RegExp; // The compiled RegExp (filled automatically)
isRegExpString: boolean; // The string RegExp flag
}
// The parameters of a step (a dialog chain)
interface IStepParam<TBotController extends BotController = BotController> {
stepName: string; // The unique step name
// false — the step does not apply, the lookup continues with commands; a string is the response text
cb: (botController: TBotController) => void | false | string | Promise<void | false | string>;
}
// The command slots type
type TSlots = (string | RegExp)[];
// A custom command resolver
type TCommandResolver = (
userCommand: string,
commands: Map<string, ICommandParam>,
) => string | null | Promise<string | null>;
addEvent)Declarative handling of non-text updates — like bot.on(':photo') in Telegram frameworks, but for all connected platforms at once. The adapter determines the event type and writes it to controller.eventType; addEvent handlers are called before steps and commands.
// Universal events (TEventType):
type TEventType =
| 'message' // a text message (the default)
| 'photo'
| 'voice'
| 'video'
| 'document'
| 'location'
| 'contact'
| 'sticker'
| 'callback' // an inline/callback button press
| 'inline' // an inline query (Telegram only for now)
| 'message_edited'
| 'channel_post'
| 'start' // the start of a dialog (the deep-link payload is in controller.payload)
| 'subscribed'
| 'unsubscribed'
| 'auth' // account linking completed (Alice account_linking)
| 'rating'; // a rating result (SmartApp)
// Event layer validators (exported from 'umbot'):
const ALL_EVENT_TYPES: readonly TEventType[]; // the list of all 17 universal events
function isEventType(event: string): event is TEventType; // true if the event name is known to the framework
Support is declared by the adapter itself (the supportedEvents field): Telegram — media/callback/inline/edited/channels; VK — message/callback; MAX — message/callback/start/edited; Viber — media types/start/subscribed/unsubscribed; Alice — message/auth; SmartApp — message/start/rating; Marusia — message/auth. The handler is simply not called where the event is physically impossible — a multi-platform bot does not break. A custom platform (BasePlatform) declares its own supportedEvents and takes part in the validation automatically: bot.addEvent warns if no connected adapter supports the event (the handler is still registered and starts working once the right platform is connected).
A summary table of supportedEvents by adapter (the values come from supportedEvents in the adapters' code):
| Platform | supportedEvents |
|---|---|
| Telegram | message, photo, voice, video, document, location, contact, sticker, callback, inline, message_edited, channel_post |
| VK | message, callback |
| MAX | message, callback, start, message_edited |
| Viber | message, photo, video, document, contact, location, sticker, start, subscribed, unsubscribed |
| Alice | message, auth |
| Marusia | message, auth |
| SmartApp | message, start, rating |
// A photo from the user — without parsing requestObject manually
bot.addEvent('photo', (ctx) => {
ctx.text = 'Great photo!';
});
// A button press (the payload is in ctx.payload)
bot.addEvent('callback', (ctx) => {
ctx.text = `You pressed: ${String(ctx.payload)}`;
});
// A "filter": intercept the message but hand it to the regular pipeline
bot.addEvent('message', (ctx) => {
if (ctx.userEvents?.auth?.status) return false;
ctx.text = 'Intercepted!';
});
// Events can also be handled in action() by controller.eventType
class MyController extends BotController {
action(intentName: string | null): void {
if (this.eventType === 'photo') this.text = 'A photo!';
}
}
Handler semantics:
false (including void) — the event is intercepted: a returned string becomes the response text; processing is finished, commands are not looked up;false — "not my event", the pipeline continues (steps → commands → intents → fallback); this is the only way to pass the request on to the regular pipeline;async handlers are supported, the framework awaits the result;false one.addAction)A callback button press handler by its payload — like bot.action() in competing frameworks. The button is created with a payload (buttons.addBtn('Buy', '', 'buy')), and the Telegram/VK/MAX adapters normalize the payload into a command name:
bot.addCommand('catalog', ['catalog'], (_, ctx) => {
ctx.text = 'Choose a product:';
ctx.buttons.addBtn('iPhone', '', 'buy').addBtn('MacBook', '', 'buy');
});
// Fires on pressing a button with the 'buy' payload
// (or the {"command":"buy"} payload): the button message goes as the 'buy' command
bot.addAction('buy', (_, ctx) => {
ctx.text = 'Placing your order...';
});
On platforms without callback buttons (Alice, Marusia) buttons send text that is matched by a regular slot — no handler is needed.
match)For commands with RegExp slots, isPattern patterns and a matched regex group, the handler gets the ready match in controller.match — without running the regex again:
bot.addCommand('order', [/(?:order|buy)\s+(\d+)/], (_, ctx) => {
ctx.text = `Placing order No. ${ctx.match?.[1]}`;
});
match is computed lazily: the framework remembers the regex of the matched command and runs it only on the first read — requests that do not use groups spend no time on it. For string commands match === null.
controller.api)Unified access to the active platform's capabilities from a handler — without constructing Request classes manually:
// A photo handler: send a photo in reply
bot.addEvent('photo', async (ctx) => {
await ctx.api?.sendPhoto('answer.jpg', { caption: 'Here is your report' });
ctx.skipAutoReply = true; // the reply has already been sent manually
});
// Acknowledging a button press on any callback platform
bot.addAction('buy', async (_, ctx) => {
await ctx.api?.answerCallback('Order placed!');
});
The facade methods:
| Method | Description | Telegram | VK | MAX | Viber |
|---|---|---|---|---|---|
| sendPhoto | Sends a photo (a local path, a URL or a file_id/attachment) | ✓ | ✓ (upload) | ✓ | — |
| sendDocument | Sends a file/document | ✓ | ✓ (upload) | ✓ | — |
| sendAudio | Sends audio | ✓ | — | ✓ | — |
| sendVideo | Sends video | ✓ | — | ✓ | — |
| answerCallback | A notification/snackbar in reply to a callback button press | ✓ | ✓ | ✓ | — |
| can(method) | Checks whether the current platform supports a method | ✓ | ✓ | ✓ | — |
The facade is a lazy object: it is created on the first access to ctx.api, and on voice platforms (Alice, SmartApp, Marusia) it is null (their reply is built as the webhook body — use card/sound). Unsupported methods log a warning and return null; support is checked in advance with can(). For Viber can() returns false for all methods — the Viber Bot API requires a URL and the file size, so the facade is not available there.
The facade is chosen by the adapter: the createApi(controller) method of the IPlatformAdapter contract (the BasePlatform base implementation returns null). A custom platform connects its own facade by overriding this method — it returns an object implementing IControllerApi; an example is in platform-integration.md, the "The platform API" section.
addForm)A multi-step form is a wrapper over steps: each field becomes a separate step, and the answers are collected into an object and passed to onComplete.
// A single form field
interface IAddFormField<TBotController extends BotController = BotController> {
name: string; // The field key in the answers object
prompt: string | ((ctx: TBotController) => string); // The question to the user
// true — accepted; false — repeat the prompt; string — the error text for the user
validate?: (value: string) => boolean | string | Promise<boolean | string>;
}
// addForm options
interface IAddFormOptions<TBotController extends BotController = BotController> {
fields: IAddFormField<TBotController>[]; // The fields, processed one after another
onComplete: (ctx: TBotController, answers: Record<string, string>) => void | Promise<void>; // Called after all fields are filled
cancelText?: string; // The text on cancel (by default 'Форма отменена.')
cancelCommands?: string[]; // The cancel commands (by default ['отмена', 'cancel'])
}
bot.addForm('registration', {
fields: [
{ name: 'name', prompt: 'What is your name?' },
{
name: 'email',
prompt: 'Enter your email',
validate: (v) => /\S+@\S+/.test(v) || 'Invalid email',
},
],
onComplete: (ctx, answers) => {
ctx.text = `Thank you, ${answers.name}! We saved your email: ${answers.email}`;
},
});
Intermediate answers are stored in
userData.__formdata_<name>. After the form is filled or cancelled, the field is set tonullrather than deleted: Alice removes a field fromuser_state_updateonly when its value isnull, while a field deleted withdeletewould remain in the user state.
removeForm('registration')removes the form and all its internal steps. The form step names have the__form_<name>_prefix, soremoveForm('user')also removes theuser_2form — use unique names.
// Creating an interactive button
getButton(
appContext: AppContext,
title: string | null,
url: string | null,
payload: TButtonPayload | null,
options?: IButtonOptions
): IButtonType | null
// Creating a link button
getLinkButton(
appContext: AppContext,
title: string | null,
url: string | null,
payload: TButtonPayload | null,
options?: IButtonOptions
): IButtonType | null
// Creating an image for a card
getImage(
appContext: AppContext,
image: string | null,
title: string,
desc = '',
button: TButton | null = null,
isToken = false
): IImageType | null
umbot/plugins)Helpers for those who write their own platform adapter or their own API client. Usage details
are in the adapter/platformAdapter.md document.
// A unified format of a platform API request error message
getErrorMsg(error: Error | string, path: string, url: string | null): string
// A unified format of a missing platform token message
getErrorToken(platform: string, methodName: string): string
// Building the API facade of a built-in platform by controller.appType
// (the core calls the adapter's createApi(); the dispatcher is kept for manual use)
makePlatformApi(controller: BotController): TApiFacade | null
The other adapter helpers are collected in the pUtils namespace (import { pUtils } from 'umbot/plugins'):
working with media tokens (getImageToken, getSoundToken), parsing incoming requests
(tryParse, normalizeActionPayload, getPlatformRequestData, setThisUserToNlu,
telegramMessageEvent, viberMessageEvent) and building the response (getChatText, getSpeechText,
getCorrectButtons, serializePlatformPayload).
// The request processing result
type TRunResult = object | string;
import { BotController, WELCOME_INTENT_NAME } from 'umbot';
class MyController extends BotController {
public action(intentName: string | null): void {
switch (intentName) {
case WELCOME_INTENT_NAME:
this.text = 'Hi! How can I help?';
this.buttons.addBtn('Help').addBtn('About the app');
break;
case 'about':
this.text = 'This is an example application built with umbot';
this.card.setTitle('About the app').addImage('image_token');
break;
default:
this.text = 'Sorry, I did not understand you';
break;
}
}
}
import { Bot } from 'umbot';
const bot = new Bot();
// Adding a simple command
bot.addCommand('greeting', ['hello', 'hi'], (_, bc) => 'Hi!');
// Adding a command with a callback
bot.addCommand(
'numbers',
['\\b\\d{3}\\b'],
(userCommand, botController) => {
botController.text = `You entered a number: ${userCommand}`;
},
true,
);
interface GameData extends IUserData {
score: number;
level: number;
example?: string;
result?: number | string;
isGame?: boolean;
}
class GameController extends BotController<GameData> {
public action(intentName: string | null): void {
// Initializing data on the first launch.
// The data must be merged, not overwritten:
// reassigning `this.userData = {...}` breaks saving to the database.
if (!this.userData.score) {
Object.assign(this.userData, {
score: 0,
level: 1,
});
}
// Handling commands
switch (intentName) {
case 'addScore':
this.userData.score += 10;
this.text = `Your score: ${this.userData.score}`;
break;
}
}
}
class ButtonController extends BotController {
public action(intentName: string | null): void {
switch (intentName) {
case 'showButtons':
// Adding buttons
this.buttons.addBtn('Help').addBtn('Back').addBtn('Exit');
this.text = 'Choose an action:';
break;
}
}
}
class CardController extends BotController {
public action(intentName: string | null): void {
switch (intentName) {
case 'showCard':
// Creating a card
this.card
.setTitle('Card title')
.addImage('image_token', ' ', 'Image description');
this.text = 'Here is your card:';
break;
}
}
}
class NluController extends BotController {
public action(intentName: string | null): void {
// Getting an intent from the NLU (for example, 'YANDEX.CONFIRM')
const nluIntent = this.nlu.getIntent('YANDEX.CONFIRM');
if (nluIntent) {
// nluIntent is an INluIntent object with a slots property
this.text = `Slots found: ${JSON.stringify(nluIntent.slots)}`;
} else {
this.text = 'Intent not found';
}
}
}
class AuthController extends BotController {
public action(intentName: string | null): void {
// Authorization check
if (this.isAuth) {
this.text = 'You are authorized';
this.userToken = this.userToken || 'default_token';
} else {
this.text = 'Authorization is required';
this.isAuth = true;
}
}
}
class RatingController extends BotController {
public action(intentName: string | null): void {
// Rating check
if (this.isSendRating) {
this.text = 'Thank you for your rating!';
this.isSendRating = false;
} else {
this.text = 'Please rate our service';
this.isSendRating = true;
}
}
}
The application context is the storage of the configuration, registries and connected modules. Each Bot instance has its own
context (bot.getAppContext()), so several bots in one process do not share settings.
| Property | Type | Description |
|---|---|---|
appConfig |
Required<IAppConfig> |
The current configuration (with all defaults) |
platformParams |
IAppParam |
Platform parameters |
platforms |
Record<TAppType, IPlatformAdapter> |
The registry of connected platforms |
database |
{ adapter?: IDatabaseAdapter, databaseInfo?: unknown, isSendConnect?: boolean } |
The connected DB adapter and connection information |
command |
CommandReg |
The command registry (the main access; handy getters below) |
commands |
Map<string, ICommandParam> |
All registered commands (a getter over command) |
steps |
Map<string, IStepParam> |
All registered steps (a getter over command) |
regexpGroup |
Map<string, IGroupData> |
Regex command groups (a getter over command) |
httpClient |
THttpClient |
The HTTP client (a public field, can be overridden) |
plugins |
TAppPlugin |
The plugin registry (the i18n, nlu, regExp slots + yours) |
| Method | Description |
|---|---|
log(...args) |
Logging |
logError(msg, meta?) |
Error logging |
logWarn(msg, meta?, options?) |
Warning logging. options.stderr: true — outside dev and without a custom logger, also write to stderr (for warnings that must not be missed) |
logMetric(name, value, label) |
Metric logging |
The component for page-by-page navigation through lists and menus.
import { Navigation } from 'umbot';
const nav = new Navigation(5); // 5 items per page
const elements = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Getting the items of the current page
const page = nav.getPageElements(elements);
// Navigation by commands (Navigation recognizes the Russian words)
nav.getPageElements(elements, 'дальше'); // the next page ("next")
nav.getPageElements(elements, 'назад'); // the previous page ("back")
// Finding an item: 'iPhone' must be on the CURRENT page (the window of the first
// maxVisibleElements items) — the similarity search runs only inside it
const item = nav.selectedElement(elements, 'iPhone', ['title']);
| Method | Parameters | Return value | Description |
|---|---|---|---|
getPageElements |
elements?: T[] | null, text?: string |
T[] |
The items of the current page; without elements — the last passed list (mutates thisPage on the Russian "next"/"back") |
selectedElement |
elements: T[] | null, text: string, keys?: TKeys | null, thisPage?: number | null |
T | null |
Finds an item by value (by number or by text similarity) — only among the items of the current page. When skipping later parameters, pass them explicitly as null |
getPageNav |
isNumber?: boolean |
string[] |
Pagination button labels: ['Дальше 👉']/['👈 Назад', 'Дальше 👉'] or ['[1]', '2', '3'] — the "back" label is not returned on the first page, "next" on the last; with a single page — ['[1]'] |
getPageInfo |
- | string |
Information about the current page: "N страница из M" ("page N of M"; an empty string for a single page) |
getMaxPage |
elements?: T[] | null |
number |
The number of pages |
numberPage |
text: string |
boolean |
Recognizes a command like "2 страница" (a page number in Russian) and goes there |
| Property | Type | Description |
|---|---|---|
thisPage |
number |
The current page number (0-indexed) |
maxVisibleElements |
number |
The maximum number of items per page |
Preloading media resources to platform servers.
import { Preload } from 'umbot/preload';
import { T_ALISA, T_TELEGRAM } from 'umbot/plugins';
const preload = new Preload(bot.getAppContext());
// Uploading images (Alice needs the skill's skill_id)
await Promise.all(preload.loadImages(['./img.jpg'], [T_ALISA], { alisaSkillId: 'your-skill-id' }));
// Uploading sounds
await Promise.all(preload.loadSounds(['./sound.mp3'], [T_ALISA], { alisaSkillId: 'your-skill-id' }));
// Telegram requires a recipient ID
await Promise.all(preload.loadImages(['./img.jpg'], [T_TELEGRAM], { telegramUseId: 123 }));
⚠️ Uploading happens only for platforms that have a token set (
appConfig.tokensor environment variables). Without configured tokens the methods return an empty array (no promises), and the upload silently does not happen. For unsupported platforms (Viber, SmartApp) promises do not get into the array at all.
optionsof all four methods:alisaSkillId— the skill id (the Alice resource API is addressed by skill, and outside a request there is nowhere to take it from; without it Alice is skipped both when uploading and when deleting);telegramUseId— the user Telegram will send the file to in order to get itsfile_id.
loadImages() is called, the framework checks whether the database already has a token for this file (the ImageTokens model).| Method | Parameters | Return value | Description |
|---|---|---|---|
loadImages |
paths: string[], platforms?: TAppType[], options? |
Promise<string | null>[] |
Uploads images (resolves with the image token or null on error) |
loadSounds |
paths: string[], platforms?: TAppType[], options? |
Promise<string | null>[] |
Uploads sounds (resolves with the sound token or null on error) |
removeImages |
paths: string[], platforms?: TAppType[], options? |
Promise<boolean>[] |
Deletes images (implemented only for Alice and Marusia; for other platforms the promise resolves with true without deleting anything) |
removeSounds |
paths: string[], platforms?: TAppType[], options? |
Promise<boolean>[] |
Deletes sounds (implemented only for Alice and Marusia; for other platforms the promise resolves with true without deleting anything) |
The result of loadImages/loadSounds is the token of the uploaded media or null if the upload failed: check
success with !== null.
The custom logger interface. All methods are optional.
interface ILogger {
log?(...args: unknown[]): void;
error?(message: string, meta?: Record<string, unknown>): void;
warn?(message: string, meta?: Record<string, unknown>): void;
metric?(name: string, value: unknown, labels?: Record<string, unknown>): void;
maskSecrets?: boolean; // true by default: secret masking is always on, it is disabled only by an explicit maskSecrets: false
}
Interfaces for framework extensions.
// A plugin class
interface IPlugin {
init: (appContext: AppContext, bot: Bot) => void;
destroy: (bot: Bot) => void | Promise<void>;
}
// A plugin function (recommended)
interface IPluginFn {
(appContext: AppContext, bot: Bot): void | ((bot: Bot) => void);
isPlugin: boolean; // REQUIRED: myPlugin.isPlugin = true;
}
Recommendation: instead of assigning
myPlugin.isPlugin = truemanually, use thecreatePlugin()helper — it sets the flag automatically, so you cannot forget it:import { createPlugin } from 'umbot';
const myPlugin = createPlugin((appContext, bot) => {
// initialization logic
return () => {
// cleanup logic on destruction
};
});
bot.use(myPlugin);
Constants of standard sounds and effects.
| Constant | Description |
|---|---|
S_AUDIO_GAME_WIN |
A victory sound |
S_AUDIO_GAME_LOSS |
A loss sound |
S_AUDIO_GAME_8_BIT_COIN |
A coin |
S_AUDIO_NATURE_RAIN |
Rain |
S_AUDIO_NATURE_SEA |
The sea |
S_EFFECT_HAMSTER |
The hamster effect (a high voice) |
S_EFFECT_MEGAPHONE |
The megaphone effect |
The full list is in src/components/sound/constants.ts.
A utility for working with strings.
| Method | Parameters | Return value | Description |
|---|---|---|---|
Text.resize |
text: string | null, size?: number, isEllipsis?: boolean |
string |
Trims a string to a length |
Text.getText |
str?: string | string[] |
string |
Picks a random element from an array |
Text.isSayText |
find: string | RegExp | (string | RegExp)[], text: string, isPattern?: boolean, useDirectRegExp?: boolean, customReg?: RegExpConstructor |
boolean |
Checks whether a slot matches the text |
The framework collects execution time metrics of key operations. To enable them, implement the metric() method in the logger.
| Metric | Constant | What it measures |
|---|---|---|
| Request time | EMetric.REQUEST |
The time of an outgoing HTTP request to the platform API (url, method, status in labels) |
| Webhook start | EMetric.START_WEBHOOK |
The moment processing starts (the value is a timestamp, not a duration) |
| Webhook time | EMetric.END_WEBHOOK |
The total webhook processing time (the incoming request) |
| Intent lookup | EMetric.GET_INTENT |
The time to find a matching intent |
| Command lookup | EMetric.GET_COMMAND |
The time to find a matching command |
| action execution | EMetric.ACTION |
The execution time of the controller's action() |
| Middleware | EMetric.MIDDLEWARE |
The execution time of the middleware chain |
| DB query (SELECT) | EMetric.DB_SELECT |
The SELECT execution time |
| DB query (INSERT) | EMetric.DB_INSERT |
The INSERT execution time |
| DB query (UPDATE) | EMetric.DB_UPDATE |
The UPDATE execution time |
| DB query (REMOVE) | EMetric.DB_REMOVE |
The DELETE execution time |
A connection example:
bot.setLogger({
metric: (name: string, value: unknown, meta?: Record<string, unknown>) => {
console.log(`[METRIC] ${name}: ${value}`, meta);
},
});
An output example:
[METRIC] umbot_get-command_duration_ms: 0.45 { commandName: 'weather', status: true }
[METRIC] umbot_action_duration_ms: 12.3 { commandName: 'weather', platform: 'telegram', isCommand: true }
The base class for working with data in the database. Extend it to create custom models (leaderboards, catalogs, etc.).
import { Model, IModelState, IModelRules, AppContext } from 'umbot';
interface IScoreState extends IModelState {
userId: string | null;
score: number | string | null; // string allows a text field label (attributeLabels)
}
const RULES: IModelRules[] = [
{ name: ['userId'], type: 'string', max: 250 },
{ name: ['score'], type: 'integer' },
];
export class ScoreModel extends Model<IScoreState> {
public static readonly TABLE_NAME = 'Scores';
constructor(appContext: AppContext) {
super(appContext);
this.state = { userId: null, score: null };
}
rules() {
return RULES;
}
attributeLabels() {
return { userId: 'ID', score: 'Score' };
}
tableName() {
return ScoreModel.TABLE_NAME;
}
}
Usage in a controller:
const score = new ScoreModel(this.appContext);
score.state.userId = String(this.userId);
if (await score.whereOne({ userId: score.state.userId })) {
score.state.score = Number(score.state.score) + 1;
await score.update();
} else {
score.state.score = 1;
await score.add();
}
| Method | Description |
|---|---|
add() |
Inserts a new record |
update() |
Updates the current record |
remove() |
Deletes the record |
whereOne(where?) |
Finds one record by conditions |
where(where?, isOne?) |
Finds records by conditions |
query(callback) |
A raw database query |
save(isNew?) |
Saves (add if isNew=true, otherwise update) |
If the primary key is unique only together with another field (like userId in UsersData — within a platform),
override the protected getUniqueKeys() method: the model adds these fields to the select/update/remove condition and passes
them to the adapter in IQuery.uniqueKeys.
protected getUniqueKeys(): string[] {
return ['platform'];
}
The built-in model for storing userData. Usually it is not used directly — the framework works with it automatically through controller.userData.
Built-in models for caching the tokens of uploaded media. They are managed by the framework automatically through Preload and the Card/Sound components.
When you connect MongoAdapter or FileAdapter, umbot automatically creates the following tables (collections).
UsersDataStores the state between requests for each user on each platform.
| Field | Type | Description |
|---|---|---|
userId |
string | number | The user ID (the record key together with platform) |
data |
Record<string, unknown> | The contents of ctx.userData — arbitrary JSON |
meta |
Record<string, unknown> | Metadata: when it was created, the last request, the platform |
platform |
string | The platform name ('telegram', 'alisa', ...) |
A record is identified by the userId + platform pair: Telegram user 42 and VK user 42 are different records
(before 3.1.4 the lookup used only userId, and such users shared one record). FileAdapter stores rows under the
<platform>:<userId> key and migrates rows of the old format on first access.
You do not need to create tables manually. The description of all built-in tables (fields, keys, indexes) is exported as
DB_TABLES_SCHEMA and passed to the DB adapter's ensureSchema() after connecting: FileAdapter creates the files itself,
MongoAdapter creates indexes ({ userId, platform }, { platform, path }; MongoDB creates the collections on the first
write), and an SQL adapter must create the tables (see the
external adapter specification).
ImageTokensA cache for images that need to be uploaded to the platform when they are sent.
| Field | Type | Description |
|---|---|---|
imageToken |
string | The unique image ID on the platform (primary key) |
path |
string | The local path or the CDN URL of the original |
platform |
string | The name of the platform it was uploaded for |
Reusing the same path does not re-upload the image.
SoundTokensThe ImageTokens equivalent for audio files.
| Field | Type | Description |
|---|---|---|
soundToken |
string | The unique sound ID on the platform (primary key) |
path |
string | The local path or the CDN URL of the original |
platform |
string | The platform name |
If you create your own model, extend Model:
import { Model, IModelState, IModelRules, AppContext } from 'umbot';
interface IMyState extends IModelState {
id: string | null;
name: string | null;
age: number | string | null; // string allows a text field label (attributeLabels)
}
const RULES: IModelRules[] = [
{ name: ['name'], type: 'string', max: 200 },
{ name: ['age'], type: 'integer' },
];
class MyTable extends Model<IMyState> {
constructor(appContext: AppContext) {
super(appContext);
this.state = { id: null, name: null, age: null };
}
rules() {
return RULES;
}
attributeLabels() {
return { id: 'ID', name: 'Name', age: 'Age' };
}
tableName() {
return 'my_table';
}
}
Note:
tableName(),rules()andattributeLabels()are public abstract methods; override them without theprotectedmodifier. The allowed field types inrules()are'text' | 'string' | 'integer' | 'int' | 'date' | 'bool'. The primary key is determined automatically by the'id'/'ID'label inattributeLabels().
| Provider | Notes |
|---|---|
| FileAdapter | A simple JSON file in ./json. Not thread-safe, for development/local tests only. |
| MongoAdapter | Production-ready. Uses the official mongodb v7 driver (Stable API v1) — compatible with current MongoDB Server versions. |
Tables, collections and indexes are created automatically: right after connecting to the database the framework calls
the adapter's ensureSchema(), before the first query.
umbot provides a root export as well as separate import paths for specific tasks:
// The main module — the core of the API
import {
Bot,
BotController,
BaseBotController,
AppContext,
WELCOME_INTENT_NAME,
HELP_INTENT_NAME,
FALLBACK_COMMAND,
IUserData,
IPlatformData,
IUserEvent,
TStatus,
IAppConfig,
IAppParam,
IAppIntent,
IAppDB,
ITokenPlatform,
ILogger,
TAppType,
TAppMode,
EMetric,
Buttons,
Card,
Sound,
Nlu,
Navigation,
SoundConstants,
IButton,
IButtonType,
IButtonOptions,
TButton,
IImageType,
IImageParams,
getImage,
ISound,
IEffect,
INlu,
INluFIO,
INluGeo,
INluDateTime,
INluThisUser,
INluIntents,
INluResult,
Model,
UsersData,
ImageTokens,
SoundTokens,
IModelRes,
IQuery,
IQueryData,
IModelRules,
IPlugin,
IPluginFn,
createPlugin,
Text,
getRegExp,
isRegex,
rand,
keysCount,
httpBuildQuery,
isPromise,
fread,
fwrite,
isFile,
saveData,
ICommandParam,
IStepParam,
TSlots,
TCommandResolver,
TBotControllerClass,
MiddlewareFn,
MiddlewareNext,
} from 'umbot';
// Platforms and DB adapters
import {
fullPlatforms,
voicePlatforms,
botPlatforms,
adapters,
AlisaAdapter,
TelegramAdapter,
VkAdapter,
ViberAdapter,
MaxAdapter,
MarusiaAdapter,
SmartAppAdapter,
FileAdapter,
MongoAdapter,
BaseDbAdapter,
BasePlatformAdapter,
TContent,
IAdapterOptions,
T_ALISA,
T_MARUSIA,
T_SMART_APP,
T_TELEGRAM,
T_VK,
T_VIBER,
T_MAX_APP,
AlisaConstants,
MarusiaConstants,
SmartAppConstants,
YandexRequest,
YandexImageRequest,
YandexSoundRequest,
YandexSpeechKit,
TelegramRequest,
VkRequest,
ViberRequest,
MaxRequest,
MarusiaRequest,
} from 'umbot/plugins';
// Middleware
import {
rateLimiter,
destroyRateLimiter,
RateLimitQueueOverflowError,
authGuard,
requestId,
maintenance,
ipFilter,
} from 'umbot/middleware';
// Utilities (Text is also available from 'umbot')
import { loadEnvFile } from 'umbot/utils';
// Local testing
import { BotTest, IBotTestParams } from 'umbot/test';
// Media preloading
import { Preload, IOptions as IPreloadOptions } from 'umbot/preload';
// The simplified start utility
import { run, IConfig, TMode } from 'umbot/build';
| Constant | Value | Purpose |
|---|---|---|
WELCOME_INTENT_NAME |
'welcome' |
The greeting intent name (messageId === 0) |
HELP_INTENT_NAME |
'help' |
The help intent name |
FALLBACK_COMMAND |
'*' |
The fallback command name |
T_ALISA |
'alisa' |
The Alice platform identifier |
T_MARUSIA |
'marusia' |
Marusia |
T_SMART_APP |
'smart_app' |
Sber SmartApp |
T_TELEGRAM |
'telegram' |
Telegram |
T_VK |
'vk' |
VK |
T_VIBER |
'viber' |
Viber |
T_MAX_APP |
'max_app' |
MAX |
Full reference — API v-3.1 · all versions.