Skip to content

Руководство по созданию сценариев

📖 Полное руководство по созданию и настройке сценариев для Telegram-ботов с поддержкой плейсхолдеров, переходов и динамической логики.

📋 Структура сценария

Организация сценариев

Организация файлов — сценарии можно организовывать в папки и подпапки для удобства. Все 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 файла:

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 — последовательность действий

Практический пример:

yaml
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 — перейти к другому сценарию (к любому сценарию тенанта)
    • execute_scenario — выполнить другой сценарий и вернуться к текущему
  • Транзишн-сценарии — сценарии без триггеров, вызываются программно из других сценариев
  • Scheduled сценарии — сценарии с полем schedule (cron выражение), запускаются автоматически по расписанию
  • Гибридные сценарии — могут иметь и trigger, и schedule одновременно (работают по событиям И по расписанию)
  • Данные события — доступны в параметрах действий (user_id, chat_id, timestamp и др.)
  • Вложения — можно пересылать файлы через attachment[0].file_id и attachment[0].type

🎯 Триггеры (trigger)

Назначение: Условия, при которых запускается сценарий.

Механика работы триггеров:

Простая форма (атрибуты):

yaml
trigger:
  - event_type: "message"
    event_text: "/start"

Преобразуется в условие:

python
$event_type == "message" and $event_text == "/start"

Сложная формаcondition):

yaml
trigger:
  - event_type: "message"
    condition: |
      $event_text == "test"
      and $user_id > 100
      or $username in ('@admin', '@moderator')

Преобразуется в условие:

python
$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, а не как строковое значение.

Решение: Всегда оборачивайте плейсхолдеры в кавычки при сравнении строк в условиях.

Примеры:

yaml
# ❌ ОШИБКА - плейсхолдер развернется в 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 — является ли чат группой
  • И другие поля события (см. Гайд по событиям)

Данные предыдущих действий:

  • $last_message_id — ID последнего отправленного сообщения
  • $response_data — любые данные, возвращенные предыдущими действиями
  • Другие поля — в зависимости от действий в сценарии

Вложенные поля:

  • $message.text — доступ к вложенным полям через точку
  • $user.profile.name — многоуровневая вложенность

Операторы в условиях:

  • == — равно
  • != — не равно
  • >, <, >=, <= — сравнение чисел
  • in, not in — вхождение в список
  • ~ — содержит подстроку
  • !~ — не содержит подстроку
  • regex — регулярное выражение
  • is_null — поле пустое (None или пустая строка)
  • not is_null — поле не пустое (существует и имеет значение)

⏰ Scheduled сценарии (запуск по расписанию)

Назначение: Автоматический запуск сценариев по расписанию через cron выражения.

Структура scheduled сценария:

yaml
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 сценариев в гайде по событиям.

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

yaml
step:
  - action: "send_message"
    params:
      text: |
        ⏰ Scheduled сценарий запущен
        
        Время: {scheduled_at|format:datetime}
        Название: {scheduled_scenario_name}
        ID: {scheduled_scenario_id}

Гибридный режим (trigger + schedule):

Сценарий может работать и по событиям, и по расписанию одновременно:

yaml
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 кнопки:

yaml
step:
  - action: "send_message"
    params:
      text: |
        👋 Привет!
        
        📋 Выберите действие:
      inline:
        - [{"📋 Меню": "main_menu"}, {"📚 Справка": "help"}]
        - [{"🌐 Сайт": "https://example.com"}, {"📞 Поддержка": "tg://resolve?domain=support"}]
        - [{"🔙 Назад": "start"}]

Reply клавиатура:

yaml
step:
  - action: "send_message"
    params:
      text: |
        👋 Привет!
        
        📋 Выберите действие:
      reply:
        - ["📋 Меню", "📚 Справка"]
        - ["🔙 Назад"]

Убрать клавиатуру:

yaml
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.

Пример: Пересылка файла из события:

yaml
step:
  - action: "send_message"
    params:
      text: "Пересылаю ваш файл обратно"
      attachment:
        - file_id: "{event_attachment[0].file_id}"
          type: "{event_attachment[0].type}"

