Перейти к основному содержимому

Промт для ИИ: редактирование JSON сценария бота

Ты редактируешь JSON-структуру Telegram-бота. Ниже — полное описание формата.


Верхний уровень (BotDataWithSheets)

{
"version": 2,
"activeSheetId": "sheet-id",
"sheets": [ ...листы... ]
}
ПолеТипОписание
versionnumberВерсия формата, всегда 2
activeSheetIdstringID активного листа
sheetsSheet[]Массив листов холста

Лист (Sheet)

{
"id": "FnLYhGUixLRUzeobS3May",
"name": "Старт / Меню",
"nodes": [ ...узлы... ],
"viewState": { "pan": { "x": 0, "y": 0 }, "zoom": 100 }
}

Узел (Node)

{
"id": "уникальный-id",
"type": "тип_узла",
"position": { "x": 100, "y": 200 },
"data": { ...данные зависят от типа... }
}

Рекомендации по position:

  • Шаг по X: 300 (между связанными узлами по горизонтали)
  • Шаг по Y: 200 (между параллельными ветками)
  • ID генерировать через nanoid (21 символ) или осмысленные slug'и

🧭 Авто-раскладка (db_auto_layout): если не хочешь вручную считать координаты, добавляй ноды с любыми position и затем вызови MCP-тул db_auto_layout — он пересчитает позиции всех нод листа иерархической раскладкой (как кнопка «Авто-расстановка» на холсте), меняя только координаты. Data, связи и состав листов не затрагиваются. Параметры: project_id (обязателен), sheet_id (опц., по умолчанию активный/первый), commit_message (опц.). Связи для раскладки выводятся из data нод автоматически; изолированные ноды получают позицию справа.


Типы триггеров

ТипКогда срабатывает
command_triggerКоманда /start, /help и т.д.
text_triggerТекстовое сообщение (точное / содержит)
incoming_message_triggerЛюбое входящее сообщение (с фильтрами чата)
incoming_callback_triggerCallback от inline-кнопки
callback_triggerКонкретный callback_data
group_message_triggerСообщение в группе
member_triggerВход или выход участника из группы
managed_bot_updated_triggerОбновление управляемого бота
schedule_triggerЗапуск по расписанию (интервал / cron)
api_triggerВходящий HTTP-запрос от внешней системы
api_responseHTTP-ответ на api_trigger

Catch-all обработчики (CATCH_ALL_HANDLERS)

Флаг catchAllHandlers (env CATCH_ALL_HANDLERS, значения 0/1, по умолчанию 1) управляет генерацией универсальных catch-all обработчиков handle_unhandled_message, handle_unhandled_photo, fallback_callback_handler.

Действует предохранитель-автодетект: при наличии incoming_message_trigger, incoming_callback_trigger или динамических кнопок catch-all генерируются принудительно, даже если флаг выключен — иначе в aiogram 3 middleware этих триггеров не срабатывает. Формула: generateCatchAll = (catchAllHandlers !== 0) || есть incoming-триггеры/динамические кнопки.

Защита контента (PROTECT_CONTENT)

Флаг protectContent (env PROTECT_CONTENT, значения 0/1, по умолчанию 0) управляет генерацией обёртки защиты контента от копирования/пересылки (PROTECT_CONTENT = os.getenv(...), _protect_content_kwargs, _wrap_bot_protect_content, добавляющая protect_content=True ко всем исходящим методам бота).

Код защиты теперь генерируется только при включённом флаге (protectContent=true). При выключенной защите (protectContent=0/false или флаг не задан) этот блок не попадает в сгенерированный код вовсе — раньше он генерировался всегда и лишь активировался рантайм-флагом env.

Живое обновление контента (CONTENT_CACHE)

Флаг contentCache (env CONTENT_CACHE, значения 0/1, по умолчанию 0) управляет генерацией машинерии «живого» обновления контента из таблицы _content: функций load_content, reload_content, фоновых задач _content_reload_loop (перезагрузка кэша каждые 60 сек) и _content_subscribe_redis (мгновенное обновление через Redis pub/sub), а также их вызовов в main().

Важно: аксессор get_content(key, fallback) и кэш _content_cache генерируются всегда (при заданном projectId), потому что текст каждой ноды обращается к get_content(...). При contentCache=0 машинерия live-reload не генерируется, кэш остаётся пустым, и get_content всегда возвращает вшитый в код fallback-текст — живого обновления нет, но код не ломается. Формула: generateContent = (contentCache === true).

Поля command_trigger

{
"type": "command_trigger",
"data": {
"command": "/start",
"description": "Запустить бота",
"showInMenu": true,
"autoTransitionTo": "nodeId"
}
}

Поля text_trigger

{
"type": "text_trigger",
"data": {
"textMatchType": "exact",
"textSynonyms": ["привет", "хай"],
"autoTransitionTo": "nodeId"
}
}

textMatchType: "exact" — точное совпадение, "contains" — содержит подстроку.

Поля callback_trigger

{
"type": "callback_trigger",
"data": {
"callbackData": "approve_",
"matchType": "startswith",
"autoTransitionTo": "next_node_id",
"callbackParseTemplate": "approve_{applicant_id}",
"callbackSaveVariables": [
{ "templateVar": "applicant_id", "saveAs": "_cb_dynamic_id" }
]
}
}
ПолеОписание
callbackDataПаттерн callback_data для перехвата
matchType"exact" — точное совпадение, "startswith" — начинается с
callbackParseTemplateШаблон для извлечения переменных из callback_data
callbackSaveVariablesМассив [{templateVar, saveAs}] — какие переменные извлечь и куда сохранить
autoTransitionToID следующего узла
adminOnlyТолько для администраторов

Паттерн: кросс-пользовательский сценарий

Когда один пользователь (админ) действует в контексте другого (заявитель), используй callback_trigger с startsWith + callbackSaveVariables:

  1. Кнопка: customCallbackData: "approve_{user_id}" — вшивает ID заявителя в callback_data
  2. callback_trigger с matchType: "startswith" — ловит callback, извлекает ID в переменную
  3. Далее bot_table READ по извлечённому ID — загружает данные заявителя
  4. Далее edit_message / message — использует загруженные данные

Не нужно менять движок — используй существующие ноды. callback_trigger — универсальный перехватчик динамических callback'ов.

Если кнопка с customCallbackData: "profile:name:{user_id}" ведёт на input / bot_table / другую ноду без собственного startswith — генератор сам создаёт виртуальный триггер startswith("profile:name:"). Для message и edit_message виртуальный триггер не дублируется.

Поля incoming_callback_trigger

{
"type": "incoming_callback_trigger",
"data": {
"callbackData": "work_",
"matchType": "startsWith",
"callbackDataStripPrefix": "work_",
"callbackDataSaveAs": "callback_data",
"autoTransitionTo": "nodeId",
"enableAutoTransition": true
}
}
ПолеОписание
callbackDataПаттерн для фильтрации callback_data
matchTypeВАЖНО: camelCase! "startsWith" / "equals" / "contains"
callbackDataStripPrefixПрефикс для удаления из callback_data перед сохранением (опционально)
callbackDataSaveAsИмя переменной куда сохранять callback_data (по умолчанию "callback_data")

⚠️ Критично: matchType должен быть в camelCase: "startsWith", НЕ "startswith". При неправильном регистре фильтрация не применяется и middleware перехватывает ВСЕ callback_query.

Поля incoming_message_trigger

{
"type": "incoming_message_trigger",
"data": {
"imtChatTypeFilter": "group",
"imtGroupChatId": "2300967595",
"imtGroupChatIdSource": "manual",
"imtStopOnFlag": true,
"autoTransitionTo": "guard_handler_node"
}
}
ПолеОписание
imtChatTypeFilter"any" / "private" / "group" — фильтр типа чата
imtGroupChatIdID группы без префикса -100 (при imtChatTypeFilter: "group")
imtGroupChatIdSource"manual" — из imtGroupChatId, "variable" — из groupChatVariableName
groupChatVariableNameИмя переменной с ID группы (при imtGroupChatIdSource: "variable")
imtStopOnFlagПо умолчанию true. Учитывать флаг _stop_processing от узла stop_processing
autoTransitionToID целевого узла

Пример охраны группы: incoming_message_trigger (фильтр group + chat id) → stop_processing → дальнейшая логика.

Поля member_trigger

{
"type": "member_trigger",
"data": {
"memberEventType": "join",
"groupChatId": "2300967595",
"groupChatIdSource": "manual",
"saveJoinedUserIdTo": "joined_user_id",
"saveJoinedUsernameTo": "joined_username",
"saveLeftUserIdTo": "left_user_id",
"saveLeftUsernameTo": "left_username",
"autoTransitionTo": "welcome_node"
}
}
ПолеОписание
memberEventType"join" / "leave" / "both"
groupChatIdОпциональный фильтр по ID группы (без -100)
groupChatIdSource"manual" / "variable"
groupChatVariableNameПеременная с ID группы (при groupChatIdSource: "variable")
saveJoinedUserIdToПеременная для ID вошедшего (по умолчанию joined_user_id)
saveJoinedUsernameToПеременная для username вошедшего
saveLeftUserIdToПеременная для ID вышедшего
saveLeftUsernameToПеременная для username вышедшего
autoTransitionToID следующего узла

