umbot
    Preparing search index...

    The flow.json format of the umbot visual editor

    This page is machine-translated from the Russian original. If something reads oddly, the Russian version is the source of truth — open an issue.

    The full specification of the flow.json JSON format (an export from Umbot Flow) for the npx umbot create from-flow project generator. Based on this description you can build a generator for any platform (Telegram, Alisa, etc.).


    If you downloaded a JSON file from the visual editor, follow three steps:

    1. Install Node.js (version 22+ — the generated project requires it)
    2. Put the downloaded flow.json into any folder
    3. Run in the terminal:
    npx umbot create from-flow flow.json --output ./my-bot
    

    The finished TypeScript project appears in the my-bot folder. Install the dependencies, build and run it:

    cd my-bot
    npm install
    npm run build
    npm start

    A project generated with from-flow has no dev server with hot reload — there is no npm run dev script. For development, use the classic cycle: change the code → npm run build → npm start. The default/quiz templates created by the CLI do not provide hot reload either (they run the same way: npm run build && npm start), but when created in the dev mode, the entry point uses BotTest — interactive console debugging without an HTTP server.

    More about the CLI: umbot CLI documentation


    {
    "schemaVersion": "1.0",
    "name": "my-bot",
    "version": "1.0.0",
    "description": "Bot description",
    "platforms": ["telegram", "alisa"],
    "database": { "type": "file", "config": {} },
    "isLocalStorage": true,
    "nodes": [],
    "edges": [],
    "fallback": { "text": "I didn't get that" },
    "welcome": { "text": "Hi! I am a bot.", "buttons": [] },
    "helpText": { "text": "Bot help" },
    "variables": { "userName": "User name", "score": "Player score" }
    }
    Field Type Description
    schemaVersion string The format version. Defaults to "1.0".
    name string The project name (used in package.json and the title).
    version string The project version (semver).
    description string The bot description.
    platforms string[] Platforms: "telegram", "alisa", "marusia", "vk", "smart_app", "max_app", "viber".
    database object The database configuration (see below).
    isLocalStorage boolean Store userData in the platform's local storage (voice platforms) instead of a database.
    nodes array All graph nodes (commands, steps, conditions, actions, responses).
    edges array Connections between nodes.
    fallback object The response to unrecognized input: { "text": "..." }.
    mode string The application mode: "dev", "prod", "strict_prod" — bot.setAppMode(...) is generated. Without the field or with an unknown value — strict_prod.
    welcome object The greeting from the settings: { "text": "...", "buttons": [] }. Used if the scenario has no welcome node: fires on /start, the Russian "hi" and at the start of a dialog.
    helpText object The help text (optional): { "text": "..." }. Used if there is no help node: answers the Russian "help"/"what can you do".
    tokens object Platform tokens: { "telegram": "..." }. They are moved to the project's .env; you can leave them empty and fill in .env after generation.
    variables object Registered variables: { "name": "comment", ... }. A visual editor field — the generator ignores it.

    umbot has only 2 types of handlers:

    1. bot.addCommand(name, slots, handler) — reacts to trigger words (slots).
    2. bot.addStep(name, handler) — activated via ctrl.thisIntentName = 'stepName'.

    Actions, conditions and responses are INLINE CODE inside the handlers. Standalone blocks (response/action/condition) connected via edges are generated as separate reusable __<name>(ctrl: BotController) functions called from commands/steps (or from other blocks). A node without incoming edges does not get into the code at all.


    A command reacts to trigger words.

    {
    "type": "command",
    "id": "node_123",
    "name": "greeting",
    "slots": ["hello", "hi"],
    "isPattern": false,
    "saveTo": "userName",
    "actions": [],
    "conditions": [],
    "response": {
    "text": "Hi, {{userName}}!",
    "tts": "Hi!",
    "emotion": "good",
    "isEnd": false,
    "buttons": [],
    "card": null,
    "sounds": []
    }
    }
    Field Type Required Description
    type "command" yes The node type
    id string yes A unique ID
    name string yes The command name (in the generated code)
    slots string[] yes Trigger words (case-insensitive, see below)
    isPattern boolean no If true, the slots are regular expressions (case-sensitive)
    saveTo string no Save the input to userData
    varComment string no A variable comment (an editor field, the generator ignores it)
    actions ActionBlock[] no Inline actions
    conditions FlowCondition[] no Inline conditions
    response FlowResponse yes Response settings

    Slot case. The framework compares slots with the user's utterance, already converted to lower case, so the generator converts string slots to lower case: "Weather" from the editor becomes 'weather' in the code and matches "weather", "Weather" and "WEATHER". Regex slots (isPattern: true) stay as they are — \D and \d mean different things. Write them for lowercase text: ^code\d+$, not ^Code\d+$.

    A step asks for input and saves it.

    {
    "type": "step",
    "id": "node_456",
    "name": "ask_name",
    "prompt": {
    "text": "What is your name?",
    "tts": "",
    "emotion": "",
    "buttons": [],
    "card": null
    },
    "saveTo": "userName",
    "saveAs": "original",
    "actions": [],
    "conditions": []
    }
    Field Type Required Description
    type "step" yes The node type
    id string yes A unique ID
    name string yes The step name (for thisIntentName)
    prompt FlowPrompt yes The question text, TTS, buttons, card
    saveTo string yes The userData field to save to
    saveAs "original" | "lowercase" no The case of the saved input
    varComment string no A variable comment (an editor field, the generator ignores it)
    actions ActionBlock[] no Inline actions
    conditions FlowCondition[] no Inline conditions

    Navigation between steps happens via edges (connections), not via a next field.

    A condition checks a variable and leads along the True/False branches via edges.

    {
    "type": "condition",
    "id": "node_789",
    "name": "check_age",
    "variable": "age",
    "operator": "gte",
    "value": 18
    }
    Field Type Description
    type "condition" The node type
    id string A unique ID
    name string The node name (for thisIntentName)
    variable string The variable name from userData
    operator string The comparison operator (see the table below)
    value string|number The value to compare with (can be a variable name)

    responseTrue/responseFalse are not stored in a standalone condition node — they are defined via branch_true/branch_false edges.

    Operators:

    Operator Code Description
    eq a === b Equal
    neq a !== b Not equal
    gt Number(a) > Number(b) Greater than
    gte Number(a) >= Number(b) Greater than or equal
    lt Number(a) < Number(b) Less than
    lte Number(a) <= Number(b) Less than or equal
    contains String(a).includes(String(b)) Contains
    isEmpty !a Empty / undefined
    isNotEmpty !!a && a !== '' Not empty
    isSayTrue Text.isSayTrue(String(a)) The user said "yes" (in Russian)
    isSayFalse Text.isSayFalse(String(a)) The user said "no" (in Russian)
    isUrl Text.isUrl(String(a)) The user entered a URL

    The isSayTrue, isSayFalse and isUrl operators require importing Text from umbot.

    An action runs code (setting variables, numbers, HTTP).

    {
    "type": "action",
    "id": "node_101",
    "name": "generate_numbers",
    "actions": [
    { "type": "random_number", "field": "num1", "min": 1, "max": 10 },
    { "type": "set_variable", "field": "answer", "value": "num1 + num2" },
    {
    "type": "http_request",
    "url": "https://api.com",
    "method": "GET",
    "saveResponseTo": "data"
    }
    ],
    "text": "Your number: {{num1}}",
    "buttons": []
    }
    Field Type Description
    type "action" The node type
    id string A unique ID
    name string The node name (for thisIntentName)
    actions ActionBlock[] Action blocks
    text string The text after the actions run (supports {{}})
    buttons FlowButton[] Buttons after the actions run

    A response shows text, buttons and cards without asking for input.

    {
    "type": "response",
    "id": "node_202",
    "name": "show_help",
    "response": {
    "text": "This is the help.",
    "tts": "Bot help.",
    "isEnd": false,
    "buttons": [{ "title": "Back", "type": "action" }],
    "card": null,
    "sounds": []
    }
    }

    ⚠️ Not supported by the generator. An end node can appear in the editor export, but create from-flow ignores it — no code for ending the dialog (isEnd = true) is generated. If you need to end the dialog, add an action/response that sets isEnd manually in the generated code.

    { "type": "end", "id": "node_303" }
    

    {
    "text": "Response text",
    "tts": "Text for speech",
    "emotion": "good",
    "isEnd": false,
    "shuffleButtons": false,
    "buttons": [],
    "card": null,
    "sounds": []
    }

    The emotion and sounds fields are visual editor fields; the generator ignores them.

    {
    "text": "Question text",
    "tts": "",
    "emotion": "",
    "shuffleButtons": false,
    "buttons": [],
    "card": null
    }
    {
    "title": "Button text",
    "type": "action",
    "targetNodeId": "node_id",
    "url": "https://..."
    }
    Field Description
    type: "action" An action button. The generator currently ignores targetNodeId — only addBtn(title) is generated.
    type: "link" A link button. url is the URL.
    {
    "type": "gallery",
    "title": "Title",
    "images": [
    {
    "src": "https://example.com/photo.jpg",
    "title": "Name",
    "description": "Description",
    "button": { "title": "Buy", "type": "action", "targetNodeId": "..." }
    }
    ]
    }
    type Description
    single A single image
    list A list of images (vertical)
    gallery Horizontal scrolling

    The generator ignores the type field: for each element of images it generates ctrl.card.addImage(src, title, description) (if the element has a button, its text is added as the fourth argument), and each platform chooses the final card view (BigImage, ItemsList, ImageGallery, etc.) itself based on the number of images.

    { "type": "set_variable", "field": "name", "value": "userName", "fieldComment": "User name" }
    { "type": "random_number", "field": "num", "min": 1, "max": 100, "fieldComment": "A random number" }
    { "type": "http_request", "url": "https://api.com", "method": "GET", "headers": "{ \"Auth\": \"token\" }", "body": "{\"key\": \"{{var}}\"}", "saveResponseTo": "data" }
    Field Type Description
    type string "set_variable" / "random_number" / "http_request"
    field string The variable name in userData
    fieldComment string A variable comment (an editor field, the generator ignores it)
    value string The expression for set_variable (supports {{var}})
    min number The minimum for random_number (1 by default)
    max number The maximum for random_number (10 by default)
    url string The URL for http_request: {{var}} in the path and parameters (encoded), {{env.NAME}} anywhere
    method string The HTTP method: "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"
    headers string | object Headers: a JSON string or an object; values support {{var}} and {{env.NAME}}
    body string | object The request body: a JSON string or an object; strings support {{var}} and {{env.NAME}}
    saveResponseTo string Save the response to userData
    {
    "variable": "score",
    "operator": "gte",
    "value": 100,
    "responseTrue": { "text": "Victory!", "buttons": [] },
    "responseFalse": { "text": "Try again.", "buttons": [] }
    }

    Used for inline conditions in a command/step. Standalone condition nodes use edges.

    { "text": "Text", "buttons": [] }
    

    targetNodeId is set at the top level of the condition object ({ "variable": "...", "targetNodeId": "step_id" }), not inside the response: the generator reads it only there and only for the true branch (navigation via thisIntentName).


    {
    "from": "source_node_id",
    "to": "target_node_id",
    "type": "next",
    "label": ""
    }
    Type Description
    next A sequential transition
    branch_true The "yes" branch of a condition
    branch_false The "no" branch of a condition
    slot_match A slot match (reserved: the generator does not use this connection type currently)
    Command → Step (next)
    Step → Condition (next)
    Condition → Response (branch_true)
    Condition → Response (branch_false)
    Response → Step (next) — for loops
    Command → Response (next) — Response is generated as a __name(ctrl) function called from the command
    Command → Action (next) — Action is generated as a __name(ctrl) function called from the command
    Command → Condition (next) — generated by one of two mechanisms: a standalone Condition node
    (connected via an edge) becomes a separate __name(ctrl) function; inline conditions from the
    cmd.conditions array are embedded right in the command body (without a separate function)

    In JSON: {{variableName}} In the generated code: `${ctrl.userData.variableName}`

    "Hi, {{userName}}!"  →  setText(ctrl, `Hi, ${ctrl.userData.userName}!`)
    

    {{var}} substitution works in all text fields that go through textExpr: text (response, prompt, fallback, welcome, helpText) and the texts of action blocks. In value (set_variable), a value with {{}} is also substituted as a template string; arithmetic such as num1 + num2 (names without {{}}) is evaluated by a separate expression parser. The rules for url, headers and body of an http_request are in the "HTTP requests: variables and secrets" section. {{env.NAME}} is not expanded in response texts: an environment variable must not reach the user.


    {
    "type": "file",
    "config": {}
    }
    Type Description Generated code
    file File storage import { FileAdapter } from 'umbot/plugins'; bot.use(new FileAdapter()); — without arguments; the generator does not create a separate configuration file, the data path is set in the project's own appConfig.json section via bot.setAppConfig({ json: ... }), the config field is ignored here
    mongo MongoDB import { MongoAdapter } from 'umbot/plugins'; bot.use(new MongoAdapter({ host: '...', database: '...' })); — from config.host/config.database
    none No database Does not import an adapter

    For mongo: user/pass from database.config are not written to the source code — the generator moves them to the generated project's .env (the DB_USER/DB_PASSWORD variables), where the framework reads them from. An existing .env is not overwritten: only missing variables are appended. MongoAdapter connects to MongoDB by host/database.


    JSON:

    {
    "type": "command",
    "id": "n1",
    "name": "greeting",
    "slots": ["hello"],
    "response": { "text": "Hi!" }
    }

    Generated code:

    bot.addCommand('greeting', ['hello'], (cmd: string, ctrl: BotController): void => {
    setText(ctrl, 'Hi!');
    });

    JSON:

    [
    {
    "type": "command",
    "id": "n1",
    "name": "start",
    "slots": ["start"],
    "response": { "text": "What is your name?" }
    },
    {
    "type": "step",
    "id": "n2",
    "name": "enterName",
    "prompt": { "text": "What is your name?" },
    "saveTo": "userName"
    }
    ]

    (between the nodes — an edge { "from": "n1", "to": "n2", "type": "next" })

    Generated code:

    bot.addCommand('start', ['start'], (cmd: string, ctrl: BotController): void => {
    setText(ctrl, 'What is your name?');
    ctrl.thisIntentName = 'enterName';
    });
    bot.addStep('enterName', (ctrl: BotController): void => {
    setText(ctrl, 'What is your name?');
    ctrl.userData.userName = ctrl.originalUserCommand ?? ctrl.userCommand ?? '';
    });

    JSON:

    {
    "type": "condition",
    "id": "n3",
    "name": "check",
    "variable": "score",
    "operator": "gte",
    "value": 100
    }

    Generated code (a block function; called from a command/step connected by a next edge):

    /** Condition: check that score is greater than or equal to 100 */
    function __check(ctrl: BotController): void {
    if (Number(ctrl.userData.score) >= Number(100)) {
    __win(ctrl); // the block connected by the branch_true edge
    } else {
    __lose(ctrl); // the block connected by the branch_false edge
    }
    }

    JSON:

    {
    "type": "command",
    "id": "n4",
    "name": "gallery",
    "slots": ["gallery"],
    "response": {
    "text": "Choose:",
    "card": {
    "type": "gallery",
    "images": [
    { "src": "https://example.com/1.jpg", "title": "iPhone", "description": "999₽" }
    ]
    }
    }
    }

    Generated code:

    bot.addCommand('gallery', ['gallery'], (cmd: string, ctrl: BotController): void => {
    setText(ctrl, 'Choose:');
    ctrl.card.addImage('https://example.com/1.jpg', 'iPhone', '999₽');
    });

    JSON:

    {
    "type": "command",
    "id": "n5",
    "name": "menu",
    "slots": ["menu"],
    "response": {
    "text": "Choose:",
    "buttons": [{ "title": "Help", "type": "action", "targetNodeId": "help_step" }]
    }
    }

    Generated code:

    bot.addCommand('menu', ['menu'], (cmd: string, ctrl: BotController): void => {
    setText(ctrl, 'Choose:');
    ctrl.buttons.addBtn('Help');
    });

    targetNodeId of buttons is currently ignored: only addBtn(title) is generated (or addLink for type: "link").

    JSON:

    {
    "type": "command",
    "id": "n6",
    "name": "bye",
    "slots": ["bye"],
    "response": { "text": "Goodbye!", "isEnd": true }
    }

    Generated code:

    bot.addCommand('bye', ['bye'], (cmd: string, ctrl: BotController): void => {
    setText(ctrl, 'Goodbye!');
    ctrl.isEnd = true;
    });

    JSON:

    {
    "type": "command",
    "id": "n7",
    "name": "tts_demo",
    "slots": ["say it"],
    "response": { "text": "Text on the screen", "tts": "Text for speech" }
    }

    Generated code:

    bot.addCommand('tts_demo', ['say it'], (cmd: string, ctrl: BotController): void => {
    setText(ctrl, 'Text on the screen');
    setTTS(ctrl, 'Text for speech');
    });

    JSON:

    {
    "type": "action",
    "id": "n8",
    "name": "fetch_data",
    "actions": [
    {
    "type": "http_request",
    "url": "https://api.example.com/data",
    "method": "GET",
    "saveResponseTo": "apiResult"
    }
    ],
    "text": "Received: {{apiResult}}"
    }

    Generated code (a block function; called with await from a command/step connected by a next edge):

    /** Action: an HTTP request to https://api.example.com/data */
    async function __fetch_data(ctrl: BotController): Promise<void> {
    // fetchWithTimeout is a wrapper with a 2000 ms timeout, generated automatically in ./utils
    try {
    const response = await fetchWithTimeout('https://api.example.com/data');
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const responseText = await response.text();
    let data: unknown = null;
    if (responseText.trim()) {
    try {
    data = JSON.parse(responseText);
    } catch {
    data = responseText;
    }
    }
    ctrl.userData.apiResult = data;
    } catch (e) {
    const errorMessage = e instanceof Error ? e.message : String(e);
    setText(ctrl, `Request error: ${errorMessage}`);
    }
    setText(ctrl, `Received: ${ctrl.userData.apiResult}`);
    }

    JSON:

    {
    "type": "action",
    "id": "n9",
    "name": "roll",
    "actions": [{ "type": "random_number", "field": "dice", "min": 1, "max": 6 }],
    "text": "You rolled: {{dice}}"
    }

    Generated code:

    /** Action: generate a random number into dice */
    function __roll(ctrl: BotController): void {
    ctrl.userData.dice = rand(1, 6);
    setText(ctrl, `You rolled: ${ctrl.userData.dice}`);
    }

    JSON:

    {
    "type": "action",
    "id": "n10",
    "name": "set_score",
    "actions": [{ "type": "set_variable", "field": "score", "value": "0" }]
    }

    Generated code:

    /** Action: set the score variable */
    function __set_score(ctrl: BotController): void {
    ctrl.userData.score = 0;
    }

    JSON:

    { "type": "response", "id": "n11", "name": "show_help", "response": { "text": "This is the help." } }
    

    Generated code (a block function; called from a command/step connected by a next edge):

    /** Response: "This is the help." */
    function __show_help(ctrl: BotController): void {
    setText(ctrl, 'This is the help.');
    }

    JSON:

    {
    "type": "http_request",
    "url": "https://api.com",
    "method": "POST",
    "body": "{\"user\": \"{{userName}}\", \"score\": \"{{score}}\"}",
    "saveResponseTo": "result"
    }

    Generated code:

    // body is valid JSON: the generator parses it and rebuilds it with
    // JSON.stringify, substituting the variables; a template literal is only a fallback for invalid JSON
    const response = await fetchWithTimeout('https://api.com', {
    method: 'POST',
    body: JSON.stringify({ user: `${ctrl.userData.userName}`, score: `${ctrl.userData.score}` }),
    headers: { 'Content-Type': 'application/json' }, // added when there is a body, unless headers has its own Content-Type
    });
    // The response is read as text and parsed, falling back to the raw string for invalid JSON
    const responseText = await response.text();
    let data: unknown;
    try {
    data = JSON.parse(responseText);
    } catch {
    data = responseText;
    }
    ctrl.userData.result = data; // of type unknown, not a Promise

    Two substitutions work in the url, headers and body of an http_request:

    • {{var}} — a scenario variable (or the system {{__currentDate}}). In url the value is encoded with encodeURIComponent, so user input cannot add its own path or parameter to the request. A scenario variable is allowed only after the server address — in the path and parameters: https://api.example.com/weather/{{city}}. https://{{host}}/… or {{url}} is a validate error: otherwise the bot user would choose the server the bot talks to.
    • {{env.NAME}} — an environment variable (the name: Latin letters, digits, _). The value is substituted as is, including in the server address: {{env.API_BASE}}/items/{{id}}. In code it is env('NAME') from ./utils: first the process environment (Docker, serverless), then the project's .env. The variable is added to .env as an empty string if it is not there.

    Secrets do not get into the code. If the value of a header, a URL parameter or a JSON body key with a "secret" name (Authorization, Cookie, token, secret, password, api_key, X-Api-Key, key, etc.) is written in flow.json in plain text, the generator moves it to .env as HTTP_<NAME> (HTTP_AUTHORIZATION, HTTP_API_KEY), and writes env('HTTP_…') in src/index.ts — with a warning in the console. Identical values get one variable, different ones get the _2, _3 suffix. A secret in a non-JSON body is not detected — use {{env.NAME}} explicitly.

    {
    "type": "http_request",
    "url": "https://api.example.com/weather/{{city}}?key={{env.WEATHER_KEY}}",
    "method": "GET",
    "headers": { "Authorization": "Bearer sk-live-123" },
    "saveResponseTo": "weather"
    }
    // .env: HTTP_AUTHORIZATION=Bearer sk-live-123, WEATHER_KEY=
    const response = await fetchWithTimeout(
    `https://api.example.com/weather/${encodeURIComponent(String(ctrl.userData.city ?? ''))}?key=${env('WEATHER_KEY')}`,
    { headers: { Authorization: `${env('HTTP_AUTHORIZATION')}` } },
    );

    import { Bot, BotController, FALLBACK_COMMAND } from 'umbot';
    import { fullPlatforms } from 'umbot/plugins'; // if 7 platforms
    import { Text } from 'umbot'; // only with isSayTrue/isSayFalse/isUrl
    import { setText, setTTS, fetchWithTimeout } from './utils'; // conditionally
    import { FileAdapter, MongoAdapter } from 'umbot/plugins'; // by database.type
    Condition Import
    Any node has TTS import { setText, setTTS } from './utils'
    No TTS import { setText } from './utils'
    There are http_request actions import { fetchWithTimeout } from './utils' (generated by the CLI)
    Requests use {{env.NAME}} import { env } from './utils' (reads the environment and .env)
    There is isSayTrue/isSayFalse/isUrl import { Text } from 'umbot'
    There are random_number actions import { rand } from 'umbot/utils'
    database.type === 'file' import { FileAdapter } from 'umbot/plugins'
    database.type === 'mongo' import { MongoAdapter } from 'umbot/plugins'
    All 7 platforms (or an empty list) import { fullPlatforms } from 'umbot/plugins'
    Voice platforms only import { voicePlatforms } from 'umbot/plugins'
    Chat platforms only import { botPlatforms } from 'umbot/plugins'
    A mixed set of platforms import { TelegramAdapter, VkAdapter, ... } from 'umbot/plugins'

    • Node names (name) are used as command identifiers in the generated code. For standalone blocks the function name __ + name is generated: non-ASCII characters are replaced with _ (replace(/[^a-zA-Z0-9_$]/g, '_')), a name starting with a digit gets the _ prefix; on name collisions the _2, _3, ... suffix is added; an empty name is replaced with _block. To be safe, use ASCII identifiers without spaces.
    • Variable names (saveTo, field) — names that are not a valid JS identifier (for example, starting with a digit) are wrapped in brackets: ctrl.userData['123field']
    • Package name — starts with a letter, a valid npm identifier