Пример: Отправка файла по известному file_id:

yaml
step:
  - action: "send_message"
    params:
      text: "Инструкция"
      attachment:
        - file_id: "AgACAgIAAxkBAAIUJWjyLorr_d8Jwof3V_gVjVNdGHyXAAJ--zEb4vGQS-a9UiGzpkTNAQADAgADeQADNgW"
          type: "photo"

Пример: Несколько файлов:

yaml
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.

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

yaml
step:
  - action: "completion"
    params:
      prompt: "Привет, как дела?"
      # openrouter_token автоматически берется из _config.openrouter_token
      # Не нужно указывать openrouter_token явно!

Доступ через плейсхолдеры:

Конфигурация тенанта также доступна напрямую через плейсхолдеры:

yaml
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}:

yaml
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}"  # ✅ ПРАВИЛЬНО

Пример с несколькими действиями:

yaml
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. Конфликты имен между разными действиями:

yaml
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. Перезапись при повторных вызовах одного действия:

yaml
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}
  • Позволяет сохранять результаты повторных вызовов одного действия
  • Позволяет избежать конфликтов имен между разными действиями

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

yaml
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:

yaml
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
  • Работает только для действий, которые поддерживают переименование основного поля

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

yaml
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}"  # ✅ ПРАВИЛЬНО - используем кастомный ключ

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

yaml
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}

Пример с форматированием текста:

yaml
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:

yaml
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}

Пример с форматированием текста:

yaml
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:

yaml
# Сценарий 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}"  # ✅ Доступен

Когда кэш НЕ сохраняется:

❌ Между разными сценариями, которые триггерятся одним событием:

yaml
# Событие: 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}"  # ❌ НЕ доступно! Разные цепочки выполнения

❌ Между разными событиями:

yaml
# Событие 1: Пользователь отправил "/start"
step:
  - action: "set_cache"
    params:
      cache:
        step: 1

# Событие 2: Пользователь отправил "/menu" (другое событие!)
step:
  - action: "send_message"
    params:
      text: "Step: {_cache.set_cache.step}"  # ❌ НЕ доступно! Разные события

🔄 Переходы (transition)

Назначение: Управление потоком выполнения сценария.

yaml
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 + прыжок, текущий сценарий далее не выполняется)
  • execute_scenario — выполнить другой сценарий и вернуться к текущему (продолжить выполнение текущего сценария после завершения вызванного)
  • any — универсальный переход, который обрабатывается первым независимо от результата действия

Особенности переходов:

  • transition_value — обязателен для move_steps (количество шагов), jump_to_step (индекс шага, начиная с 0), jump_to_scenario и execute_scenario (название сценария или массив названий)
  • Поиск сценария — система ищет сценарий в том же tenant'е
  • Приоритет переходов — переход any обрабатывается первым, независимо от результата действия
  • Единственный переход — всегда выполняется только один переход из всех возможных
  • Блокировка других переходов — если есть переход any, остальные переходы (success, error, timeout и т.д.) не будут обработаны
  • Различие между переходами:
    • break — прерывает только текущий сценарий, другие сценарии (если есть) продолжают обработку события
    • abort — прерывает всю цепочку выполнения текущего сценария (включая все вложенные сценарии), но позволяет другим сценариям (из других триггеров) продолжить работу
    • stop — полностью прекращает обработку события во всех сценариях, возвращает управление системе
    • jump_to_scenario — прерывает выполнение текущего сценария и переходит к другому (аналогично break + прыжок)
    • execute_scenario — выполняет другой сценарий, затем возвращается и продолжает выполнение текущего сценария (кэш из вызванного сценария доступен в текущем)

🔧 Плейсхолдеры (Placeholders)

В параметрах действий (params) все значения обрабатываются плейсхолдерами: подставляются данные события, результаты предыдущих действий, доступны модификаторы и вложенный доступ.

📖 Полный синтаксис, модификаторы и примеры: Справочник по плейсхолдерам

Примеры переходов:

Пример 1: Переход к сценарию (прерывание текущего)

yaml
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: "Обычное меню для пользователей"

Пример 1.1: Выполнение сценария с возвратом