Поля schedule_trigger

{
"type": "schedule_trigger",
"data": {
"rules": [
{ "mode": "interval", "intervalMinutes": 5 }
],
"timezone": "Europe/Moscow",
"autoTransitionTo": "nodeId",
"runOnStart": false,
"enabled": true,
"maxConcurrent": 1
}
}

Режимы расписания (rules[].mode):

  • "interval" — каждые N минут (intervalMinutes)
  • "daily" — ежедневно в указанное время (hour, minute)
  • "weekly" — по дням недели (weekdays: [0-6], hour, minute)
  • "cron" — cron-выражение (cronExpression)

Поля api_trigger

{
"type": "api_trigger",
"data": {
"apiMethod": "POST",
"apiPath": "/payment",
"apiSecretToken": "сгенерированный-secret",
"apiSaveBodyTo": "body",
"apiSaveQueryTo": "",
"apiSaveHeadersTo": "",
"apiParseJson": true,
"autoTransitionTo": "nodeId"
}
}
  • Публичный URL: {API_BASE_URL}/api/hooks/{projectId}{apiPath}
  • Secret только в заголовке X-Api-Secret или Authorization: Bearer
  • Уникальность пары (apiMethod, apiPath) в проекте
  • Запрещены пути с /api/, /webhook, ..

Поля api_response

{
"type": "api_response",
"data": {
"apiResponseStatusCode": 200,
"apiResponseBody": "{\"ok\":true}",
"apiResponseContentType": "application/json",
"apiResponseHeaders": [],
"autoTransitionTo": ""
}
}

Завершает HTTP-запрос, инициированный api_trigger. Без этой ноды — ответ 200 {"ok":true}, таймаут 30 с → 504.


Типы контент-узлов

message — отправить сообщение

{
"type": "message",
"data": {
"messageText": "Текст сообщения, поддерживает {переменные}",
"markdown": false,
"keyboardType": "none",
"buttons": [],
"conditionalMessages": [],
"enableConditionalMessages": false,
"adminOnly": false,
"requiresAuth": false,
"isPrivateOnly": false,
"enableStatistics": true
}
}

Запрещено использовать collectUserInput, inputVariable, enableTextInput, enablePhotoInput и другие поля сбора ввода в message-ноде. Для сбора ввода используйте только отдельную ноду input.

Форматирование текста (formatMode)

ЗначениеОписание
"none"Без форматирования (по умолчанию)
"html"HTML-разметка (<b>, <i>, <code>, <a href="">)
"markdown"Markdown (*bold*, _italic_, `code`)

Важно при formatMode: "html":

  • Символы <, >, & в тексте нужно экранировать: &lt;, &gt;, &amp;
  • Нельзя использовать < для сравнения (например "реп < 50") — Telegram парсит как тег
  • Допустимые теги: <b>, <i>, <u>, <s>, <code>, <pre>, <a href="...">, <tg-spoiler>
  • Символ (bullet) безопасен, но не используйте его рядом с < или >
  • HTML mention пользователя: <a href='tg://user?id={user_id}'>{user.nickname}</a> — если в тексте используется HTML mention, обязательно ставить "formatMode": "html"

ℹ️ Hot-reload и formatMode: в режиме горячей перезагрузки контента (генерация с projectId/contentCache) статичный formatMode ноды учитывается напрямую: markdownparse_mode="Markdown", htmlparse_mode="HTML". При formatMode: "none" (или отсутствии поля) работает автодетект — parse_mode="HTML" подставляется, если в тексте найдены HTML-теги (<b>, <i>, <u>, <s>, <code>, <pre>, <a , <tg-spoiler>), иначе parse_mode=None.

http_request — HTTP запрос

{
"type": "http_request",
"data": {
"httpRequestUrl": "https://api.example.com/data?id={user_id}",
"httpRequestMethod": "GET",
"httpRequestHeaders": "{\"Authorization\": \"Bearer {token}\"}",
"httpRequestBody": "{\"key\": \"{value}\"}",
"httpRequestTimeout": 30,
"httpRequestResponseVariable": "response_var",
"autoTransitionTo": "next_node_id"
}
}

Переменные в URL, заголовках, теле и Auth задаются через {имя}: переменные сценария + env бота (вкладка Бот → env). Сценарий перекрывает одноимённые ключи env.

Bearer из env бота

Секрет хранится в env токена (API_TOKEN=...), в ноде — только плейсхолдер:

{
"type": "http_request",
"data": {
"httpRequestUrl": "https://api.example.com/me",
"httpRequestMethod": "GET",
"httpRequestAuthType": "bearer",
"httpRequestAuthBearerToken": "{API_TOKEN}",
"httpRequestResponseVariable": "me_response",
"autoTransitionTo": "next_node_id",
"enableAutoTransition": true
}
}

Получение файла (base64)

{
"type": "http_request",
"data": {
"httpRequestUrl": "https://api.example.com/export/project.json",
"httpRequestMethod": "GET",
"httpRequestResponseFormat": "file",
"httpRequestResponseVariable": "export_file",
"autoTransitionTo": "send-file-node"
}
}

При httpRequestResponseFormat: "file" ответ сохраняется как объект:

{
"type": "file",
"data": "base64...",
"mimeType": "application/json",
"fileName": "project.json"
}

Этот объект можно передать в медиа-ноду через {export_file} для отправки файла пользователю.

condition — ветвление

{
"type": "condition",
"data": {
"variable": "имя_переменной",
"branches": [
{
"id": "branch-1",
"label": "Равно 1",
"operator": "equals",
"value": "1",
"target": "node_id_if_true"
},
{
"id": "branch-else",
"label": "Иначе",
"operator": "else",
"value": "",
"target": "node_id_else"
}
]
}
}

Строковые операторы: equals, not_equals, contains, not_contains, starts_with, ends_with, matches_regex, filled, empty, else

Числовые операторы: greater_than, less_than, between, is_even, is_odd, divisible_by (работают, только если значение можно привести к числу; is_even/is_odd не требуют value, divisible_by использует value как делитель)

Системные операторы (не требуют variable): is_private, is_group, is_channel, is_admin, is_premium, is_bot, is_subscribed, is_not_subscribed

⚠️ КРИТИЧНО: Операторы not_empty, is_empty, is_not_emptyНЕ СУЩЕСТВУЮТ в шаблоне condition. Используй filled вместо not_empty/is_not_empty, empty вместо is_empty.

⚠️ Формат data: Обязательные поля: variable (строка — имя переменной) и branches (массив веток). НИКОГДА не использовать conditions + defaultTarget — такого формата не существует.

Значения в value и value2 поддерживают переменные: {user.balance}, {item.price}, {now_ts} и т.д. Переменные раскрываются в рантайме через replace_variables_in_text.

set_variable — установка переменных

Узел для задания, изменения и вычисления переменных без HTTP-запроса.

{
"type": "set_variable",
"data": {
"assignments": [
{ "id": "assign_1", "variable": "score", "value": "0", "mode": "text" }
],
"autoTransitionTo": "next_node_id",
"enableAutoTransition": true
}
}

Режимы присваивания (mode)

