Руководство по созданию сценариев
📖 Полное руководство по созданию и настройке сценариев для Telegram-ботов с поддержкой плейсхолдеров, переходов и динамической логики.
📑 Содержание
- Структура сценария
- Триггеры (trigger)
- Scheduled сценарии (запуск по расписанию)
- Действия (step)
- Переходы (transition)
- Плейсхолдеры (Placeholders)
- Асинхронные действия (Async Actions)
📋 Структура сценария
Организация сценариев
Организация файлов — сценарии можно организовывать в папки и подпапки для удобства. Все YAML-файлы из папки scenarios/ (включая подпапки) автоматически загружаются.
Важные принципы:
- Уникальность названий: Название сценария должно быть уникальным в рамках всего тенанта
- Гибкая структура файлов: Можно создавать файлы с любыми названиями и организовывать их в подпапки — все файлы будут загружены
- Рекурсивная загрузка: Все YAML-файлы из
scenarios/и всех подпапок автоматически парсятся - Глобальные переходы: Сценарии могут переходить к любым другим сценариям тенанта через
jump_to_scenario
Пример структуры:
tenant_101/
scenarios/
commands.yaml # Сценарии: start, menu, help
settings.yaml # Сценарии: settings, profile
admin/ # Подпапка для организации
panel.yaml # Сценарии: admin_panel, logs
moderation.yaml # Сценарии: ban, kick
users/ # Еще одна подпапка
profile.yaml # Сценарии: user_profile, edit_profileВ этом примере:
- Все файлы из
scenarios/и подпапок загружаются автоматически - Сценарии могут переходить друг к другу независимо от расположения в папках
- Подпапки используются только для логической организации файлов
Структура YAML файла:
# Название сценария
scenario_name:
description: "Описание сценария (опционально)"
# Условия запуска сценария (опционально для scheduled сценариев)
trigger:
- event_type: "message"
event_text: "/start"
- event_type: "callback"
callback_data: "start"
# Расписание запуска (опционально, cron выражение)
schedule: "0 9 * * *" # Каждый день в 9:00
# Действия сценария
step:
- action: "send_message"
params:
text: "Привет!"
inline:
- [{"Кнопка": "callback_data"}]Параметры сценария:
description— описание сценария (опционально)trigger— условия запуска сценария (опционально для scheduled сценариев)schedule— cron выражение для запуска по расписанию (опционально)step— последовательность действий
Практический пример:
user_registration:
description: "Процесс регистрации пользователя с проверками и уведомлениями"
# ТРИГГЕРЫ: Каждый работает независимо - любой из них запустит сценарий
trigger:
- event_type: "message" # Команда /register
event_text: "/register"
- event_type: "callback" # Кнопка "Начать регистрацию"
callback_data: "start_registration"
# ШАГИ: Выполняются последовательно один за другим
step:
# Шаг 1: Отправляем приветствие
- action: "send_message"
params:
text: |
👋 Добро пожаловать, {username|fallback:Гость}!
Давайте зарегистрируем вас в системе.
inline:
- [{"✅ Продолжить": "continue_registration"}]
# Шаг 2: Проверяем права пользователя с переходами
- action: "validate"
params:
condition: "$user_role == 'admin'"
transition:
- action_result: "success"
transition_action: "jump_to_scenario"
transition_value: "admin_welcome" # Переход к админскому приветствию
- action_result: "error"
transition_action: "continue" # Обычный поток для пользователей
# Шаг 3: Запрашиваем имя
- action: "send_message"
params:
text: |
📝 Шаг 2 из 3
Пожалуйста, введите ваше имя:
inline:
- [{"❌ Отмена": "cancel_registration"}]
# Шаг 4: Отправляем подтверждение с вложениями
- action: "send_message"
params:
text: |
✅ Регистрация начата!
Мы отправили вам инструкции на email.
📎 Также прикрепляем полезные материалы:
attachment:
- file_id: "AgACAgIAAxkBAAIUJWjyLorr_d8Jwof3V_gVjVNdGHyXAAJ--zEb4vGQS-a9UiGzpkTNAQADAgADeQADNgW"
type: "photo"
- file_id: "AgACAgIAAxkBAAIUQWjyOH65wQYR4onkrrqM4J65yUD7AAM8-zEb4vGQS1mwvckFcZHlAQADAgADdwADNgQ"
type: "photo"
inline:
- [{"🏠 Главное меню": "main_menu"}]
# Шаг 5: Удаляем предыдущее сообщение
- action: "delete_message"
params:
delete_message_id: "{last_message_id}" # Используем ID предыдущего сообщения
admin_welcome:
description: "Приветствие для админов (транзишн-сценарий)"
# НЕТ ТРИГГЕРОВ - вызывается только программно
step:
- action: "send_message"
params:
text: |
🔧 Добро пожаловать, администратор!
У вас есть дополнительные права.
inline:
- [{"⚙️ Панель управления": "admin_panel"}]Ключевые моменты:
- Триггеры — логика ИЛИ: если сработал хотя бы один, сценарий запускается
- Шаги — выполняются строго по порядку записи
- Переходы — управляют потоком выполнения через
transition:continue— продолжить выполнение следующих действийbreak— прервать выполнение только текущего сценарияabort— прервать всю цепочку выполнения текущего сценария (включая вложенные)stop— прервать всю обработку события (все сценарии)move_steps— переместиться на указанное количество шагов (положительное = вперед, отрицательное = назад)jump_to_step— перейти к конкретному шагу по индексу (шаги нумеруются с 0)jump_to_scenario— перейти к другому сценарию (к любому сценарию тенанта)
- Транзишн-сценарии — сценарии без триггеров, вызываются программно из других сценариев
- Scheduled сценарии — сценарии с полем
schedule(cron выражение), запускаются автоматически по расписанию - Гибридные сценарии — могут иметь и
trigger, иscheduleодновременно (работают по событиям И по расписанию) - Данные события — доступны в параметрах действий (user_id, chat_id, timestamp и др.)
- Вложения — можно пересылать файлы через
attachment[0].file_idиattachment[0].type
🎯 Триггеры (trigger)
Назначение: Условия, при которых запускается сценарий.
Механика работы триггеров:
Простая форма (атрибуты):
trigger:
- event_type: "message"
event_text: "/start"Преобразуется в условие:
$event_type == "message" and $event_text == "/start"Сложная форма (с condition):
trigger:
- event_type: "message"
condition: |
$event_text == "test"
and $user_id > 100
or $username in ('@admin', '@moderator')Преобразуется в условие:
$event_type == "message" and ($event_text == "test" and $user_id > 100 or $username in ('@admin', '@moderator'))Логика триггеров:
- Между триггерами — логика ИЛИ (OR): если сработал хотя бы один, сценарий запускается
- Внутри триггера — логика И (AND): все условия должны выполняться
- В condition — полная свобода:
and,or,not, скобки, операторы
⚠️ Важно: Плейсхолдеры в условиях condition
Проблема: При использовании плейсхолдеров в condition триггеров для сравнения строк нужно оборачивать плейсхолдер в кавычки. Иначе плейсхолдер будет "разворачиваться" как атрибут для поиска данных в condition_parser, а не как строковое значение.
Решение: Всегда оборачивайте плейсхолдеры в кавычки при сравнении строк в условиях.
Примеры:
# ❌ ОШИБКА - плейсхолдер развернется в random_user без кавычек
# condition_parser попытается найти переменную random_user в данных
trigger:
- event_type: "message"
condition: "{_cache.detected_intent} == 'random_user'"
# После обработки плейсхолдера: random_user == 'random_user'
# condition_parser ищет переменную random_user в данных, а не сравнивает строки!
# ✅ ПРАВИЛЬНО - плейсхолдер обернут в кавычки
trigger:
- event_type: "message"
condition: "'{_cache.detected_intent}' == 'random_user'"
# После обработки плейсхолдера: 'random_user' == 'random_user'
# condition_parser корректно сравнивает строки
# ✅ ПРАВИЛЬНО - сравнение чисел (кавычки не нужны)
trigger:
- event_type: "message"
condition: "{user_id} > 100"
# После обработки плейсхолдера: 12345 > 100
# Числовое сравнение работает корректно
# ✅ ПРАВИЛЬНО - сравнение с модификаторами (кавычки не нужны для boolean)
trigger:
- event_type: "message"
condition: "{response_value.feedback|exists} == True"
# Модификатор exists возвращает boolean, сравнение работает корректноПравило:
- Для сравнения строк — всегда оборачивайте плейсхолдер в кавычки:
'{_cache.field}' == 'value' - Для сравнения чисел — кавычки не нужны:
{user_id} > 100 - Для boolean значений — кавычки не нужны:
{field|exists} == True
Доступные поля для условий:
⚠️ Важно: Все поля в условиях должны иметь маркер $name (например, $event_type, $user_id, $event_text).
Данные события:
$event_text— текст сообщения$event_type— тип события (message, callback)$user_id— ID пользователя$username— имя пользователя$chat_id— ID чата$callback_data— данные callback кнопки$event_date— время события$tenant_id— ID тенанта$message_id— ID сообщения$chat_type— тип чата$is_group— является ли чат группой- И другие поля события (см. EVENT_GUIDE.md)
Данные предыдущих действий:
$last_message_id— ID последнего отправленного сообщения$response_data— любые данные, возвращенные предыдущими действиями- Другие поля — в зависимости от действий в сценарии
Вложенные поля:
$message.text— доступ к вложенным полям через точку$user.profile.name— многоуровневая вложенность
Операторы в условиях:
==— равно!=— не равно>,<,>=,<=— сравнение чиселin,not in— вхождение в список~— содержит подстроку!~— не содержит подстрокуregex— регулярное выражениеis_null— поле пустое (None или пустая строка)not is_null— поле не пустое (существует и имеет значение)
⏰ Scheduled сценарии (запуск по расписанию)
Назначение: Автоматический запуск сценариев по расписанию через cron выражения.
Структура scheduled сценария:
daily_report:
description: "Ежедневный отчет в 9:00"
# Cron выражение для запуска каждый день в 9:00
schedule: "0 9 * * *"
# Триггеры опциональны - сценарий будет работать по расписанию
# Можно добавить триггеры для гибридного режима (по событиям И по расписанию)
step:
- action: "send_message"
params:
target_chat_id: 123456789
text: |
📊 Ежедневный отчет
Время запуска: {scheduled_at|format:datetime}
Сценарий: {scheduled_scenario_name}Cron выражения:
Формат: минута час день месяц день_недели
| Пример | Описание |
|---|---|
* * * * * | Каждую минуту |
*/5 * * * * | Каждые 5 минут |
0 * * * * | Каждый час в начале часа |
0 9 * * * | Каждый день в 9:00 |
0 9 * * 1-5 | Каждый рабочий день в 9:00 |
0 0 1 * * | Первое число каждого месяца в 00:00 |
ℹ️ Важно: Cron выражения интерпретируются в локальном времени платформы.
Доступные поля в scheduled сценариях:
В scheduled сценариях доступны специальные поля события. Подробное описание всех полей см. в разделе Поля scheduled сценариев в EVENT_GUIDE.md.
Пример использования:
step:
- action: "send_message"
params:
text: |
⏰ Scheduled сценарий запущен
Время: {scheduled_at|format:datetime}
Название: {scheduled_scenario_name}
ID: {scheduled_scenario_id}Гибридный режим (trigger + schedule):
Сценарий может работать и по событиям, и по расписанию одновременно:
hybrid_scenario:
description: "Работает по событиям И по расписанию"
# Запуск по событиям
trigger:
- event_type: "message"
event_text: "/report"
# Запуск по расписанию (каждый день в 9:00)
schedule: "0 9 * * *"
step:
- action: "send_message"
params:
text: |
📊 Отчет
{scheduled_at|fallback:Запущено вручную|format:datetime}⚠️ Важные ограничения и рекомендации:
1. Синхронизация сценариев:
- При синхронизации сценариев (удаление и пересоздание) теряется информация о последнем запуске (
last_scheduled_run) - После синхронизации scheduled сценарии пересчитывают следующее время запуска от текущего момента
- Рекомендация: Синхронизируйте сценарии в периоды низкой активности scheduled задач
2. Пропуск запуска:
- Если синхронизация происходит в момент, когда должен запускаться scheduled сценарий, запуск может быть пропущен
- Рекомендация: Избегайте синхронизации сценариев за несколько секунд до запланированного времени запуска
3. Поведение при ошибках:
- Scheduled сценарии обновляют время следующего запуска даже при ошибке выполнения
- Это гарантирует предсказуемое поведение и предотвращает повторные запуски проблемных сценариев
4. Контекст выполнения:
- Scheduled сценарии выполняются с системным контекстом (автоматически получают
bot_id,tenant_id) - Общие поля события недоступны: В scheduled сценариях большинство общих полей (
user_id,chat_id,event_text,message_idи др.) недоступны, так как событие не связано с пользователем или чатом - Передача параметров: Необходимо передавать большинство параметров вручную в действиях или извлекать данные из storage тенанта или пользователя (из базы данных)
- Используйте
target_chat_idв параметрах действий для указания конкретного чата
⚡ Действия (step)
Назначение: Последовательность действий, выполняемых в сценарии.
Важно:
- Порядок выполнения шагов определяется их позицией в массиве
step(сверху вниз). Шаги выполняются последовательно в том порядке, в котором они указаны в YAML. Нумерация шагов начинается с 0. - Последовательное выполнение: По умолчанию сценарий идет последовательно по шагам независимо от результата действия. Если у шага нет
transitionили нет подходящего перехода для результата действия, выполнение автоматически переходит к следующему шагу. - Изменение потока: Переходы (
transition) позволяют изменить последовательность выполнения (перейти к другому шагу, сценарию, прервать выполнение и т.д.).
Отправка сообщения:
Inline кнопки:
step:
- action: "send_message"
params:
text: |
👋 Привет!
📋 Выберите действие:
inline:
- [{"📋 Меню": "main_menu"}, {"📚 Справка": "help"}]
- [{"🌐 Сайт": "https://example.com"}, {"📞 Поддержка": "tg://resolve?domain=support"}]
- [{"🔙 Назад": "start"}]Reply клавиатура:
step:
- action: "send_message"
params:
text: |
👋 Привет!
📋 Выберите действие:
reply:
- ["📋 Меню", "📚 Справка"]
- ["🔙 Назад"]Убрать клавиатуру:
step:
- action: "send_message"
params:
text: "Клавиатура убрана"
reply: [] # Пустой массив убирает клавиатуруПараметры сообщения:
text— текст сообщения (поддерживает многострочность с|)inline— массив кнопок:[{"Текст кнопки": "callback_data"}]— одна кнопка в строке[{"Кнопка 1": "data1"}, {"Кнопка 2": "data2"}]— несколько кнопок в строке[{"Ссылка": "https://example.com"}]— кнопка со ссылкой
reply— reply клавиатура:[["Кнопка 1", "Кнопка 2"]]— одна строка кнопок[["Кнопка 1"], ["Кнопка 2"]]— каждая кнопка в отдельной строке[]— убрать клавиатуру
attachment— массив вложений (фото, документы, видео, аудио):[{"file_id": "AgACAgI...", "type": "photo"}]— пересылка файла по file_id[{"file_id": "{event_attachment[0].file_id}", "type": "{event_attachment[0].type}"}]— динамическая пересылка из события[{...}, {...}]— несколько файлов за раз
Прикрепление файлов:
📎 Прикрепление файлов к сообщениям:
Можно отправлять фото, документы, видео, аудио. Файлы можно брать из событий или использовать сохранённые file_id.
Пример: Пересылка файла из события:
step:
- action: "send_message"
params:
text: "Пересылаю ваш файл обратно"
attachment:
- file_id: "{event_attachment[0].file_id}"
type: "{event_attachment[0].type}"Пример: Отправка файла по известному file_id:
step:
- action: "send_message"
params:
text: "Инструкция"
attachment:
- file_id: "AgACAgIAAxkBAAIUJWjyLorr_d8Jwof3V_gVjVNdGHyXAAJ--zEb4vGQS-a9UiGzpkTNAQADAgADeQADNgW"
type: "photo"Пример: Несколько файлов:
step:
- action: "send_message"
params:
text: "Полезные материалы"
attachment:
- file_id: "{event_attachment[0].file_id}"
type: "{event_attachment[0].type}"
- file_id: "BQACAgIAAxkBAAI..."
type: "document"📋 Как получить file_id и type: см. пример сценария ниже 👇
Конфигурация тенанта в действиях:
Назначение: Автоматическое использование атрибутов конфигурации тенанта в действиях.
Некоторые действия автоматически используют параметры из конфигурации тенанта (_config). Такие параметры не нужно передавать явно в params — они автоматически берутся из _config.
Пример использования:
step:
- action: "completion"
params:
prompt: "Привет, как дела?"
# openrouter_token автоматически берется из _config.openrouter_token
# Не нужно указывать openrouter_token явно!Доступ через плейсхолдеры:
Конфигурация тенанта также доступна напрямую через плейсхолдеры:
step:
- action: "send_message"
params:
text: |
Конфигурация тенанта:
Токен: {_config.openrouter_token|fallback:Не установлен}Важно:
_configдоступен во всех сценариях тенанта автоматически- Если атрибут не установлен в конфигурации тенанта, значение будет
None - Используйте
fallbackв плейсхолдерах для обработки отсутствующих значений - Если параметр передан явно в
params, явное значение имеет приоритет
📖 Подробнее о конфигурации тенанта: См. раздел config.yaml в TENANT_CONFIG_GUIDE.md
Кэширование данных в сценариях:
Назначение: Автоматическое сохранение результатов действий в _cache для использования в последующих шагах сценария.
⚠️ Важно: Область видимости кэша:
- Кэш
_cacheсохраняется только в рамках одной цепочки выполнения сценариев (одна цепочка обработки одного события) - Данные доступны между сценариями в рамках одной цепочки выполнения (например, при переходе через
jump_to_scenarioилиexecute_scenario) - Кэш НЕ сохраняется между разными цепочками выполнения (даже если они триггерятся одним событием — каждый сценарий имеет свой изолированный кэш)
- Кэш НЕ сохраняется между разными событиями (разными сообщениями, разными callback'ами)
- Для долгосрочного хранения данных используйте
set_storage/set_user_storageвместоset_cache - Подробнее см. раздел Сохранение контекста в рамках обработки события
Общая механика работы:
- Каждое действие может вернуть
response_dataс любыми полями - Данные автоматически сохраняются в
_cacheи доступны в последующих действиях через плейсхолдеры - Система кэширования поддерживает два системных поля для управления структурой данных:
_namespace— для контроля перезаписи и организации данных в вложенные структуры_response_key— для подмены ключа replaceable поля перед сохранением в_cache
- Все механизмы работают на уровне движка сценариев и не требуют изменений в сервисах
Плоское кэширование (по умолчанию):
По умолчанию используется плоское кэширование — данные мержатся напрямую в _cache без вложенности. Доступ к данным осуществляется через {_cache.field}:
step:
- action: "generate_int"
params:
min: 1
max: 100
# response_data: {"random_value": 42, "random_seed": 123}
# Попадает в: _cache.random_value = 42, _cache.random_seed = 123
- action: "send_message"
params:
text: "Число: {_cache.random_value}" # ✅ ПРАВИЛЬНОПример с несколькими действиями:
step:
- action: "generate_int"
params:
min: 1
max: 100
# _cache.random_value = 42, _cache.random_seed = 123
- action: "generate_array"
params:
min: 1
max: 10
count: 5
# _cache.random_list = [3, 7, 2, 9, 1], _cache.random_seed = 456
- action: "send_message"
params:
text: |
Число: {_cache.random_value}
Массив: {_cache.random_list|comma}⚠️ Проблемы плоского кэширования:
1. Конфликты имен между разными действиями:
step:
- action: "get_storage_value"
params:
group_key: "settings"
key: "api_key"
# _cache.response_value = "key1"
- action: "get_user_storage_value"
params:
key: "api_secret"
# _cache.response_value = "secret1" # ⚠️ Перезаписано!
- action: "send_message"
params:
text: "Key: {_cache.response_value}" # Покажет только "secret1"2. Перезапись при повторных вызовах одного действия:
step:
- action: "get_storage_value"
params:
group_key: "settings"
key: "api_key"
# _cache.response_value = "key1"
- action: "get_storage_value" # То же действие!
params:
group_key: "settings"
key: "api_secret"
# _cache.response_value = "secret1" # ⚠️ Первый результат перезаписан!Системное поле _namespace (контроль перезаписи):
Назначение: Сохранение данных в вложенную структуру _cache[_namespace] для контроля перезаписи и избежания конфликтов имен.
Механика работы:
- Если указан
_namespace, данные сохраняются в_cache[_namespace]вместо плоского_cache - Доступ к данным:
{_cache[_namespace].field} - Позволяет сохранять результаты повторных вызовов одного действия
- Позволяет избежать конфликтов имен между разными действиями
Пример использования:
step:
- action: "generate_int"
params:
min: 1
max: 100
_namespace: "my_random"
# Попадает в: _cache.my_random = {random_value: 42, random_seed: 123}
- action: "send_message"
params:
text: "Число: {_cache.my_random.random_value}" # ✅ ПРАВИЛЬНОРешение конфликтов с _namespace:
step:
- action: "get_storage_value"
params:
group_key: "settings"
key: "api_key"
_namespace: "first"
# _cache.first.response_value = "key1"
- action: "get_storage_value"
params:
group_key: "settings"
key: "api_secret"
_namespace: "second"
# _cache.second.response_value = "secret1"
# ✅ Оба результата сохранены!
- action: "send_message"
params:
text: |
Первый: {_cache.first.response_value}
Второй: {_cache.second.response_value}Системное поле _response_key (переименование основного поля):
Назначение: Позволяет задать кастомное имя для основного поля результата действия для удобного доступа к данным в _cache.
Механика работы:
- Некоторые действия возвращают основное значение под определенным ключом (например,
storage_values,user_storage_values,formatted_text) - По умолчанию данные сохраняются в
_cacheс оригинальным ключом из результата действия - Если указан
_response_key, движок автоматически переименовывает основное поле перед сохранением в_cache - Работает только для действий, которые поддерживают переименование основного поля
Пример использования:
step:
# Получаем storage значение (по умолчанию будет в _cache.storage_values)
- action: "get_storage"
params:
tenant_id: "{tenant_id}"
group_key: "settings"
key: "api_key"
_response_key: "api_key" # Переименовываем storage_values в api_key
# response_data: {"storage_values": "secret_key_123"}
# Попадает в: _cache.api_key = "secret_key_123" (вместо _cache.storage_values)
# Используем переименованное значение
- action: "send_message"
params:
text: "API ключ: {_cache.api_key}" # ✅ ПРАВИЛЬНО - используем кастомный ключПример с несколькими вызовами одного действия:
step:
# Первый вызов - сохраняем в api_key
- action: "get_storage"
params:
tenant_id: "{tenant_id}"
group_key: "settings"
key: "api_key"
_response_key: "api_key"
# _cache.api_key = "secret_key_123"
# Второй вызов - сохраняем в api_secret
- action: "get_storage"
params:
tenant_id: "{tenant_id}"
group_key: "settings"
key: "api_secret"
_response_key: "api_secret"
# _cache.api_secret = "secret_secret_456"
# ✅ Оба значения сохранены под удобными ключами!
- action: "send_message"
params:
text: |
API Key: {_cache.api_key}
API Secret: {_cache.api_secret}Пример с форматированием текста:
step:
# Форматируем данные и сохраняем под кастомным ключом
- action: "format_data_to_text"
params:
format_type: "list"
title: "Доступные намерения:"
item_template: '- "$id" - $description'
input_data: "{storage_values.ai_router.intents}"
_response_key: "formatted_intents"
# response_data: {"formatted_text": "• intent1\n• intent2"}
# Попадает в: _cache.formatted_intents = "• intent1\n• intent2"
# (вместо _cache.format_data_to_text.formatted_text)
- action: "send_message"
params:
text: "{_cache.formatted_intents}" # ✅ Используем удобный ключКомбинация _namespace и _response_key:
step:
# Используем и _namespace, и _response_key одновременно
- action: "get_storage"
params:
tenant_id: "{tenant_id}"
group_key: "settings"
key: "api_key"
_namespace: "first"
_response_key: "api_key"
# _cache.first.api_key = "secret_key_123"
# (вместо _cache.first.storage_values)
- action: "get_storage"
params:
tenant_id: "{tenant_id}"
group_key: "settings"
key: "api_secret"
_namespace: "second"
_response_key: "api_secret"
# _cache.second.api_secret = "secret_secret_456"
- action: "send_message"
params:
text: |
Первый: {_cache.first.api_key}
Второй: {_cache.second.api_secret}Пример с форматированием текста:
step:
# Форматируем данные и сохраняем под кастомным ключом
- action: "format_data_to_text"
params:
format_type: "list"
title: "Доступные намерения:"
item_template: '- "$id" - $description'
input_data: "{storage_values.ai_router.intents}"
_response_key: "formatted_intents"
# response_data: {"formatted_text": "• intent1\n• intent2"}
# Попадает в: _cache.formatted_intents = "• intent1\n• intent2"
# (вместо _cache.format_data_to_text.formatted_text)
- action: "send_message"
params:
text: "{_cache.formatted_intents}" # ✅ Используем удобный ключРекомендации по использованию:
Общие рекомендации:
- ✅ Плоское кэширование по умолчанию — меньше вложенности, проще доступ
- ✅ Используйте
_namespaceдля контроля перезаписи при повторных вызовах одного действия - ✅ Используйте
_namespaceдля избежания конфликтов имен между разными действиями - ✅ Используйте
_response_keyдля удобства доступа к основным значениям действий - ✅ Комбинируйте
_namespaceи_response_keyдля полного контроля над структурой данных в_cache - ✅ Используйте осмысленные имена для
_namespaceи_response_key - ✅ Используйте
set_cacheдля выноса данных из вложенности во флэт при необходимости
⚠️ Важно:
_response_keyработает только для действий, которые поддерживают подмену ключа основного значения- Если действие не поддерживает подмену ключа,
_response_keyигнорируется (без ошибки) - Подмена происходит автоматически перед сохранением в
_cache
Сохранение контекста в рамках обработки события:
⚠️ Важно: Кэш _cache сохраняется только в рамках одной цепочки выполнения сценариев. Это означает, что данные доступны между сценариями в рамках одной цепочки (когда один сценарий переходит к другому через jump_to_scenario или execute_scenario), но не сохраняются между разными цепочками выполнения (даже если они триггерятся одним событием) и между разными событиями.
Что такое "одна цепочка выполнения":
- Одна цепочка = последовательность сценариев, связанных переходами (
jump_to_scenario,execute_scenario) - Если сценарий A переходит к сценарию B через
jump_to_scenario— это одна цепочка, кэш сохраняется - Если сценарий A вызывает сценарий B через
execute_scenario— это одна цепочка, кэш сохраняется - Если событие триггерит несколько независимых сценариев — это разные цепочки, у каждой свой изолированный кэш
Что такое "одно событие":
- Одно событие = одно сообщение от пользователя, один callback от кнопки, один scheduled запуск и т.д.
- Если пользователь отправил сообщение
/start— это одно событие - Если пользователь нажал кнопку с
callback_data: "menu"— это другое, отдельное событие - Каждое событие может триггерить несколько сценариев, но каждый сценарий выполняется независимо со своим кэшем
Что сохраняется в рамках одного события:
- Все данные события (event data:
user_id,chat_id,event_textи т.д.) - Все результаты действий (данные из
response_dataавтоматически попадают в плоский_cacheили_cache[_namespace]если указан_namespace, с учетом подмены ключа через_response_key) - Временные данные из
_cache(сохраненные через действиеset_cacheили другие действия) - Любые другие данные, накопленные в контексте обработки события
Когда кэш сохраняется (в рамках одной цепочки выполнения):
✅ При переходе через jump_to_scenario:
# Сценарий 1: main_scenario
step:
- action: "set_cache"
params:
cache:
selected_user: "@username"
- action: "send_message"
transition:
- action_result: "success"
transition_action: "jump_to_scenario"
transition_value: "next_scenario"
# Сценарий 2: next_scenario
# ✅ Все данные из main_scenario доступны (одна цепочка выполнения):
step:
- action: "send_message"
params:
text: "Пользователь: {_cache.set_cache.selected_user}" # ✅ ДоступенКогда кэш НЕ сохраняется:
❌ Между разными сценариями, которые триггерятся одним событием:
# Событие: message "/start"
# Сценарий 1: welcome_scenario (триггерится "/start")
step:
- action: "set_cache"
params:
cache:
welcome_sent: true
# Сценарий 2: analytics_scenario (тоже триггерится "/start", но выполняется независимо!)
step:
- action: "send_message"
params:
text: "Welcome sent: {_cache.set_cache.welcome_sent}" # ❌ НЕ доступно! Разные цепочки выполнения❌ Между разными событиями:
# Событие 1: Пользователь отправил "/start"
step:
- action: "set_cache"
params:
cache:
step: 1
# Событие 2: Пользователь отправил "/menu" (другое событие!)
step:
- action: "send_message"
params:
text: "Step: {_cache.set_cache.step}" # ❌ НЕ доступно! Разные события🔄 Переходы (transition)
Назначение: Управление потоком выполнения сценария.
step:
- action: "validate"
params:
condition: "$user_role == 'admin'"
transition:
- action_result: "success"
transition_action: "jump_to_scenario"
transition_value: "admin_panel" # Переход к сценарию админки
- action_result: "error"
transition_action: "continue" # Обычный поток для пользователей
- action_result: "timeout"
transition_action: "break" # Прервать только текущий сценарий при таймауте
- action_result: "error"
transition_action: "abort" # Прервать всю цепочку при критической ошибке
- action_result: "system_failure"
transition_action: "stop" # Прервать всю обработку события при системной ошибкеТипы переходов:
continue— продолжить выполнение следующих действийbreak— прервать выполнение только текущего сценарияabort— прервать всю цепочку выполнения текущего сценария (включая вложенные)stop— прервать всю обработку события (все сценарии)move_steps— переместиться на указанное количество шагов (положительное число = вперед, отрицательное = назад). Например,move_step: 1перейдет к следующему шагу,move_step: 2пропустит 1 шаг и перейдет к следующемуjump_to_step— перейти к конкретному шагу по индексу (шаги нумеруются с 0, например,jump_to_step: 5перейдет к шагу с индексом 5)jump_to_scenario— перейти к другому сценарию (аналогично break + прыжок, текущий сценарий далее не выполняется)any— универсальный переход, который обрабатывается первым независимо от результата действия
Особенности переходов:
transition_value— обязателен дляmove_steps(количество шагов),jump_to_step(индекс шага, начиная с 0) иjump_to_scenario(название сценария)- Поиск сценария — система ищет сценарий в том же tenant'е
- Приоритет переходов — переход
anyобрабатывается первым, независимо от результата действия - Единственный переход — всегда выполняется только один переход из всех возможных
- Блокировка других переходов — если есть переход
any, остальные переходы (success,error,timeoutи т.д.) не будут обработаны - Различие между переходами:
break— прерывает только текущий сценарий, другие сценарии (если есть) продолжают обработку событияabort— прерывает всю цепочку выполнения текущего сценария (включая все вложенные сценарии), но позволяет другим сценариям (из других триггеров) продолжить работуstop— полностью прекращает обработку события во всех сценариях, возвращает управление системеjump_to_scenario— прерывает выполнение текущего сценария и переходит к другому (аналогично break + прыжок)
🔧 Плейсхолдеры (Placeholders)
Назначение: Динамическая подстановка значений в параметры действий сценариев.
Механика работы:
Все атрибуты в params автоматически обрабатываются плейсхолдерами перед выполнением действия. Это означает:
- Все значения в
params— строки, числа, массивы, объекты — проходят через обработку плейсхолдеров - Все уровни вложенности — плейсхолдеры работают на любом уровне структуры
params(в строках, элементах массивов, значениях объектов) - Автоматическая обработка — происходит для каждого шага сценария перед выполнением действия
- Источник данных — система использует накопленные данные события и результаты предыдущих действий как источник значений для подстановки
Синтаксис плейсхолдеров:
Простая замена:
params:
text: "Привет, {username}!"
chat_id: "{user_id}"С модификаторами:
params:
text: "Привет, {username|fallback:Гость}!"
price: "{amount|*0.9|format:currency}"Литеральные значения в кавычках:
params:
text: "{'hello'|upper}" # HELLO (без передачи через словарь)
duration: "{'1d 2w'|seconds}" # 1296000 (литеральное время)
calculation: "{'100'|+50}" # 150 (литеральная арифметика)
date_shift: "{'2024-12-25'|shift:+1 day}" # 2024-12-26 (литеральная дата)
# Двойные кавычки для строк с одинарными кавычками
text: "{"it's working"}" # it's working
# Экранированные кавычки
text: "{'it\'s working'}" # it's workingВложенные плейсхолдеры:
params:
text: "Ответ: {status|equals:{expected}|value:OK|fallback:BAD}"⚠️ Важно: Кавычки в YAML для плейсхолдеров с regex
Проблема: YAML по-разному обрабатывает escape-последовательности в одинарных и двойных кавычках, что критично для regex паттернов с обратными слешами.
Решение: Используйте двойные кавычки для regex паттернов с обратными слешами (\s, \d, \w и т.д.).
Примеры:
# ❌ ОШИБКА - в одинарных кавычках \\s не экранируется правильно
params:
value: '{event_text|regex:^([^\\s]+)|lower}' # \\s становится \s (обратный слеш + s), а не пробел!
# ✅ ПРАВИЛЬНО - двойные кавычки правильно экранируют обратные слеши
params:
value: "{event_text|regex:^([^\\s]+)|lower}" # \\s становится \s (пробел в regex)
# ✅ ПРАВИЛЬНО - для regex без обратных слешей можно использовать одинарные
params:
tenant_id: '{user_state|regex:tenant_(\d+)}' # Работает, так как \d не требует двойного экранирования
# ✅ ПРАВИЛЬНО - для простых regex с одним обратным слешем тоже двойные кавычки
params:
value: "{event_text|regex:\\s+([^\\s]+)$|lower}" # \\s для пробела, \\s для пробела
# ✅ ОБЫЧНЫЕ ПЛЕЙСХОЛДЕРЫ - без разницы
params:
text: "Привет, {username}!" # Работает
text: 'Привет, {username}!' # Тоже работаетКогда использовать двойные кавычки:
- Regex паттерны с обратными слешами —
{field|regex:pattern}где pattern содержит\s,\d,\w,\n,\tи т.д. - Важно: В двойных кавычках нужно использовать двойное экранирование:
\\sдля пробела,\\dдля цифры - Пример:
"{event_text|regex:^([^\\s]+)|lower}"—\\sстановится\s(пробел в regex)
Когда можно использовать одинарные кавычки:
- Regex паттерны без обратных слешей — простые паттерны без
\s,\d,\wи т.д. - Обычные плейсхолдеры —
{username},{user_id} - Простые модификаторы —
{name|upper},{price|format:currency} - Текстовые значения — без специальных символов
Правило: Если в regex паттерне есть обратные слеши (\s, \d, \w и т.д.) — всегда используйте двойные кавычки с двойным экранированием (\\s, \\d, \\w).
Доступ к вложенным данным:
params:
# Точечная нотация для объектов
text: "Пользователь: {user.profile.name|fallback:Неизвестный}"
text: "Сообщение: {message.text}"
# Индексы для массивов
text: "Первый файл: {attachment[0].file_id}"
text: "Последний пользователь: {users[-1].name}"
# Комбинированный доступ
text: "Событие: {event.attachment[0].type}"
text: "Данные: {response.data.items[0].value}"Доступные данные:
Все данные события и предыдущих действий доступны для использования в плейсхолдерах:
Данные события:
user_id— ID пользователяusername— имя пользователяchat_id— ID чатаevent_text— текст сообщенияcallback_data— данные callback кнопкиevent_date— время ивентаtenant_id— ID тенантаmessage_id— ID сообщенияreply_to_message_id— ID сообщения, на которое отвечаемforward_from— данные о пересылке- И любые другие поля — в зависимости от типа события
Конфигурация тенанта:
_config— словарь с конфигурацией тенанта (атрибуты изconfig.yaml)_config.openrouter_token— OpenRouter API ключ для тенанта (если установлен)- Другие атрибуты — в зависимости от конфигурации тенанта
- Автоматически доступен во всех сценариях тенанта
Данные предыдущих действий:
last_message_id— ID последнего отправленного сообщенияresponse_data— любые данные, возвращенные предыдущими действиями- Другие поля — в зависимости от действий в сценарии
Доступ к вложенным элементам:
Универсальный доступ к данным! Поддержка точечной нотации для объектов и индексов для массивов.
| Синтаксис | Описание | Пример | Результат |
|---|---|---|---|
| Объекты | |||
object.field | Доступ к полю объекта | {message.text} | Привет мир |
object.field.subfield | Вложенные поля | {user.profile.name} | Иван Петров |
| Массивы | |||
array[index] | Доступ к элементу массива | {attachment[0].file_id} | file_1 (первый файл) |
array[-index] | Отрицательный индекс | {attachment[-1].file_id} | file_3 (последний файл) |
array[index].field | Поле элемента массива | {users[1].name} | Боб (имя второго пользователя) |
array[index][index] | Вложенные массивы | {matrix[0][1]} | 2 (элемент матрицы) |
| Комбинированный | |||
object.array[index].field | Смешанный доступ | {event.attachment[0].file_id} | file_1 |
Примеры использования:
params:
# Объекты
text: "Сообщение: {message.text}"
text: "Пользователь: {user.profile.name|fallback:Неизвестный}"
# Массивы
text: "Первый файл: {attachment[0].file_id}"
text: "Последний пользователь: {users[-1].name|upper}"
text: "Файл: {attachment[0].file_id}, размер: {attachment[0].size}"
# Комбинированный доступ
text: "Событие: {event.attachment[0].type}"
text: "Данные: {response.data.items[0].value}"
# Конфигурация тенанта
text: "Токен: {_config.openrouter_token|fallback:Не установлен}"
# Безопасный доступ
text: "{attachment[10].file_id|fallback:Файл не найден}"Получение информации о файле
Практический пример: Получение file_id и type для пересылки файлов
Создайте сценарий, который покажет file_id и type всех вложений из события:
file_info:
description: "Получение file_id и type вложений для последующей пересылки"
trigger:
- event_type: "message"
event_text: "/file"
step:
- action: "send_message"
params:
text: |
📎 Информация о файле:
file_id: <code>{event_attachment[0].file_id|fallback:{reply_attachment[0].file_id|fallback:Файл не найден}}</code>
type: <code>{event_attachment[0].type|fallback:{reply_attachment[0].type|fallback:Тип не найден}}</code>Обработка ошибок:
- При обращении к несуществующему полю/индексу возвращается
None - Можно использовать модификатор
fallbackдля значения по умолчанию - Отрицательные индексы работают как в Python:
-1= последний элемент - Поддерживается неограниченная глубина вложенности
Модификаторы:
| Модификатор | Описание | Пример | Результат |
|---|---|---|---|
| Арифметические операции | |||
+value | Сложение | {price|+100} | 1500 (если price=1400) |
-value | Вычитание | {price|-50} | 1350 (если price=1400) |
*value | Умножение | {price|*0.9} | 1260 (если price=1400) |
/value | Деление | {seconds|/3600} | 2.5 (если seconds=9000) |
%value | Остаток от деления | {number|%7} | 3 (если number=10) |
| Регистр | |||
upper | Верхний регистр | {name|upper} | ИВАН (если name="Иван") |
lower | Нижний регистр | {name|lower} | иван (если name="Иван") |
title | Заглавные буквы каждого слова | {name|title} | Иван Петров (если name="иван петров") |
capitalize | Первая заглавная буква | {name|capitalize} | Иван (если name="иван") |
case:type | Преобразование регистра | {name|case:upper} | ИВАН (если name="Иван") |
| Списки | |||
tags | Преобразование в теги | {users|tags} | @user1 @user2 |
list | Маркированный список | {items|list} | • item1\n• item2 |
comma | Через запятую | {items|comma} | item1, item2 |
expand | Разворачивание массива массивов на один уровень (только в массивах) | {keyboard|expand} | [[a, b], [c]] → [a, b], [c] (в массиве) |
keys | Извлечение ключей из объекта (словаря) в массив | {storage_values|keys} | ["group1", "group2"] (если storage_values={"group1": {...}, "group2": {...}}) |
| Преобразования | |||
code | Оборачивание значения в code блок | {field|code} | <code>value</code> |
| Форматирование | |||
truncate:length | Обрезка текста | {text|truncate:50} | Очень длинный текст... |
format:date | Формат даты | {timestamp|format:date} | 25.12.2024 |
format:time | Формат времени | {timestamp|format:time} | 14:30 |
format:time_full | Формат времени с секундами | {timestamp|format:time_full} | 14:30:45 |
format:datetime | Полный формат | {timestamp|format:datetime} | 25.12.2024 14:30 |
format:datetime_full | Полный формат с секундами | {timestamp|format:datetime_full} | 25.12.2024 14:30:45 |
format:pg_date | Формат даты для PostgreSQL (YYYY-MM-DD) | {timestamp|format:pg_date} | 2024-12-25 |
format:pg_datetime | Формат даты и времени для PostgreSQL (YYYY-MM-DD HH:MM:SS) | {timestamp|format:pg_datetime} | 2024-12-25 14:30:45 |
seconds | Преобразование временных строк в секунды (поддержка формата: Xw Yd Zh Km Ms) | {duration|seconds} | 9000 (если duration="2h 30m") |
shift:±интервал | Сдвиг даты на интервал (PostgreSQL style: +1 day, -2 hours, +1 year 2 months). Поддержка всех форматов дат, корректная обработка месяцев/годов | {created|shift:+1 day} | 2024-12-26 (если created="2024-12-25") |
| Приведение к началу периода | |||
to_date | Приведение даты к началу дня (00:00:00), возвращает ISO формат | {created|to_date} | 2024-12-25 00:00:00 |
to_hour | Приведение даты к началу часа (минуты и секунды = 0), возвращает ISO формат | {created|to_hour} | 2024-12-25 15:00:00 (если created="2024-12-25 15:30:45") |
to_minute | Приведение даты к началу минуты (секунды = 0), возвращает ISO формат | {created|to_minute} | 2024-12-25 15:30:00 (если created="2024-12-25 15:30:45") |
to_second | Приведение даты к началу секунды (микросекунды = 0), возвращает ISO формат | {created|to_second} | 2024-12-25 15:30:45 |
to_week | Приведение даты к началу недели (понедельник 00:00:00), возвращает ISO формат | {created|to_week} | 2024-12-23 00:00:00 (если created="2024-12-25 15:30:45") |
to_month | Приведение даты к началу месяца (1 число, 00:00:00), возвращает ISO формат | {created|to_month} | 2024-12-01 00:00:00 (если created="2024-12-25 15:30:45") |
to_year | Приведение даты к началу года (1 января, 00:00:00), возвращает ISO формат | {created|to_year} | 2024-01-01 00:00:00 (если created="2024-12-25 15:30:45") |
format:currency | Форматирование валюты | {amount|format:currency} | 1000.00 ₽ |
format:percent | Форматирование процентов | {value|format:percent} | 25.5% |
format:number | Форматирование чисел | {value|format:number} | 1234.56 |
format:timestamp | Преобразование в timestamp | {date|format:timestamp} | 1703512200 |
| Условные | |||
equals:value | Проверка равенства (строковое сравнение) | {status|equals:active} | true или false |
in_list:items | Проверка вхождения в список | {role|in_list:admin,moderator} | true или false |
true | Проверка истинности (рекомендуется для boolean) | {is_active|true} | true или false |
exists | Проверка существования значения (не None и не пустая строка) | {field|exists} | true или false |
is_null | Проверка на null (None, пустая строка или строка "null") | {field|is_null} | true или false |
value:result | Возврат значения при истинности | {status|equals:active|value:Активен} | Активен или null |
| Служебные | |||
fallback:value | Значение по умолчанию | {username|fallback:Гость} | Иван или Гость |
length | Подсчет длины | {text|length} | 15 (количество символов) |
length | Подсчет длины массива | {array|length} | 3 (количество элементов) |
regex:pattern | Извлечение по regex | {text|regex:(\d+)} | 123 (первое число) |
| Асинхронные действия | |||
ready | Проверка готовности async действия | {_async_action.ai_req_1|ready} | true если завершено, false если выполняется |
not_ready | Проверка что действие еще выполняется | {_async_action.ai_req_1|not_ready} | true если выполняется, false если готово |
Примеры использования:
params:
# Простые операции
result: "{value|+100|format:currency}"
# Условная логика
status: "{user_status|equals:active|value:Активен|fallback:Неактивен}"
# Цепочка модификаторов
formatted: "{price|*0.9|format:currency|fallback:0.00 ₽}"
# Regex извлечение (используем двойные кавычки для обратных слешей!)
duration: "{event_text|regex:(\\d+)\\s*мин|fallback:0}"
# Точечная нотация
name: "{user.profile.name|fallback:Неизвестный}"
# Временные значения
duration_seconds: "{duration|seconds}"
duration_minutes: "{duration|seconds|/60}"
duration_hours: "{duration|seconds|/3600}"
formatted_time: "{duration|seconds|format:number}"
# Сдвиг дат
tomorrow: "{created|shift:+1 day}"
next_month: "{created|shift:+1 month|format:date}"
complex_shift: "{created|shift:+1 year 2 months|format:datetime}"
# Приведение к началу периода
start_of_day: "{created|to_date}"
start_of_week: "{created|to_week}"
formatted_month_start: "{created|to_month|format:date}"
# Boolean поля (различаем True/False/None)
is_active: "{is_active|equals:True|value:✅ Включен|fallback:{is_active|equals:False|value:❌ Выключен|fallback:❓ Неизвестно}}"
is_polling: "{is_polling|equals:True|value:✅ Активен|fallback:{is_polling|equals:False|value:❌ Неактивен|fallback:❓ Неизвестно}}"
# String поля (используем equals:value)
status: "{user_status|equals:active|value:Активен|fallback:Неактивен}"
# Упрощенный вариант (если None не ожидается)
is_simple: "{is_active|true|value:✅ Включен|fallback:❌ Выключен}"
# Проверка существования значений (в условиях)
# В условиях используйте: {field|exists} == True или {field|exists} == False
# Проверка на null (в условиях)
# В условиях используйте: {field|is_null} == True или {field|is_null} == FalseПример использования exists в условиях:
step:
- action: "validate"
params:
condition: "{response_value.feedback|exists} == True"
transition:
- action_result: "success"
transition_action: "continue" # Значение существует
- action_result: "failed"
transition_action: "jump_to_scenario"
transition_value: "create_feedback" # Значение не существуетВажно: boolean vs string поля
Для boolean полей (True/False):
- Рекомендуется:
{is_active|true|value:✅ Включен|fallback:❌ Выключен} - Не работает:
{is_active|equals:true|value:✅ Включен}(сравнение строк)
Для string полей:
- Используйте:
{status|equals:active|value:Активен|fallback:Неактивен} - Модификатор
trueне подходит для строковых значений
Практические примеры:
Пример 1: Персонализированное приветствие
step:
- action: "send_message"
params:
text: |
👋 Привет, {username|fallback:Гость}!
📊 Ваша статистика:
• ID: {user_id}
• Чатов: {chat_count|fallback:0}
• Последний вход: {last_login|format:datetime|fallback:Неизвестно}Пример 2: Динамическое меню с условиями
step:
- action: "send_message"
params:
text: |
🎛️ Главное меню
Статус: {user_status|equals:active|value:✅ Активен|fallback:❌ Неактивен}
Баланс: {balance|format:currency|fallback:0.00 ₽}
inline:
- [{"📊 Статистика": "stats"}, {"💰 Баланс": "balance"}]
- [{"⚙️ Настройки": "settings"}]Пример 3: Обработка ошибок с fallback
step:
- action: "send_message"
params:
text: |
❌ Ошибка обработки
Код ошибки: {error_code|fallback:UNKNOWN}
Сообщение: {error_message|fallback:Неизвестная ошибка}
Время: {timestamp|format:datetime}
inline:
- [{"🔄 Повторить": "retry"}, {"🏠 Главное меню": "main"}]Пример 4: Математические вычисления
step:
- action: "send_message"
params:
text: |
💰 Расчет стоимости
Цена: {base_price|format:currency}
Скидка: {discount_percent|fallback:0}%
Итого: {base_price|*{discount_percent|/100}|*{base_price|-1}|+{base_price}|format:currency}Пример 5: Извлечение данных из текста
step:
- action: "send_message"
params:
text: |
📝 Обработка заказа
Номер заказа: "{event_text|regex:заказ\\s*(\\d+)|fallback:Не найден}"
Сумма: "{event_text|regex:сумма\\s*(\\d+)|format:currency|fallback:0.00 ₽}"
Время доставки: "{event_text|regex:(\\d+)\\s*мин|fallback:30}" минутПример 6: Переход к сценарию (прерывание текущего)
step:
- action: "check_user_role"
params:
required_role: "admin"
transition:
- action_result: "success"
transition_action: "jump_to_scenario"
transition_value: "admin_panel" # Переход к админке, текущий сценарий прерывается
- action_result: "error"
transition_action: "continue" # Продолжить выполнение текущего сценария
# Этот шаг НЕ выполнится, если сработал jump_to_scenario
- action: "send_message"
params:
text: "Обычное меню для пользователей"Пример 7: Перемещение по шагам
step:
- action: "check_permissions"
params:
required_role: "admin"
transition:
- action_result: "success"
transition_action: "move_steps"
transition_value: 2 # Переместиться на 2 шага вперед (пропустить 1 шаг, перейти к следующему)
- action_result: "error"
transition_action: "continue" # Выполнить все шагиПример 7.1: Переход к конкретному шагу
step:
- action: "validate"
params:
condition: "$user_role == 'admin'"
transition:
- action_result: "success"
transition_action: "jump_to_step"
transition_value: 5 # Перейти к шагу с индексом 5 (шаги нумеруются с 0)
- action_result: "failed"
transition_action: "continue" # Продолжить выполнениеПример 8: Универсальный переход "any"
step:
- action: "send_message"
params:
text: "Сообщение отправлено"
transition:
- action_result: "any" # Выполняется независимо от результата
transition_action: "abort" # Всегда прерываем выполнение
- action_result: "success"
transition_action: "continue" # ❌ Этот переход НЕ выполнится из-за "any"
- action_result: "error"
transition_action: "continue" # ❌ Этот переход НЕ выполнится из-за "any"⚠️ Важно: Переход any имеет высший приоритет и блокирует выполнение всех остальных переходов. Система всегда выполняет только один переход из всех возможных.
Пример 9: Динамическая клавиатура с модификатором expand
step:
# Получаем список тенантов
- action: "get_tenants_list"
params: {}
# Строим динамическую клавиатуру из публичных тенантов
- action: "build_keyboard"
params:
items: "{public_tenant_ids}"
keyboard_type: "inline"
text_template: "Tenant $value$"
callback_template: "select_tenant_$value$"
buttons_per_row: 2
# Отправляем сообщение с динамической клавиатурой + статичные кнопки
- action: "send_message"
params:
text: "Выберите тенант:"
inline:
- "{keyboard|expand}" # Разворачиваем динамическую клавиатуру
- [{"🔙 Назад": "main_menu"}] # Статичная кнопкаКак работает expand:
- Модификатор
expandразворачивает массив массивов на один уровень только при использовании в массиве - В примере выше
{keyboard|expand}в массивеinlineразворачивает[[{...}, {...}], [{...}]]в[{...}, {...}], [{...}] - Это позволяет комбинировать динамически сгенерированные строки клавиатуры со статичными строками
- Важно:
expandработает только в контексте массива. В строке или словаре значение остается без изменений
Пример без expand (неправильно):
inline:
- "{keyboard}" # ❌ Получится [[[...]], [...]] - 3 уровня вложенности!
- [{"🔙 Назад": "main_menu"}]Пример с expand (правильно):
inline:
- "{keyboard|expand}" # ✅ Получится [[...], [...]] - 2 уровня вложенности
- [{"🔙 Назад": "main_menu"}]⚡ Асинхронные действия (Async Actions)
Назначение: Запуск долгих действий в фоне с возможностью продолжения выполнения сценария и проверки готовности результата.
Механика работы:
Асинхронные действия позволяют запускать долгие операции (например, запросы к LLM, обработку больших данных) в фоне, не блокируя выполнение сценария. Сценарий может продолжать выполнять другие действия, проверять готовность результата и ожидать завершения когда это необходимо.
Структура async действия:
step:
- step_id: 1
action_name: "ai_request"
async: true # Флаг асинхронного выполнения
action_id: "ai_req_1" # Уникальный ID для отслеживания (обязателен)
params:
prompt: "Привет!"Параметры:
async: true— флаг, что действие должно выполняться асинхронноaction_id— уникальный идентификатор действия (обязателен для async действий)action_name— имя действия для выполненияparams— параметры действия
Как работает механизм async действий:
Что происходит при запуске async действия:
Запуск действия в фоне:
- При запуске async действия оно начинает выполняться в фоне, не блокируя выполнение сценария
- Сценарий продолжает работать и может выполнять другие действия, пока async действие выполняется
- Система автоматически отслеживает состояние выполнения действия
Сохранение ссылки на действие в
_async_action:- Система сохраняет ссылку на выполняющееся действие в специальном поле
_async_action[action_id] _async_action— это системный словарь, который хранит все запущенные async действия- Ключ словаря — это
action_id(который вы указали), значение — ссылка на выполняющееся действие - Важно:
_async_actionхранится отдельно от_cache, чтобы быть доступным для проверки готовности между шагами
- Система сохраняет ссылку на выполняющееся действие в специальном поле
Автоматическое отслеживание состояния:
- Когда действие завершается (успешно или с ошибкой), система автоматически обновляет его состояние
- Обновление происходит автоматически в фоне, независимо от выполнения сценария
- Можно проверить готовность через плейсхолдер
{_async_action.action_id|ready}в любой момент
Почему используется синтаксис {_async_action.action_id|ready}:
_async_action— системное поле, которое автоматически создается при запуске async действийaction_id— это ID, который вы указали при запуске async действия (например,"ai_req_1")ready— модификатор, который проверяет, завершено ли действие (возвращаетTrueесли готово,Falseесли еще выполняется)- Это позволяет использовать проверку готовности в условиях валидации и создавать сложные сценарии с ожиданиями
Пример: После запуска нескольких async действий система автоматически создает структуру:
_async_action["ai_req_1"]— ссылка на первый AI запрос_async_action["data_proc_1"]— ссылка на обработку данных_async_action["ai_req_2"]— ссылка на второй AI запрос
Вы можете проверить готовность любого из них через плейсхолдер: {_async_action.ai_req_1|ready}
Проверка готовности:
Для проверки готовности асинхронного действия используются модификаторы ready и not_ready в плейсхолдерах. Это позволяет создавать сложные сценарии с ожиданиями через валидацию и переходы.
Модификаторы:
{_async_action.action_id|ready}— возвращаетTrueесли действие завершено,Falseесли еще выполняется{_async_action.action_id|not_ready}— возвращаетTrueесли действие еще выполняется,Falseесли готово
Как это работает:
- Плейсхолдер
{_async_action.action_id|ready}обращается к системному полю_async_actionи проверяет состояние действия с указаннымaction_id - Модификатор
readyпроверяет, завершено ли действие — возвращаетTrueесли действие готово,Falseесли еще выполняется - Результат можно использовать в условиях валидации для создания циклов ожидания и сложной логики
Пример использования в валидации:
step:
- action: "validate"
params:
condition: "{_async_action.ai_req_1|ready} == True" # Проверяем готовность
transition:
- action_result: "failed"
transition_action: "move_steps"
transition_value: -2 # Возвращаемся назад, если еще не готовоОжидание результата:
Для получения результата асинхронного действия используется действие wait_for_action:
step:
- step_id: 3
action_name: "wait_for_action"
params:
action_id: "ai_req_1"
timeout: 60 # Опциональный таймаут в секундахПараметры:
action_id— ID асинхронного действия для ожидания (обязателен)timeout— таймаут ожидания в секундах (опционально)
Результат:
wait_for_actionвозвращает результат основного действия AS IS (как будто оно выполнилось напрямую)- Если основное действие завершилось успешно → возвращается его результат с
response_data, который автоматически попадает в плоский_cache(или_cache[_namespace]если указан_namespace, с учетом подмены ключа через_response_key) - Если основное действие завершилось с ошибкой → возвращается его ошибка
- Если ошибка ожидания (таймаут, Future не найден) → возвращается ошибка
wait_for_action
Практический пример:
ai_processing:
description: "Обработка запроса к AI с показом загрузки"
trigger:
- event_type: "message"
event_text: "/ai"
step:
# Шаг 1: Запускаем AI запрос асинхронно
- action: "completion"
async: true
action_id: "ai_req_1"
params:
prompt: "{event_text|regex:/ai\\s+(.+)}"
# Шаг 2: Отправляем сообщение о загрузке
- action: "send_message"
params:
text: "⏳ Обрабатываю запрос..."
# Шаг 3: Задержка перед проверкой
- action: "sleep"
params:
seconds: 1.0 # Задержка 1 секунда между проверками
# Шаг 4: Проверяем готовность через валидацию
# Используем плейсхолдер {_async_action.ai_req_1|ready} для проверки состояния действия
- action: "validate"
params:
condition: "{_async_action.ai_req_1|ready} == True"
transition:
- action_result: "failed"
transition_action: "move_steps"
transition_value: -1 # Возвращаемся на шаг с задержкой (шаг 3), если еще не готово
# Шаг 5: Удаляем сообщение о загрузке (выполнится только когда действие готово)
- action: "delete_message"
params:
delete_message_id: "{last_message_id}"
# Шаг 6: Ожидаем результат AI (получаем данные в контекст)
- action: "wait_for_action"
params:
action_id: "ai_req_1"
timeout: 60 # Таймаут 60 секунд
# response_data попадает в плоский _cache = {response_completion: "Ответ от AI", prompt_tokens: 50, completion_tokens: 100}
# Шаг 7: Отправляем результат AI
- action: "send_message"
params:
text: "{_cache.response_completion|fallback:Ошибка обработки}"Что происходит в этом примере:
- Запускается async действие — система сохраняет ссылку на него в
_async_action["ai_req_1"] - Отправляется сообщение о загрузке
- Делается задержка 1 секунда (предотвращает 100% CPU)
- Проверяется готовность через валидацию — плейсхолдер
{_async_action.ai_req_1|ready}проверяет, завершено ли действие - Если не готово — возврат на шаг с задержкой (цикл: задержка → проверка → задержка → проверка...)
- Если готово — удаление сообщения о загрузке и получение результата
Этот пример показывает:
- Как использовать проверку готовности через валидацию для создания циклов ожидания
- Как правильно организовать цикл с задержкой для предотвращения 100% CPU
- Механизм работы плейсхолдера
{_async_action.action_id|ready}в условиях валидации
Важные особенности:
1. Результат попадает в контекст только через wait_for_action:
- Результат основного действия попадает в
_cacheсценария только после вызоваwait_for_action wait_for_actionполучает результат из завершенного действия и возвращает его как обычный результат действия- Результат автоматически сохраняется в плоский
_cache(или_cache[_namespace]если указан_namespace, с учетом подмены ключа через_response_key) - Без
wait_for_actionрезультат остается в системе и не доступен в сценарии через плейсхолдеры
2. Несколько async действий:
- Можно запустить несколько async действий одновременно с разными
action_id - Каждое действие отслеживается независимо через свой
action_idв_async_action - Можно проверять готовность каждого действия отдельно:
{_async_action.action_id_1|ready},{_async_action.action_id_2|ready} - Можно создавать сложные условия:
{_async_action.ai_1|ready} and {_async_action.data_1|ready}
3. Таймауты:
- Можно указать
timeoutвwait_for_actionдля ограничения времени ожидания - При превышении таймаута
wait_for_actionвернет ошибкуtimeout, но выполнение сценария продолжается - Основное действие продолжит выполняться в фоне даже после таймаута — ссылка на действие останется в
_async_actionи можно будет проверить готовность позже
Пример с несколькими async действиями:
parallel_processing:
description: "Параллельная обработка нескольких запросов"
trigger:
- event_type: "message"
event_text: "/process"
step:
# Запускаем несколько действий параллельно
- action: "completion"
async: true
action_id: "ai_1"
params:
prompt: "Вопрос 1"
# response_data: {"response_completion": "Ответ 1", "prompt_tokens": 50, "completion_tokens": 100}
- action: "process_data"
async: true
action_id: "data_proc_1"
params:
data: "{event_text}"
# response_data: {"processed_data": "Обработанные данные", "status": "success"}
# Ожидаем результат AI (сохраняется в _cache)
# ВАЖНО: Не используем цикл проверки готовности без задержки - это приведет к 100% CPU!
# Просто ждем результат напрямую через wait_for_action с таймаутом
- action: "wait_for_action"
params:
action_id: "ai_1"
timeout: 60 # Таймаут 60 секунд
# wait_for_action возвращает результат основного действия AS IS
# response_data из результата попадает в плоский _cache = {response_completion: "Ответ 1", prompt_tokens: 50, completion_tokens: 100}
# Ожидаем результат обработки данных (сохраняется в _cache)
- action: "wait_for_action"
params:
action_id: "data_proc_1"
timeout: 30 # Таймаут 30 секунд
_namespace: "data_processing" # Используем кастомный ключ для сохранения обоих результатов
# response_data попадает в _cache.data_processing = {processed_data: "Обработанные данные", status: "success"}
# Используем результаты (все данные доступны через _cache)
- action: "send_message"
params:
text: |
AI ответ: {_cache.response_completion|fallback:Не получен}
Токены: {_cache.completion_tokens|fallback:0}
Обработанные данные: {_cache.data_processing.processed_data|fallback:Не обработаны}
Статус: {_cache.data_processing.status|fallback:Неизвестно}ℹ️ Важно:
wait_for_actionвозвращает результат основного действия AS IS (как будто оно выполнилось напрямую)- Результат обрабатывается на уровне
scenario_engineкак обычныйresponse_data - Данные попадают в плоский
_cache(или_cache[_namespace]если указан_namespace, с учетом подмены ключа через_response_key) - Если
wait_for_actionвызывается дважды без_namespace, второй результат перезапишет первый (данные мержатся через глубокое слияние) - Решение: Используйте
_namespaceдля сохранения результатов разных async действий в разные ключи - Дополнительно: Используйте
_response_keyдля удобной подмены ключа replaceable поля в результатах async действий
Рекомендации:
- Всегда указывайте
action_idдля async действий — это единственный способ отслеживать их статус - Добавляйте таймауты в
wait_for_actionдля долгих операций — это предотвращает бесконечное ожидание - Обрабатывайте ошибки — добавляйте
transitionдля обработкиerrorиtimeoutрезультатов вwait_for_action - Используйте циклы проверки готовности с задержкой — если нужен цикл проверки готовности, обязательно добавляйте
sleepперед возвратом к проверке - Для нескольких async действий используйте
wait_for_actionнапрямую — не создавайте циклы проверки готовности для нескольких действий, просто ждите каждое черезwait_for_actionс таймаутом - Используйте
_namespaceдля сохранения результатов разных async действий в разные ключи_cache