yaml
step:
  - action: "validate"
    params:
      condition: "{user_id} in {_cache.system.admins|fallback:[]}"
    transition:
      - action_result: "success"
        transition_action: "execute_scenario"
        transition_value: "add_admin_buttons"  # Выполнить сценарий и вернуться
  
  # Этот шаг ВЫПОЛНИТСЯ после завершения add_admin_buttons
  - action: "send_message"
    params:
      text: |
        Привет! {_cache.inline}
      inline:
        - "{_cache.inline|fallback:[]}"       # Кнопки из add_admin_buttons доступны
        - [{"ℹ️ Справка": "help"}]

Пример 2: Перемещение по шагам

yaml
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"        # Выполнить все шаги

Пример 2.1: Переход к конкретному шагу

yaml
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"       # Продолжить выполнение

Пример 3: Универсальный переход "any"

yaml
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 имеет высший приоритет и блокирует выполнение всех остальных переходов. Система всегда выполняет только один переход из всех возможных.

⚡ Асинхронные действия (Async Actions)

Назначение: Запуск долгих действий в фоне с возможностью продолжения выполнения сценария и проверки готовности результата.

Механика работы:

Асинхронные действия позволяют запускать долгие операции (например, запросы к LLM, обработку больших данных) в фоне, не блокируя выполнение сценария. Сценарий может продолжать выполнять другие действия, проверять готовность результата и ожидать завершения когда это необходимо.

Структура async действия:

yaml
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 действия:

  1. Запуск действия в фоне:

    • При запуске async действия оно начинает выполняться в фоне, не блокируя выполнение сценария
    • Сценарий продолжает работать и может выполнять другие действия, пока async действие выполняется
    • Система автоматически отслеживает состояние выполнения действия
  2. Сохранение ссылки на действие в _async_action:

    • Система сохраняет ссылку на выполняющееся действие в специальном поле _async_action[action_id]
    • _async_action — это системный словарь, который хранит все запущенные async действия
    • Ключ словаря — это action_id (который вы указали), значение — ссылка на выполняющееся действие
    • Важно: _async_action хранится отдельно от _cache, чтобы быть доступным для проверки готовности между шагами
  3. Автоматическое отслеживание состояния:

    • Когда действие завершается (успешно или с ошибкой), система автоматически обновляет его состояние
    • Обновление происходит автоматически в фоне, независимо от выполнения сценария
    • Можно проверить готовность через плейсхолдер {_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 если еще выполняется
  • Результат можно использовать в условиях валидации для создания циклов ожидания и сложной логики

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

yaml
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:

yaml
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

Практический пример:

yaml
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:Ошибка обработки}"

Что происходит в этом примере:

  1. Запускается async действие — система сохраняет ссылку на него в _async_action["ai_req_1"]
  2. Отправляется сообщение о загрузке
  3. Делается задержка 1 секунда (предотвращает 100% CPU)
  4. Проверяется готовность через валидацию — плейсхолдер {_async_action.ai_req_1|ready} проверяет, завершено ли действие
  5. Если не готово — возврат на шаг с задержкой (цикл: задержка → проверка → задержка → проверка...)
  6. Если готово — удаление сообщения о загрузке и получение результата

Этот пример показывает:

  • Как использовать проверку готовности через валидацию для создания циклов ожидания
  • Как правильно организовать цикл с задержкой для предотвращения 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 действиями:

yaml
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 действий

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

  1. Всегда указывайте action_id для async действий — это единственный способ отслеживать их статус
  2. Добавляйте таймауты в wait_for_action для долгих операций — это предотвращает бесконечное ожидание
  3. Обрабатывайте ошибки — добавляйте transition для обработки error и timeout результатов в wait_for_action
  4. Используйте циклы проверки готовности с задержкой — если нужен цикл проверки готовности, обязательно добавляйте sleep перед возвратом к проверке
  5. Для нескольких async действий используйте wait_for_action напрямую — не создавайте циклы проверки готовности для нескольких действий, просто ждите каждое через wait_for_action с таймаутом
  6. Используйте _namespace для сохранения результатов разных async действий в разные ключи _cache

Coreness — Create. Automate. Scale.