modeОписаниеПример value
textПростая подстановка строки/переменных"Привет, {first_name}"
expressionАрифметическое выражение (поддерживает +, -, *, /, //, %, **)"{balance} - 50"
randomСлучайное целое число в диапазонеvalue: "500", maxValue: "900"
random_itemСлучайный элемент из списка (через запятую)"🔧,💥,💡,⚡,🔨"
array_itemЭлемент массива/объекта по индексу или ключуvalue: "{items}", maxValue: "0" или "data.user.name"
timestampUnix timestamp (текущее время + смещение в секундах)"90" (= сейчас + 90 сек)
format_durationФорматирование секунд в MM:SS или HH:MM:SS"{cd.expires_at} - {now_ts}"
format_numberФорматирование числа с разделителями (пробелами)"{pilot.credits}" → "5 000 000"
lookupПоиск значения в таблице-переменной
str_replaceЗамена подстроки"старый_текст" + replaceWith: "новый"
json_pushДобавить объект в массив-переменную"{\"name\": \"{item.name}\"}"
json_formatФорматировать массив в строкушаблон строки
regex_extractИзвлечение по регулярному выражениюvalue: "{source}", pattern: "(\\d+)\\s*руб", regexGroup: "1"
extract_numberИзвлечение первого числа из строки"{bot_response}" → "7216970"
split_getРазделить строку и взять N-й элементvalue: "{email}", separator: "@", maxValue: "1"
json_getЗначение из JSON по пути (dot notation)value: "{api_response}", jsonPath: "data.user.name"
substringПодстрока (start, end)value: "{id}", startIndex: "0", endIndex: "8"
conditionalТернарный оператор (если/иначе)conditionVariable: "{balance}", conditionOperator: "greater_than", conditionValue: "1000", trueValue: "💰", falseValue: "💸"
lowercaseВ нижний регистр"{user_input}"
uppercaseВ верхний регистр"{user_input}"
trimУбрать пробелы по краям"{user_input}"
lengthДлина строки или массива"{inventory}" → "3"
array_concatОбъединить два массива в один. Поля: value (первый массив), concatWith (второй массив). Результат: склеенный массив.value: "{arr1}", concatWith: "{arr2}"

Сохранение в PostgreSQL (persistToDb)

В каждом элементе assignments можно указать "persistToDb": true — после присваивания значение дополнительно сохраняется в PostgreSQL через set_user_var (требуется включённая пользовательская БД бота):

{
"id": "a1",
"variable": "reputation",
"value": "{reputation} + 10",
"mode": "expression",
"persistToDb": true
}

Примеры

Случайная зарплата от 500 до 900:

{ "id": "a0", "variable": "salary", "value": "500", "maxValue": "900", "mode": "random" }

Случайный эмодзи из списка:

{ "id": "a0", "variable": "target_emoji", "value": "🔧,💥,💡,⚡,🔨,🌋", "mode": "random_item" }

Установить кулдаун на 90 секунд вперёд:

{ "id": "a0", "variable": "cooldown_until", "value": "90", "mode": "timestamp" }

Текущий Unix timestamp (без смещения):

{ "id": "a0", "variable": "now_ts", "value": "0", "mode": "timestamp" }

Форматирование оставшегося времени кулдауна в MM:SS:

{ "id": "a0", "variable": "cd_text", "value": "{cd.expires_at} - {now_ts}", "mode": "format_duration" }

Результат: "01:30" (если осталось 90 секунд) или "01:05:30" (если больше часа).

Начислить 10 к репутации:

{ "id": "a1", "variable": "reputation", "value": "{reputation} + 10", "mode": "expression" }

Извлечь число (курс) из текста ответа бота:

{ "id": "a1", "variable": "rate", "value": "{bot_response}", "mode": "regex_extract", "pattern": "(\\d+)\\s*рублей", "regexGroup": "1" }

Извлечь ID заказа из строки:

{ "id": "a1", "variable": "order_id", "value": "{text}", "mode": "regex_extract", "pattern": "#(\\d+)", "regexGroup": "1" }

⚠️ ВАЖНО для regex_extract: value — это ИСТОЧНИК текста (откуда извлекать), pattern — это REGEX паттерн. НЕ путать! Поле regexSource НЕ существует — используй только value + pattern.

Списать 50 🍪:

{ "id": "a2", "variable": "balance", "value": "{balance} - 50", "mode": "expression" }

Установить текст:

{ "id": "a3", "variable": "status", "value": "VIP", "mode": "text" }

Замена подстроки:

{ "id": "a4", "variable": "bio", "value": "плохое_слово", "mode": "str_replace", "replaceWith": "***" }

Элемент массива по индексу:

{ "id": "a0", "variable": "first_item", "value": "{items_list}", "maxValue": "0", "mode": "array_item" }

Вложенный доступ через dot-notation:

{ "id": "a0", "variable": "user_name", "value": "{api_response}", "maxValue": "data.users.0.name", "mode": "array_item" }

Объединение двух массивов:

{ "id": "a0", "variable": "all_items", "value": "{active_items}", "mode": "array_concat", "concatWith": "{archived_items}" }

Lookup (поиск в таблице)

{
"id": "a5",
"variable": "user_rank",
"value": "",
"mode": "lookup",
"lookupTable": "ranks",
"lookupField": "title",
"lookupWhere": [
{ "field": "user_id", "value": "{user_id}" }
]
}

psql_query — SQL-запрос к PostgreSQL

{
"type": "psql_query",
"data": {
"query": "SELECT balance FROM users WHERE telegram_id = {user_id}",
"saveResultTo": "db_result",
"resultFormat": "first_row",
"textTemplate": "",
"enableAutoTransition": true,
"autoTransitionTo": "next_node_id",
"connectionSource": "builtin",
"connectionEnvVar": "",
"connectionString": ""
}
}
ПолеОписание
querySQL-запрос (поддерживает {переменные})
saveResultToИмя переменной для результата
resultFormat"first_row" — первая строка как объект, "all_rows" — массив, "scalar" — одно значение
connectionSource"builtin" — встроенная БД, "env" — из переменной окружения, "custom" — строка подключения

Примеры SQL

Создание таблицы:

CREATE TABLE IF NOT EXISTS profiles (
telegram_id BIGINT PRIMARY KEY,
balance INT DEFAULT 100,
reputation INT DEFAULT 100,
bio TEXT DEFAULT ''
)

Вставка/обновление:

INSERT INTO profiles (telegram_id, balance) VALUES ({user_id}, 100)
ON CONFLICT (telegram_id) DO NOTHING

Обновление поля:

UPDATE profiles SET reputation = reputation + 10 WHERE telegram_id = {target_user_id}

Выборка:

SELECT balance, reputation FROM profiles WHERE telegram_id = {user_id}

bot_table — работа с внутренними таблицами

Узел для чтения, записи и обновления данных в таблицах проекта (Bot Tables). Без SQL — через визуальный интерфейс.

Операции

operationОписание
readПолучить строку(и) по условию
insertВставить новую строку (таблица создаётся автоматически)
updateОбновить поля по условию (атомарный increment/decrement)
upsertВставить или обновить если существует
deleteУдалить строку по условию
countПодсчитать количество строк (с опциональным WHERE)
sumСумма значений колонки
maxМаксимальное значение колонки
minМинимальное значение колонки
avgСреднее значение колонки
distinctУникальные значения колонки (возвращает JSON-массив)
delete_allУдалить ВСЕ строки из таблицы

read

{
"type": "bot_table",
"data": {
"tableName": "profiles",
"operation": "read",
"where": [
{ "column": "telegram_id", "operator": "equals", "value": "{user_id}" }
],
"saveResultTo": "profile",
"resultFormat": "first_row",
"orderBy": "balance",
"orderDirection": "desc",
"limit": 10,
"offset": 0,
"autoTransitionTo": "next_node",
"enableAutoTransition": true
}
}

resultFormat: "first_row" (объект), "all_rows" (массив), "scalar" (одно значение), "count" (количество), "random_row" (случайная строка — объект, как first_row но выбирается рандомно из результатов).

Операторы WHERE: equals (по умолчанию), not_equals, greater_than, less_than, contains, is_empty, is_not_empty.

После read доступно: {profile.balance}, {profile.reputation} и т.д.

offset применяется перед limit — для пагинации: offset: 10, limit: 5 = строки 11–15.

count

{
"type": "bot_table",
"data": {
"tableName": "users",
"operation": "count",
"where": [
{ "column": "level", "operator": "greater_than", "value": "10" }
],
"saveResultTo": "high_level_count",
"autoTransitionTo": "next_node",
"enableAutoTransition": true
}
}

Результат: {high_level_count} = "42" (строка с числом). WHERE опционален — без него считает все строки.

sum / max / min / avg

{
"type": "bot_table",
"data": {
"tableName": "users",
"operation": "sum",
"aggregateColumn": "balance",
"where": [],
"saveResultTo": "total_balance",
"autoTransitionTo": "next_node",
"enableAutoTransition": true
}
}

aggregateColumn — числовая колонка для вычисления. Результат: строка с числом.

distinct

{
"type": "bot_table",
"data": {
"tableName": "users",
"operation": "distinct",
"aggregateColumn": "profession",
"saveResultTo": "professions",
"autoTransitionTo": "next_node",
"enableAutoTransition": true
}
}

Результат: JSON-массив уникальных значений ["Сварщик", "Программист", "Врач"].

delete_all

{
"type": "bot_table",
"data": {
"tableName": "logs",
"operation": "delete_all",
"saveResultTo": "deleted_count",
"autoTransitionTo": "next_node",
"enableAutoTransition": true
}
}

⚠️ Удаляет ВСЕ строки. Результат: количество удалённых строк.

update

{
"type": "bot_table",
"data": {
"tableName": "profiles",
"operation": "update",
"where": [
{ "column": "telegram_id", "operator": "equals", "value": "{reply_to_user_id}" }
],
"updates": [
{ "column": "reputation", "op": "increment", "value": "10" },
{ "column": "balance", "op": "decrement", "value": "5" }
],
"autoTransitionTo": "next_node",
"enableAutoTransition": true
}
}

Операции op: set, increment, decrement, min, max. Все атомарные (через SQL).

Обновляет ВСЕ строки подходящие под WHERE (не только первую).

saveResultTo (опционально): сохраняет количество обновлённых строк ("0" если ни одна не подошла под WHERE). Полезно для условных update'ов (optimistic locking):

{
"operation": "update",
"where": [
{ "column": "telegram_id", "operator": "equals", "value": "{user_id}" },
{ "column": "in_flight", "operator": "is_empty", "value": "" }
],
"updates": [
{ "column": "in_flight", "op": "set", "value": "1" }
],
"saveResultTo": "was_updated"
}

Если was_updated = "0" — строка не обновилась (условие WHERE не выполнено). Используйте condition после для проверки.

insert

{
"type": "bot_table",
"data": {
"tableName": "profiles",
"operation": "insert",
"row": {
"telegram_id": "{user_id}",
"balance": "100",
"reputation": "100"
},
"returnInsertedId": true,
"saveResultTo": "profile",
"autoTransitionTo": "next_node",
"enableAutoTransition": true
}
}

Если таблица не существует — создаётся автоматически с колонками из row.

returnInsertedId: true — сохраняет порядковый номер строки в {profile_id} (saveResultTo + "_id").

upsert

{
"type": "bot_table",
"data": {
"tableName": "profiles",
"operation": "upsert",
"key": "telegram_id",
"row": {
"telegram_id": "{user_id}",
"balance": "100",
"reputation": "100"
},
"onConflict": "ignore",
"returnInsertedId": true,
"saveResultTo": "profile",
"autoTransitionTo": "next_node",
"enableAutoTransition": true
}
}

onConflict: "ignore" (не трогать), "update" (перезаписать), "merge" (только пустые поля).

returnInsertedId работает только при вставке новой строки (не при обновлении существующей).

delete

{
"type": "bot_table",
"data": {
"tableName": "relationships",
"operation": "delete",
"where": [
{ "column": "user_a", "operator": "equals", "value": "{user_id}" },
{ "column": "user_b", "operator": "equals", "value": "{target_user_id}" }
],
"autoTransitionTo": "next_node",
"enableAutoTransition": true
}
}

Удаляет ВСЕ строки подходящие под WHERE. Поддерживает все операторы.

Дополнительные поля (read)

{
"orderBy": "reputation",
"orderDirection": "desc",
"limit": 10
}

Имя таблицы с переменными

tableName поддерживает {переменные}: "orders_{chat_id}", "data_{user_id}".

loop — цикл по массиву

{
"type": "loop",
"data": {
"sourceVariable": "users_list",
"itemVariable": "item",
"indexVariable": "index",
"parallel": false,
"delaySeconds": 0,
"maxIterations": 0,
"autoTransitionTo": "body_first_node",
"afterLoopTo": "after_loop_node",
"enableAutoTransition": true
}
}
ПолеОписание
sourceVariableПеременная с массивом для итерации
itemVariableИмя переменной текущего элемента (доступ: {item.field})
indexVariableИмя переменной индекса (0, 1, 2...)
paralleltrue — параллельное выполнение через asyncio.gather
delaySecondsПауза между итерациями
maxIterationsЛимит итераций (0 = без лимита)
autoTransitionToПервый узел тела цикла
afterLoopToУзел после завершения цикла

parallel_split — параллельный запуск веток (fan-out)

Запускает каждую ветку как отдельную asyncio-задачу — все ветки выполняются одновременно. Точки сбора (join) нет: сбор результатов собирается из set_variable (счётчик) + condition.

{
"type": "parallel_split",
"data": {
"parallelBranches": [
{ "id": "br_1", "label": "Погода", "target": "http-weather" },
{ "id": "br_2", "label": "Курсы", "target": "http-rates", "onErrorTarget": "setv-rates-failed" }
],
"maxConcurrent": 5,
"awaitAll": false,
"skipIfRunning": true
}
}
ПолеОписание
parallelBranchesВетки запуска: id, label (для логов), target (стартовая нода ветки)
parallelBranches[].onErrorTargetНода при ошибке ветки (обычно setv-инкремент, чтобы сбор не завис)
maxConcurrentЛимит одновременных веток через Semaphore (0 = без лимита, по умолчанию 5)
awaitAlltrue — обработчик ждёт все ветки; false (по умолчанию) — fire-and-forget
skipIfRunningtrue (по умолчанию) — повторный запуск блокируется, пока прогон пользователя не завершён

Паттерн сбора результатов (join без join-ноды):

parallel_split → ветка N: … → set_variable (done = int({done}) + 1, mode=expression) → condition (done equals "3") → итог

Инкремент в set_variable mode=expression атомарный (Lock на пользователя) — безопасен из параллельных веток.

Внутри веток НЕ использовать input (FSM один на пользователя) и триггеры (это точки входа). Перед сбросом счётчика (set_variable done=0) ставить до parallel_split, не внутри веток.

stop_processing — остановка middleware-цепочки

{
"type": "stop_processing",
"data": {
"autoTransitionTo": "next_node_id",
"enableAutoTransition": true
}
}

Устанавливает user_data[user_id]["_stop_processing"] = True. Работает в паре с incoming_message_trigger и imtStopOnFlag: true — middleware не вызовет следующий handler для этого апдейта.

rate_counter — счётчик частоты в скользящем окне

{
"type": "rate_counter",
"data": {
"counterKey": "spam_{user_id}",
"windowSeconds": "60",
"saveResultTo": "rate_count",
"autoTransitionTo": "check_limit_node",
"enableAutoTransition": true
}
}
ПолеОписание
counterKeyКлюч счётчика (поддерживает {переменные})
windowSecondsРазмер окна в секундах
saveResultToПеременная для количества событий в окне
autoTransitionToСледующий узел (enableAutoTransition: true)

Данные in-memory — сбрасываются при перезапуске бота.

delay — задержка

{
"type": "delay",
"data": {
"seconds": "90",
"unit": "seconds",
"mode": "blocking",
"autoTransitionTo": "next_node_id",
"enableAutoTransition": true
}
}
ПолеОписание
secondsЗначение задержки (поддерживает {переменные}; дробные секунды, напр. "0.1")
unitЕдиница: "seconds", "minutes", "hours", "days", "weeks"
mode"blocking" — пауза (ждёт), "background" — фоновый таймер (цепочка завершается)

Режим blocking: await asyncio.sleep(N) → переход к следующему узлу. Режим background: asyncio.create_task() → переход через N времени, текущая цепочка завершается сразу.

Пример: уведомление после кулдауна:

{
"type": "delay",
"data": {
"seconds": "90",
"unit": "seconds",
"mode": "background",
"autoTransitionTo": "msg-cd-notify"
}
}

code — произвольный Python (Telethon)

Пишите тело async-функции с await (без своего async def). Переменные пользователя доступны по имени. client и userbot_client — тот же Telethon-клиент, что у узлов юзербота.

{
"type": "code",
"data": {
"code": "msgs = await client.get_messages(entity, limit=1)\nresult_text = msgs[0].message if msgs else \"\"",
"autoTransitionTo": "next_node_id",
"enableAutoTransition": true
}
}
ПолеОписание
codeAsync-тело Python
autoTransitionToID следующего узла
enableAutoTransitionОбязательно true вместе с autoTransitionTo

Что доступно в namespace

ИмяЧто это
client, userbot_clientTelethon-клиент юзербота (тот же, что у userbot-узлов)
botЭкземпляр aiogram Bot — можно править сообщения самого бота
user_idID текущего пользователя
user_dataСловарь переменных текущего пользователя
all_user_dataСловарь всех пользователей: all_user_data[user_id]
set_user_varФункция записи переменной пользователя
callback_query, stateОбъекты aiogram текущего апдейта (могут быть None)
asyncio, json, re, math, datetime, logging, Button, eventsПредзагруженные модули

Переменные пользователя доступны просто по имени, без user_data[...].

Как возвращать результат

Присвойте значение переменной на верхнем уровне кода — она автоматически запишется в переменные пользователя:

lucky_payment = 8942

Внутри вложенных функций присваивание в локальную переменную наружу не попадёт. Пишите напрямую в _ns:

async def _worker():
_ns['lucky_payment'] = 8942 # так значение сохранится

set_user_var внутри вложенных функций вызывать нельзя — используйте _ns[...].

Прогрессбар из кода

bot в namespace позволяет обновлять сообщение по ходу выполнения — удобно для долгих опросов:

await bot.edit_message_text('Опрашиваю… 3/15', chat_id=chat_id,
message_id=progress_msg_id, parse_mode='HTML')

Ограничения

  • Таймаут 180 с на весь код узла.
  • Вызовы Telethon требуют USERBOT_* в окружении.
  • RestrictedPython нет (self-hosted) — код исполняется как есть.
  • Имена, начинающиеся с _, и предзагруженные модули в переменные пользователя не пишутся.
  • asyncio.wait_for начинает отсчёт сразу: если задачи стоят в очереди семафора, таймаут выгорит на ожидании. Ставьте wait_for внутри воркера, а не вокруг всей очереди.

comment — заметка на холсте

Текстовая заметка-стикер для пояснений к сценарию. Не влияет на логику бота — игнорируется при генерации Python-кода. Не имеет портов, связей и автоперехода.

Используй comment-ноды чтобы пояснять структуру сложного сценария: помечать воронки, ветки, TODO, описывать назначение групп нод.

{
"type": "comment",
"data": {
"messageText": "Воронка онбординга: приветствие → сбор данных → сохранение в таблицу",
"commentColor": "yellow"
}
}
ПолеОписание
messageTextТекст заметки (поддерживает многострочный текст)
commentColorЦвет стикера: "yellow", "blue", "green", "pink", "gray"

Заметка содержит только messageText и commentColor — никаких служебных полей сообщения (buttons, keyboardType и т.п.) добавлять не нужно.

Рекомендация по цветам: yellow — общие пометки, blue — пояснения логики, green — готовые блоки, pink — важное/внимание, gray — архив/черновик.

input — ожидать ввод пользователя

Единственный способ сбора ввода. Message-нода отправляет вопрос, input-нода ожидает и сохраняет ответ.

Типичная цепочка: message (вопрос) → input (сохранение ответа) → следующий узел.

Для кнопочного ввода: message (вопрос) → keyboard (кнопки) → input (сохранение нажатия).

{
"type": "input",
"data": {
"inputVariable": "user_answer",
"inputPrompt": "Введите ваш ответ:",
"inputType": "text",
"inputTargetNodeId": "next_node_id"
}
}

Поля input-ноды

ПолеТипОписание
inputVariablestringИмя переменной для сохранения ответа
inputTypestringТип ввода: text, photo, video, audio, document, location, contact, any, callback
inputTargetNodeIdstringID следующего узла после получения ответа
inputPromptstringПодсказка (не отправляется пользователю, для UI)
appendVariablebooleantrue — добавить к существующему значению, false — заменить
saveToDatabasebooleanСохранять в БД
inputRequiredbooleanВвод обязателен
maxLengthnumberМаксимальная длина текста (0 = без ограничений)
minLengthnumberМинимальная длина текста (0 = без ограничений)
validationTypestringВалидация: none, email, phone, number
retryMessagestringСообщение при ошибке валидации
inputTimeoutnumberТаймаут ожидания в секундах (> 0). По истечении FSM сбрасывается
inputTimeoutMessagestringСообщение при таймауте (по умолчанию «Время ожидания истекло.»)

Таймаут и отмена /cancel

При inputTimeout > 0 генерируется планировщик _schedule_input_timeout. По истечении срока форма сбрасывается, пользователю отправляется inputTimeoutMessage.

При активном сборе ввода автоматически регистрируется команда /cancel — сбрасывает FSM и отвечает «Форма отменена.»

{
"type": "input",
"data": {
"inputVariable": "user_answer",
"inputType": "text",
"inputTargetNodeId": "next_node_id",
"inputTimeout": 120,
"inputTimeoutMessage": "Слишком долго ждали. Начните заново."
}
}

inputType: "callback"

Для сохранения нажатия inline-кнопки. Кнопки на keyboard-ноде ведут к input-ноде с inputType: "callback" — текст нажатой кнопки сохраняется в переменную.

{
"type": "input",
"data": {
"inputVariable": "user_choice",
"inputType": "callback",
"inputTargetNodeId": "next_node_id"
}
}

Сохранение метаданных медиа

Для медиа-типов (photo, video, audio, document) можно включить сохранение метаданных в отдельные переменные:

{
"type": "input",
"data": {
"inputVariable": "user_video",
"inputType": "video",
"saveMediaMetadata": true,
"mediaMetadataSuffixes": ["thumbnail", "duration", "file_size", "file_name"],
"mediaMetadataCustomNames": { "duration": "video_len" },
"autoTransitionTo": "next_node_id"
}
}
  • saveMediaMetadata — включить сохранение метаданных (по умолчанию false)
  • mediaMetadataSuffixes — массив суффиксов для сохранения (пустой = все)
  • mediaMetadataCustomNames — кастомные имена переменных (ключ — суффикс, значение — имя)

Доступные суффиксы: file_id, file_unique_id, thumbnail, duration, file_size, file_name, width, height, mime_type, title, performer, small_file_id, small_width, small_height, sizes_count, all_sizes

broadcast — рассылка

{
"type": "broadcast",
"data": {
"messageText": "Текст рассылки",
"autoTransitionTo": "next_node_id"
}
}

Медиа-узлы

ТипОписание
photoОтправить фото (imageUrl)
videoОтправить видео (videoUrl)
audioОтправить аудио (audioUrl)
documentОтправить документ (documentUrl, documentName)
stickerОтправить стикер
animationОтправить GIF
locationОтправить геолокацию
contactОтправить контакт

Все медиа-узлы поддерживают mediaCaption — подпись.

Пример photo

{
"type": "photo",
"data": {
"imageUrl": "https://example.com/photo.jpg",
"mediaCaption": "Подпись к фото",
"autoTransitionTo": "next_node_id"
}
}

Действия с сообщениями

ТипОписание
edit_messageРедактировать сообщение
delete_messageУдалить сообщение
pin_messageЗакрепить сообщение
unpin_messageОткрепить сообщение
forward_messageПереслать сообщение
answer_callback_queryОтветить на callback (всплывающее уведомление)

customCallbackData для edit_message

Кнопки, ведущие к edit_message, поддерживают customCallbackData с переменными. Это позволяет передать данные (например ID пользователя) через callback:

{
"id": "btn-approve",
"text": "✅ Подтвердить",
"action": "goto",
"target": "edit-status-node",
"customCallbackData": "approve_{user_id}"
}

При нажатии кнопки динамическая часть (после префикса) сохраняется в переменную {_cb_dynamic_id}. Это полезно для кросс-пользовательских сценариев (админ одобряет заявку другого пользователя).


delete_message — Удалить сообщение

Удаляет одно или несколько сообщений в чате. Поддерживает удаление текущего сообщения, последнего сообщения бота, последних N сообщений, по конкретному ID или массиву ID.

{
"id": "del-1",
"type": "delete_message",
"position": { "x": 400, "y": 200 },
"data": {
"messageIdSource": "current_message",
"messageIdManual": "",
"lastNCount": "",
"chatIdSource": "current_chat",
"chatIdManual": "",
"ignoreErrors": true,
"bulkDelete": false,
"bulkMessageIdsVariable": "",
"enableAutoTransition": true,
"autoTransitionTo": "next-node-id"
}
}

Поля data:

ПолеТипОписание
messageIdSource"current_message" / "last_bot_message" / "reply_message" / "range_from_reply" / "last_n" / "custom"Какое сообщение удалить
messageIdManualstringID или {переменная} — для режима custom
lastNCountstringКоличество — для режима last_n (число или {переменная})
chatIdSource"current_chat" / "custom"В каком чате удалять
chatIdManualstringID чата или {переменная} — для режима custom
ignoreErrorsbooleanНе прерывать сценарий если сообщение не найдено
bulkDeletebooleanМассовое удаление из переменной-массива
bulkMessageIdsVariablestringИмя переменной с JSON-массивом message_id
autoTransitionTostringID следующего узла

Примеры использования:

Удалить последние 50 сообщений:

{ "messageIdSource": "last_n", "lastNCount": "50" }

Удалить по ID из переменной:

{ "messageIdSource": "custom", "messageIdManual": "{saved_msg_id}" }

Массовое удаление:

{ "bulkDelete": true, "bulkMessageIdsVariable": "old_message_ids" }

Ограничения Telegram: бот удаляет чужие сообщения только в группах (нужны права админа). Сообщения старше 48 часов удалить нельзя. В личных чатах — только свои.


Действия с пользователями (группы)

ТипОписание
ban_userЗабанить пользователя
unban_userРазбанить
mute_userЗамутить
unmute_userРазмутить
kick_userИсключить пользователя из группы
promote_userНазначить администратором
demote_userСнять права администратора
admin_rightsУстановить конкретные права

kick_user — Исключить пользователя

Исключает пользователя из группы (может вернуться по ссылке). Использует unbanChatMember(only_if_banned=False).

{
"id": "kick-1",
"type": "kick_user",
"position": { "x": 400, "y": 300 },
"data": {
"userIdSource": "current_user",
"userIdManual": "",
"chatIdSource": "current_chat",
"chatIdManual": "",
"ignoreErrors": true,
"enableAutoTransition": true,
"autoTransitionTo": "msg-done"
}
}
ПолеТипОписание
userIdSource"current_user" | "reply_user" | "custom"Откуда брать ID пользователя
userIdManualstringID или {переменная} (для режима custom)
chatIdSource"current_chat" | "custom"Откуда брать ID чата
chatIdManualstringID чата или {переменная} (для режима custom)
ignoreErrorsbooleanНе прерывать сценарий при ошибке
autoTransitionTostringID узла для автоперехода

userbot_message — Сообщение через юзербот (Telethon)

Отправляет сообщение от аккаунта пользователя через Telethon MTProto.

{
"id": "ub-msg-1",
"type": "userbot_message",
"position": { "x": 400, "y": 200 },
"data": {
"messageText": "Привет, {user_name}!",
"formatMode": "html",
"userbotEntity": "@target_channel",
"userbotRecipients": ["@channel1", "{chat_id}", "-1001234567890"],
"attachedMedia": [],
"disableLinkPreview": false,
"saveMessageIdTo": "",
"autoTransitionTo": ""
}
}
ПолеТипОписание
messageTextstringТекст сообщения (поддерживает {переменные} и HTML)
formatMode"html" | "markdown" | "none"Режим форматирования
userbotEntitystringОсновной получатель (обратная совместимость)
userbotRecipientsstring[]Список получателей (@username, ID, {переменная})
attachedMediastring[]Медиафайлы (/uploads/... или URL)
disableLinkPreviewbooleanОтключить превью ссылок
saveMessageIdTostringПеременная для сохранения ID сообщения
saveResponseIdTostringПеременная для сохранения ID ответного сообщения от получателя
saveResponseTextTostringПеременная для сохранения текста ответного сообщения
saveButtonsTostringПеременная для сохранения кнопок ответа как JSON-массив [{text, type, data?, url?}]
autoTransitionTostringID узла для автоперехода

Примечания:

  • Кнопки не поддерживаются (ограничение Telegram для user-аккаунтов)
  • File_id от Bot API не работает в Telethon — используйте /uploads/ или URL
  • Между получателями автоматическая пауза 2 секунды (защита от FloodWait)
  • Текст и entity поддерживают горячую перезагрузку через таблицу _content

userbot_click_button — Нажатие кнопки через юзербот

Нажимает inline-кнопку в сообщении через Telethon userbot.

{
"id": "ub-click-1",
"type": "userbot_click_button",
"position": { "x": 400, "y": 200 },
"data": {
"userbotEntity": "@target_bot",
"messageId": "{response_msg_id}",
"messageIdSource": "last",
"clickMode": "text",
"clickValue": "Играть",
"saveAlertTo": "alert_text",
"saveResultTo": "new_message_text",
"saveButtonsTo": "buttons_json",
"saveHasMediaTo": "has_media",
"saveMediaTo": "media_obj",
"autoTransitionTo": ""
}
}
ПолеТипОписание
userbotEntitystringЧат с кнопками (@username, ID, {переменная})
messageIdstringID сообщения ({переменная} или число)
messageIdSource"manual" | "last"Источник: конкретный ID или последнее сообщение
clickMode"text" | "data" | "index"Способ поиска кнопки
clickValuestringТекст / callback_data / "row, col"
saveAlertTostringПеременная для alert
saveResultTostringПеременная для текста после нажатия
saveButtonsTostringПеременная для JSON кнопок
saveHasMediaTostringПеременная для флага медиа
saveMediaTostringПеременная для медиа-объекта
autoTransitionTostringID узла для автоперехода

userbot_inline_query — Inline-запрос через юзербот

Выполняет inline-запрос (@bot query) и отправляет результат в чат.

{
"id": "ub-iq-1",
"type": "userbot_inline_query",
"position": { "x": 400, "y": 200 },
"data": {
"botUsername": "@scdoo_bot",
"query": "buy_btc 1",
"targetChat": "",
"sendToSameChat": true,
"resultIndex": "0",
"saveResultTitleTo": "result_title",
"saveResultDescTo": "result_desc",
"saveResponseIdTo": "sent_msg_id",
"autoTransitionTo": ""
}
}
ПолеТипОписание
botUsernamestringUsername бота (@bot или {переменная})
querystringТекст inline-запроса
targetChatstringЦелевой чат (если sendToSameChat=false)
sendToSameChatbooleanОтправить в чат с ботом (по умолчанию true)
resultIndexstringИндекс результата (0 = первый)
saveResultTitleTostringПеременная для title
saveResultDescTostringПеременная для description
saveResponseIdTostringПеременная для ID сообщения
autoTransitionTostringID узла для автоперехода

Кнопки (Button)

Кнопки задаются в data.buttons[] узла. Тип клавиатуры задаётся в data.keyboardType:

  • "none" — без клавиатуры
  • "reply" — reply-клавиатура (под полем ввода)
  • "inline" — inline-кнопки (под сообщением)

Дополнительное поле "shuffleButtons": true — перемешивает порядок inline-кнопок при каждом показе (для мини-игр, капч, квизов).

{
"id": "btn_1",
"text": "Нажми меня",
"action": "goto",
"target": "node_id",
"buttonType": "normal",
"style": "primary",
"hideAfterClick": false,
"skipDataCollection": false
}

Действия кнопок (action)

actionОписаниеДоп. поля
gotoПерейти на узелtarget: "nodeId"
urlОткрыть URLurl: "https://..."
web_appОткрыть Mini AppwebAppUrl: "https://..."
copy_textСкопировать текстcopyText: "текст"
commandВыполнить команду
contactЗапросить контактrequestContact: true
locationЗапросить геолокациюrequestLocation: true
selectionВыбор из списка
completeЗавершить сбор данных
request_managed_botСоздать управляемого ботаsuggestedBotName, suggestedBotUsername

Стили кнопок (style)

ЗначениеЦвет
primaryСиний
successЗелёный
dangerКрасный

Переменные

Переменные используются в messageText, httpRequestUrl, httpRequestBody, query и других полях через синтаксис {имя_переменной}.

Вложенные поля из HTTP-ответа: {response.data.user.name}

Inline-выражения {=...}

В любом текстовом поле (messageText, mediaCaption, button text, httpRequestUrl и т.д.) поддерживается синтаксис {=выражение} для вычисления формул прямо в тексте:

СинтаксисРезультатОписание
{=credits - price}3800Арифметика
{=thousands(credits)}5 000Форматирование числа
{=round(score / games, 2)}70.58Округление
{=max(hp - damage, 0)}0Функции min/max

Доступные функции: round, abs, int, float, min, max, str, reversed, thousands.

Доступные методы строк: replace, strip, lstrip, rstrip, lower, upper, startswith, endswith, split, join, count, find, format, а также проверки isdigit, isnumeric, isdecimal, isalpha, isalnum, isspace.

Генераторы и comprehension не поддерживаются — выражение вроде ''.join(c for c in x if c.isdigit()) молча вернётся как текст. Для защиты от нечислового ввода используйте isdigit:

float('{amount}'.replace(',', '.')) if '{amount}'.replace(',', '.').replace('.', '', 1).isdigit() else 0

Пример разворота порядка слов: {=' '.join(reversed('{user_message}'.split()))} → «Привет как дела» станет «дела как Привет». Разворот символов проще делать срезом: {='{user_message}'[::-1]}.

Переменные внутри {=...} пишутся без фигурных скобок: {=thousands(user_amount)}, не {=thousands({user_amount})}.

Выражения вычисляются через _eval_expr — безопасный вычислитель на основе AST. При ошибке или невалидном выражении текст {=...} остаётся без изменений.

Системные переменные

ПеременнаяОписание
{user_id}Telegram ID пользователя
{username}username пользователя
{first_name}Имя пользователя
{last_name}Фамилия пользователя
{chat_id}ID чата
{chat_type}Тип чата: private, group, supergroup, channel
{message_text}Текст или подпись последнего сообщения
{message_type}Тип содержимого: text, photo, video, document и т.д.
{callback_data}Данные нажатой кнопки
{reply_to_user_id}ID автора сообщения, на которое ответили
{reply_to_username}Username автора reply-сообщения
{reply_to_first_name}Имя автора reply-сообщения
{reply_to_last_name}Фамилия автора reply-сообщения
{reply_to_message_id}ID сообщения, на которое ответили
{reply_to_text}Текст reply-сообщения
{message_id}ID текущего сообщения

chat_id, chat_type, message_* и reply_to_* заполняются capture_message_context в обработчиках команд, текста, incoming_message_trigger и member_trigger. reply_to_* — только если входящее сообщение является reply; иначе пустые строки.

Переменная типа file

HTTP-узел с httpRequestResponseFormat: "file" сохраняет ответ как объект:

ПолеТипОписание
type"file"Маркер типа переменной
datastringСодержимое файла в base64
mimeTypestringMIME-тип файла
fileNamestringИмя файла при отправке

Используется в медиа-ноде через {имя_переменной} — файл отправляется через BufferedInputFile без сохранения на диск.


Связи между узлами

Узлы связываются через:

  • data.autoTransitionTo: "nodeId" — автоматический переход после выполнения
  • data.buttons[].target: "nodeId" — переход по кнопке
  • data.branches[].target: "nodeId" — переход по ветке условия
  • data.afterLoopTo: "nodeId" — переход после завершения цикла

⚠️ Важно: для нетриггерных нод (message, set_variable, bot_table, delete_message, delay, code, psql_query, convert_file, http_request и др.) при использовании autoTransitionTo обязательно добавлять "enableAutoTransition": true. Без этого флага связь не отрисуется на канвасе. Триггеры (command_trigger, text_trigger, schedule_trigger и др.) не нуждаются в этом флаге — их связи обрабатываются отдельно.


Пример: сценарий с базой данных и логикой

{
"version": 2,
"activeSheetId": "sheet1",
"sheets": [{
"id": "sheet1",
"name": "Профиль",
"nodes": [
{
"id": "cmd-profile",
"type": "command_trigger",
"position": { "x": 0, "y": 0 },
"data": {
"command": "/profile",
"description": "Мой профиль",
"showInMenu": true,
"autoTransitionTo": "db-get-profile"
}
},
{
"id": "db-get-profile",
"type": "psql_query",
"position": { "x": 300, "y": 0 },
"data": {
"query": "INSERT INTO profiles (telegram_id, balance, reputation) VALUES ({user_id}, 100, 100) ON CONFLICT (telegram_id) DO NOTHING; SELECT balance, reputation, bio FROM profiles WHERE telegram_id = {user_id}",
"saveResultTo": "profile",
"resultFormat": "first_row",
"enableAutoTransition": true,
"autoTransitionTo": "msg-profile",
"connectionSource": "builtin"
}
},
{
"id": "msg-profile",
"type": "message",
"position": { "x": 600, "y": 0 },
"data": {
"messageText": "👤 Профиль {first_name}\n\n💰 Баланс: {profile.balance} 🍪\n⭐ Репутация: {profile.reputation}\n📝 О себе: {profile.bio}",
"markdown": true,
"keyboardType": "inline",
"buttons": [
{ "id": "btn-edit", "text": "✏️ Редактировать", "action": "goto", "target": "edit-menu" }
]
}
}
]
}]
}

Пример: schedule + loop (ежедневный сброс)

{
"id": "sched-reset",
"type": "schedule_trigger",
"position": { "x": 0, "y": 0 },
"data": {
"rules": [{ "mode": "daily", "hour": 0, "minute": 0 }],
"timezone": "Europe/Moscow",
"autoTransitionTo": "db-reset-rep",
"enabled": true
}
},
{
"id": "db-reset-rep",
"type": "psql_query",
"position": { "x": 300, "y": 0 },
"data": {
"query": "UPDATE profiles SET reputation = 50 WHERE reputation < 50",
"saveResultTo": "",
"resultFormat": "scalar",
"connectionSource": "builtin"
}
}

Пример: text_trigger + set_variable (репутация)

{
"id": "trig-plus-rep",
"type": "text_trigger",
"position": { "x": 0, "y": 400 },
"data": {
"textMatchType": "exact",
"textSynonyms": ["+реп", "плюс реп"],
"autoTransitionTo": "db-add-rep"
}
},
{
"id": "db-add-rep",
"type": "psql_query",
"position": { "x": 300, "y": 400 },
"data": {
"query": "UPDATE profiles SET reputation = LEAST(reputation + 10, 100) WHERE telegram_id = {reply_to_user_id} RETURNING reputation",
"saveResultTo": "new_rep",
"resultFormat": "scalar",
"enableAutoTransition": true,
"autoTransitionTo": "msg-rep-done",
"connectionSource": "builtin"
}
},
{
"id": "msg-rep-done",
"type": "message",
"position": { "x": 600, "y": 400 },
"data": {
"messageText": "✅ Репутация @{reply_to_username} теперь: {new_rep}",
"keyboardType": "none"
}
}

Пример минимального сценария

{
"version": 2,
"activeSheetId": "sheet1",
"sheets": [{
"id": "sheet1",
"name": "Главный",
"nodes": [
{
"id": "cmd-start",
"type": "command_trigger",
"position": { "x": 0, "y": 0 },
"data": {
"command": "/start",
"description": "Запустить бота",
"showInMenu": true,
"autoTransitionTo": "msg-hello"
}
},
{
"id": "msg-hello",
"type": "message",
"position": { "x": 300, "y": 0 },
"data": {
"messageText": "Привет, {first_name}! Выбери действие:",
"keyboardType": "inline",
"buttons": [
{ "id": "btn1", "text": "О нас", "action": "goto", "target": "msg-about" },
{ "id": "btn2", "text": "Сайт", "action": "url", "url": "https://example.com" }
]
}
}
]
}]
}

Где смотреть в коде

ЧтоГде
Схема узловshared/schema/tables/node-schema.ts
Схема кнопокshared/schema/tables/button-schema.ts
Схема листовshared/schema/tables/bot-sheets.ts
Генератор Pythonlib/bot-generator.ts
Шаблоны генерацииlib/templates/
Примеры JSONbots/*/project.json
Типы conditionshared/types/condition-node.ts
Шаблон set_variablelib/templates/set-variable/
Шаблон psql_querylib/templates/psql-query/
Шаблон schedulelib/templates/schedule-trigger/
Шаблон looplib/templates/loop/
Шаблон bot_tablelib/templates/bot-table/

Архитектура данных

Уровни хранения

УровеньЧто хранитТаблица БДФильтрация
ПлатформаВладельцы ботов, сессииtelegram_users, session
ПроектСценарий, шаблоны, пользовательские таблицыbot_projects, bot_tables, bot_table_rowsproject_id
Токен (бот)Пользователи, сообщения, логи, рассылкиbot_users, bot_messages, bot_logsproject_id + token_id

Системные таблицы (read-only, уровень токена)

Виртуальные таблицы, отображающие реальные данные бота в UI:

ТаблицаИсточникAPI
Пользователиbot_usersGET /api/projects/:id/users
Переменныеbot_users.user_data (JSONB → колонки)GET /api/projects/:id/users/variables
Сообщенияbot_messagesGET /api/projects/:id/messages/all
Группыbot_groupsGET /api/projects/:id/groups
Логиbot_logsGET /api/projects/:id/logs/all
Запускиbot_launch_historyGET /api/projects/:id/launches/all
РассылкиbroadcastsGET /api/projects/:id/broadcasts
Токеныbot_tokensGET /api/projects/:id/tokens
Медиафайлыmedia_filesGET /api/media/project/:id

Пользовательские таблицы (read-write, уровень проекта)

Таблицы создаваемые пользователем в конструкторе. Используются нодами bot_table.

Хранение: bot_tablesbot_table_columnsbot_table_rows (data: JSONB).

GIN-индекс на bot_table_rows.data для быстрого поиска по ключам.

Переменные пользователей (user_data)

Поле bot_users.user_data (JSONB) хранит все пользовательские переменные бота:

{
"user_age": "25",
"user_bio": "текст",
"utm_source": "direct",
"score": "150"
}

Доступ в сценарии: {user_age}, {user_bio} и т.д.

Служебные ключи (не показываются в UI): _*, waiting_*, input_*.


Правила генерации сценариев

Структура листов

  • Один лист = одна логическая группа (профиль, репутация, магазин, админка)
  • Имя листа: эмодзи + название ("⭐ Репутация", "🛒 Магазин")
  • Не более 15-20 узлов на лист

Позиционирование

  • Триггеры: x = 100, разные y (шаг 200)
  • Цепочка обработки: x += 300 для каждого шага
  • Параллельные ветки: y += 200

ID узлов

  • Осмысленные: cmd-start, tbl-read-profile, msg-welcome, cond-check-rep
  • Префиксы: cmd- (команды), trig- (триггеры), msg- (сообщения), tbl- (bot_table), cond- (условия), set- (set_variable), http- (запросы), loop- (циклы)

Выбор между psql_query и bot_table

Критерийpsql_querybot_table
Сложные JOIN
Агрегации (COUNT, SUM)
Простой CRUDМожно✅ Предпочтительно
Автосоздание таблицы
Атомарный incrementЧерез SQL✅ Встроено
Безопасность (injection)Нужна осторожность✅ Безопасно

Рекомендация: для простых операций (профиль, баланс, репутация) — bot_table. Для сложной аналитики и кросс-таблиц — psql_query.

Антипаттерны

  • ❌ Не хранить массивы как строку — использовать json_push в set_variable
  • ❌ Не делать SELECT + UPDATE в двух узлах — использовать bot_table с operation: "update" и op: "increment"
  • ❌ Не дублировать данные в user_data и в пользовательской таблице — выбрать одно место
  • ❌ Не использовать psql_query для простого чтения одной строки — bot_table проще и безопаснее
  • ❌ Не создавать таблицу на каждого пользователя (orders_{user_id}) — использовать одну таблицу с колонкой user_id

Паттерны

  • ✅ Регистрация: command_trigger(/start)bot_table(upsert, key=telegram_id)message(приветствие)
  • ✅ Профиль: command_trigger(/profile)bot_table(read)message({profile.field})
  • ✅ Репутация: text_trigger(+реп)bot_table(read me)condition(rep >= 50)bot_table(update target, increment)message(результат)
  • ✅ Магазин: message(меню)inline кнопкиcondition(balance >= price)bot_table(update, decrement balance)message(успех)
  • ✅ Расписание: schedule_trigger(daily 00:00)psql_query(UPDATE ... WHERE condition)message(отчёт в админ-чат)

Таблица контента (_content)

Концепция

Каждый проект автоматически имеет системную таблицу _content — единый реестр всего редактируемого контента бота. Тексты сообщений, подписи к медиа, тексты кнопок, URL ссылок — всё синхронизируется с JSON сценария.

Бот читает контент из таблицы через кэш — горячая перезагрузка без рестарта (через Redis pub/sub или polling каждые 60 сек).

Структура таблицы

КолонкаОписаниеРедактируемая
keyУникальный ключ
typeТип контента
sheetИмя листа
valueЗначение

Формат ключей

ИсточникtypeФормат key
messageTextmessage{node_id}
mediaCaptioncaption{node_id}.caption
buttons[].textbutton{node_id}.btn.{btn_id}
buttons[].urlurl{node_id}.btn.{btn_id}.url
buttons[].webAppUrlurl{node_id}.btn.{btn_id}.webapp
imageUrl/videoUrl/audioUrl/documentUrlmedia_url{node_id}.media
httpRequestUrlapi_url{node_id}.api
httpRequestBodyhttp_body{node_id}.body
httpRequestHeadershttp_headers{node_id}.headers
psql_query.querysql{node_id}.sql
command_trigger.descriptioncommand{node_id}.desc
inputPromptprompt{node_id}.prompt

Keyboard-ноды

Если кнопки хранятся в отдельной keyboard-ноде (поле keyboardNodeId в message-ноде), ключи в таблице используют ID message-ноды, а не keyboard-ноды. Это обеспечивает совпадение с ключами в сгенерированном коде бота.

Поведение keyboard-ноды при вызове

Когда пользователь нажимает inline-кнопку с target на keyboard-ноду, кнопки текущего сообщения обновляются через editMessageReplyMarkup — без отправки нового сообщения.

Это позволяет:

  • Менять набор кнопок по нажатию (подменю, пагинация)
  • Убирать кнопки (пустая keyboard-нода → reply_markup=None)
  • Обновлять текст кнопок динамически

Горячая перезагрузка

Поля поддерживающие мгновенное обновление без рестарта бота:

  • messageText — текст сообщения
  • buttons[].text — текст inline и reply кнопок
  • buttons[].url — URL кнопок
  • mediaCaption — подпись к медиа (через text=caption)
  • formatMode — статичный режим ноды (markdown/html) применяется напрямую; при formatMode: "none" parse_mode определяется автодетектом из HTML-тегов в тексте (fallback для совместимости со старыми нодами и горячей правкой текста)

Поля требующие перезапуска бота:

  • Структура графа (связи, autoTransitionTo)
  • Типы нод, callback_data кнопок
  • Имена переменных
  • Операции bot_table

Где смотреть в коде

ЧтоГде
Синхронизация JSON → таблицаserver/services/content-table/sync-content-to-table.ts
Синхронизация таблица → JSONserver/services/content-table/sync-table-to-scenario.ts
Маппинг ключейserver/services/content-table/content-key-parser.ts
Шаблон Python (get_content)lib/templates/content/content.py.jinja2
Документация фичиdocs/futures/features/content-table.md

⚠️ Частые ошибки и подводные камни

1. set_variable: вложенные переменные (obj.field) в mode expression

При использовании {obj.field} в выражениях, переменные хранятся как плоские ключи (sell_item.quantity), а не как вложенные объекты. _eval_expr теперь корректно ищет сначала полный путь как плоский ключ.

Работает корректно:

{
"type": "set_variable",
"data": {
"assignments": [
{ "id": "a1", "variable": "sell_price", "value": "{sell_ore.base_price_earth}", "mode": "text" },
{ "id": "a2", "variable": "sell_income", "value": "{sell_item.quantity} * {sell_price}", "mode": "expression" }
]
}
}

Каждый следующий assignment видит результат предыдущего (assignments выполняются последовательно).

2. bot_table upsert: ключ должен быть уникальным идентификатором записи

НЕПРАВИЛЬНОkey: "pilot_id" при составном ключе (pilot_id + ore_id):

{
"operation": "upsert",
"key": "pilot_id",
"row": { "pilot_id": "{user_id}", "ore_id": "iron", "quantity": "1" }
}

Результат: если у пилота уже есть ЛЮБАЯ руда, upsert найдёт первую запись по pilot_id и перезапишет её (потеря данных).

ПРАВИЛЬНО — использовать read + condition + insert/update:

read (where: pilot_id + ore_id) → condition (is_empty?)
→ insert (новая запись)
→ update (increment quantity)

3. Переменные не сбрасываются между вызовами

Если пользователь нажал кнопку "Продать Железо" → sell_item.quantity = 5. Потом нажал "Продать Уран" → read вернул 0 строк, но sell_item.quantity всё ещё = 5 от предыдущего вызова!

Решение: всегда проверять через is_empty (если read вернул 0 строк, поля объекта будут пустыми) И через equals "0":

{
"branches": [
{ "operator": "is_empty", "value": "", "target": "no-item" },
{ "operator": "equals", "value": "0", "target": "no-item" },
{ "operator": "else", "value": "", "target": "has-item" }
]
}

4. bot_table update decrement: может уйти в минус

decrement не проверяет что значение >= 0. Если cargo_used = 0 и вы делаете decrement 1, получите -1.

Решение: всегда ставить condition перед decrement:

condition (cargo_used > 0?) → update (decrement)

5. Inline кнопки: callback_data = target узла

При keyboardType: "inline" и action: "goto", callback_data автоматически равен target кнопки. Убедитесь что целевой узел существует на том же листе или доступен глобально.

6. bot_table read с all_rows: переменная — массив

После read с resultFormat: "all_rows" переменная содержит JSON-массив. Для доступа к элементам используйте loop или array_item в set_variable. Нельзя обращаться как {list.field} — это работает только с first_row.

7. bot_table read с random_row: случайная строка из результата

resultFormat: "random_row" — выбирает случайную строку из отфильтрованных результатов и сохраняет как объект (аналогично first_row).

Пример: выбрать случайную руду из трюма игрока:

{
"type": "bot_table",
"data": {
"tableName": "pilot_cargo",
"operation": "read",
"where": [
{ "column": "pilot_id", "operator": "equals", "value": "{user_id}" }
],
"saveResultTo": "stolen",
"resultFormat": "random_row",
"autoTransitionTo": "next_node"
}
}

После выполнения доступны: {stolen.ore_id}, {stolen.ore_name}, {stolen.ore_emoji}, {stolen.quantity}.

Если результат пуст (0 строк) — переменные не устанавливаются. Используйте condition с is_empty для проверки.

8. Inline кнопки с condition: проверка после нажатия

Inline кнопка с action: "goto" ведёт на target-ноду. Первой нодой в цепочке может быть condition — это позволяет проверять условия после нажатия кнопки.

Пример: кнопка "Откупиться" → проверка баланса:

{
"id": "btn-pay",
"text": "💰 Откупиться ({ransom} кр.)",
"action": "goto",
"target": "cond-check-balance"
}
{
"id": "cond-check-balance",
"type": "condition",
"data": {
"variable": "pilot.credits",
"branches": [
{ "operator": "less_than", "value": "{ransom}", "target": "msg-no-money" },
{ "operator": "else", "value": "", "target": "do-pay" }
]
}
}

Это заменяет "скрытие кнопки по условию" — кнопка всегда видна, но при нажатии проверяется условие.

9. schedule_trigger: выполняется без контекста пользователя

schedule_trigger запускается по расписанию один раз для всего бота, а не для каждого пользователя. У него нет user_id, chat_id или переменных конкретного игрока.

Что можно:

  • Обновить данные в таблицах (пересчитать цены, сбросить флаги)
  • Отправить сообщение в конкретный чат (если ID захардкожен или в переменной бота)
  • Через loop по таблице пользователей — отправить каждому

Что нельзя напрямую:

  • Отправить message "текущему пользователю" — его нет в контексте schedule
  • Использовать {user_id}, {first_name} — они пустые

Паттерн: рассылка всем пользователям по расписанию:

schedule_trigger (daily 12:00)
→ bot_table read users (all_rows, saveResultTo: "all_users")
→ loop (sourceVariable: "all_users", itemVariable: "user_item")
→ message (messageSendRecipients: [{type: "chat_id", chatId: "{user_item.telegram_id}"}])

Паттерн: ежедневный бонус (по кнопке, не по расписанию): Лучше реализовать через reply-кнопку или команду с проверкой timestamp:

trigger → read pilot → set now_ts (timestamp) → condition (last_daily is_empty OR now_ts > last_daily + 86400)
→ update (credits +500, last_daily = now_ts) → msg "Бонус получен!"
→ else → msg "Бонус ещё не готов, осталось: {remaining}"

10. messageSendRecipients: отправка сообщения другим пользователям

Поле messageSendRecipients в message-ноде позволяет отправить сообщение дополнительным получателям (помимо текущего пользователя):

{
"type": "message",
"data": {
"messageText": "Уведомление для вас!",
"messageSendRecipients": [
{ "id": "r1", "type": "chat_id", "chatId": "{target_user_id}" }
]
}
}

Типы: "user", "chat_id", "admin_ids". Поле chatId поддерживает переменные.