Logo
ХелпдескУправление знаниямиБизнесуТарифыЧто такое KCS?Блог
Русский
Личный кабинет
Автоматизации и интеграции
Назад
© 2023 - 2026 ООО «Свормика». Swarmica - Платформа управления знаниями и ресурсами техподдержки. Версия: 6.1.1

#2091: Снятие ответственного агента при смене группы заявки

Данная статья описывает инструкцию для снятия назначенного агента при смене группы у заявки.

Логика:

1. Происходит событие смены группы, появилось new value для группы.

2. Если есть Ответственный на тикете, И он находится в статусе не Оффлайн, то

    - Проверяется, является ли Ответственный участником новой группы

        - Если нет, то снимается ответственный и никто не назначается.

        - Если является участником новой группы, то ничего не делается.

3. Если нет Ответственного или он находится в статусе Оффлайн, то ничего не делается.

Инструкция

1. Авторизуйтесь в Swarmica как Администратор

2. Создайте скрипт в Настройки - Скрипты для роли Администратор

3. На открывшейся странице добавьте скрипт trigger_unset_responsible_on_group_change.py

4. Создайте Действие по событию (Скрипт) в Настройки - Действия по событию

5. На открывшейся странице выберите созданный скрипт (п.2-3) в качестве Действия

6. Добавьте условие:

Тип события = Группа заявки изменена

7. Измените статус на Включен и нажмите Сохранить

Должно выглядеть вот так:

Диагностика проблем

Отследить процесс можно в логе контейнера celeryworker перед запуском скрипта:

docker logs swarmica-celeryworker-1 -f --tail 0
Обновлена: 27 сент. 2026 г.

#1926: Как передать вложения в веб-хуке?

Симптомы

Как передать через вебхук вложения в заявке?

Решение

В данных в вебхуке прописать:

"attachments": [{% for attach in event.ticket_comment.attachments.all() %}
    {
      "public_url": "{{ attach.public_url }}",
      "mime_type": "{{ attach.mime_type }}"
    }{% if not loop.last%},{% endif %}
  {% endfor %}
  ]

Если на сервере включен __private_attachments фича флаг, то у прикрепленных вложений не будет отображаться public url, только у тех, что в тексте.

Обновлена: 13 сент. 2026 г.

#1927: Можно ли передать значение кастомного поля в веб-хуке?

Симптомы

Как передать значения кастомного поля через вебхук?

Решение

Пример для кастомного поля тикета:

  "department": "{{ event.ticket.custom_fields.get_value('8Sif6t8Khx3le6Z3') }}",

где 8Sif6t8Khx3le6Z3 - UID кастомного поля.

Пример для кастомного поля юзера:

"USER": "{{ event.ticket_comment.author.custom_fields.get_value('s1V0f1NwOpb56dRu') }}", 

где s1V0f1NwOpb56dRu - UID кастомного поля.

Обновлена: 9 сент. 2026 г.

#1928: Как передать группу в веб-хук?

Симптомы

Как передать название группы через вебхук?

Как передать UID группы через вебхук?

Решение

Название:

"group": "{{ event.ticket.group}}",

UID:

"group_uid": "{{ event.ticket.group.uid}}",
Обновлена: 13 сент. 2026 г.

#1906: Отправка отчета о переходе заявок из/в статус Ожидание ответа от клиента на почту

Инструкция

1. Авторизуйтесь в Swarmica как Администратор

2. Создайте скрипт в Настройки - Скрипты с ролью Администратор:

3. Добавьте скрипт onetime_send_csv_with_ticket_status_change_report.py

4. Добавьте следующие параметры вебформы:

Название: UID группы
Название в API: group_uid
Тип: Строка
Обязательное поле: Да
Только для чтения: Нет
Название: Дата с
Название в API: date_from
Тип: Дата
Обязательное поле: Да
Только для чтения: Нет
Название: Дата до
Название в API: date_to
Тип: Дата
Обязательное поле: Да
Только для чтения: Нет
Название: Получатель
Название в API: recipient
Тип: Строка
Обязательное поле: Да
Только для чтения: Нет

5. Нажмите Сохранить

Должно в итоге выглядеть вот так:

6. В Веб-форме справа введите UID группы, период выборки(Дата с/Дата до), почтовый адрес получателя и нажмите Отправить

Диагностика проблем

Отследить процесс можно в логе контейнера celeryworker перед запуском скрипта:

docker logs swarmica-celeryworker-1 -f --tail 0
Обновлена: 9 сент. 2026 г.

#1898: Собственные виджеты в Swarmica (веб-плагины)

Описание

Swarmica позволяет встраивать собственные приложения в интерфейс системы на экранах работы с заявкой, статьей, учетной записью пользователя, компании, лицензии и актива в виде веб-плагинов.

Виджет, веб-плагин – это код HTML, который может использовать данные из контекста экрана (например, номер заявки, значение кастомного поля учетной записи пользователя и тд) в своей внутренней логике.

Примеры задач, которые решаются с помощью веб-плагинов:

  • Отобразить ссылку на учетную запись пользователя в CRM, используя значение поля user.ext_id
  • Отобразить в iframe раздел Личного Кабинета пользователя в приложении, динамически подставив в адрес iframe ID аккаунта
  • Отобразить мини-приложение для прочтения текста статьи вслух
  • Отобразить виджет для работы с лицензионным ключом в биллинге, для получения информации о лицензии и быстрых действий по продлению этого ключа

Веб-плагин может содержать любой код HTML, в том числе и блоки с Javascript и CSS, что позволяет реализовать практически любую логику любого frontend-приложения.

Для того, чтобы подключить возможность управления веб-плагинами, необходимо обновить систему до версии 6.0.x или новее и добавить фича-флаг ui_widgets.

Настройка веб-плагина

Чтобы добавить плагин в систему, выполните следующие действия:

  1. Зайдите в Настройки > Интеграции > Кастомные виджеты
  2. Нажмите Создать и выберите нужный раздел, в котором предполагается отображать виджет
  3. Задайте настройки виджета:
    1. Название (будет отображаться в интерфейсе в качестве заголовка панели с виджетом)
    2. Доступен для (указывает роли пользователей, которые будут видеть данный виджет)
    3. Код (непосредственно код виджета. См. Использование динамических данных контекста про возможность динамической параметризации кода)
  4. Нажмите Сохранить и введите ID объекта в системе, чтобы посмотреть, как будет выглядеть виджет на странице этого объекта (например, номер заявки): Пример настройки встраиваемого виджета веб-плагина в Swarmicaа
  5. Чтобы включить отображение виджета, включите соответствующую настройку и нажмите Сохранить. Чтобы отрегулировать положение виджета на экране, зайдите в раздел Настройки > Поля и формы > Настройки экранов и переместите виджет в удобное для пользователей место: Пример настройки отображения встраиваемого виджета веб-плагина в Swarmica

ВНИМАНИЕ! Код виджета передается в браузер всех пользователей системы, которым, согласно настройкам, показывается виджет. НЕ ИСПОЛЬЗУЙТЕ перманентные токены для доступов API в коде виджета или любые авторизационные данные, которые могут позволить получить доступ к интегрируемым системам с повышенными привилегиями

Использование динамических данных контекста

При рендере (отображении) плагина для конечного пользователя системы, код проходит обработку шаблонизатора Jinja, куда в контекст передается объект, для которого данный плагин отображается. Таким образом, вы можете использовать любые атрибуты объекта, доступные в этом контексте.

Например, для того, чтобы использовать значение кастомного поля в учетной записи пользователя, для того, чтобы сгенерировать адрес для iframe, можно использовать следующий код:

<iframe src="https://my_crm/accounts/{{ user.custom_fields.get_value('CF_UID') }}" ></iframe><p></p>

Более подробно про переменные контекста можно узнать в статье "Как использовать переменные контекста".

Примеры плагинов

Ниже примеры виджетов, которые демонстрируют возможности этого механизма

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

Внешний вид:

Код:

<style>
#notes-widget {
  background: #fff8a6;
  border-radius: 10px;
  padding: 12px;
  box-shadow:
    0 3px 10px rgba(0, 0, 0, .15);
}

#notes {
  min-height: 220px;
  max-height: 300px;
  overflow-y: auto;
  outline: none;
  font-family: "Segoe Print", "Comic Sans MS", cursive;
  font-size: 15px;
  line-height: 30px;
  padding: 8px 10px;
  background-color: transparent;
  background-image:
    repeating-linear-gradient(
      to bottom,
      transparent 0,
      transparent 29px,
      rgba(0,0,0,.15) 30px
    );
  white-space: pre-wrap;
  color: black;
}

#notes:empty::before {
  content: "Напишите что-нибудь...";
  color: rgba(0,0,0,.4);
}

#notes::-webkit-scrollbar {
  display: none;
}

#status {
  margin-top: 8px;
  text-align: right;
  font-size: 12px;
  color: #666;
}
</style>

<div id="notes-widget">
  <div id="notes" contenteditable="true"></div>
  <div id="status"></div>
</div>

<script type="module">
const STORAGE_KEY = `quick-notes:{{ticket.id}}`

const notes = document.querySelector("#notes")
const status = document.querySelector("#status")

notes.innerHTML = localStorage.getItem(STORAGE_KEY) ?? ""

let saveTimer
let statusTimer

function showStatus(text) {
  status.textContent = text

  clearTimeout(statusTimer)

  statusTimer = setTimeout(() => {
    status.textContent = ""
  }, 1500)
}

function save() {
  localStorage.setItem(STORAGE_KEY, notes.innerHTML)
  showStatus("Сохранено ✓")
}

notes.addEventListener("input", () => {
  clearTimeout(saveTimer)

  saveTimer = setTimeout(() => {
    save()
  }, 500)
})

notes.addEventListener("keydown", event => {
  if ((event.ctrlKey || event.metaKey) && event.key === "s") {
    event.preventDefault()
    save()
  }
})
</script>

Для чего: отобразить адрес контрагента на карте

Внешний вид:

Код:

<div id="map-widget">
  <div class="d-flex align-items-stretch gap-2 mb-2">
    <input
      type="text"
      class="form-control form-control-sm"
      id="map-query"
      placeholder="Например: Москва"
    />
     <button
      type="button"
      class="btn btn-primary btn-sm"
      id="show-map"
    >
    GO
    </button>
  </div>

  <div
    id="map-container"
    class="border rounded overflow-hidden"
    style="height:400px"
  ></div>
</div>

<script type="module">
const input = document.querySelector("#map-query")
const button = document.querySelector("#show-map")
const container = document.querySelector("#map-container")


async function renderMap(query) {
  const res = await fetch(
    `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`
  )

  const [place] = await res.json()

  if (!place) {
    container.innerHTML = `
      <div class="d-flex align-items-center justify-content-center h-100 text-danger">
        Ничего не найдено
      </div>
    `
    return
  }

  const lat = Number(place.lat)
  const lon = Number(place.lon)

  container.innerHTML = `
    <iframe
      width="100%"
      height="100%"
      frameborder="0"
      loading="lazy"
      src="https://www.openstreetmap.org/export/embed.html?bbox=${lon-0.01},${lat-0.01},${lon+0.01},${lat+0.01}&layer=mapnik&marker=${lat},${lon}">
    </iframe>
  `
}

button.addEventListener("click", () => {
  const query = input.value.trim()

  if (!query) {
    container.innerHTML = `
      <div class="d-flex align-items-center justify-content-center h-100 text-secondary">
        Введите место
      </div>
    `
    return
  }

  renderMap(query)
})

input.addEventListener("keydown", e => {
  if (e.key === "Enter") {
    button.click()
  }
})

renderMap("Moscow")
</script>

Для чего: дать возможность редакторам проверить доступность статьи для людей с ограниченными возможностями

Внешний вид:

Пример встраиваемого виджета веб-плагина в Swarmica: прочтение статьи вслух

Код:

<div id="voice-reader-widget" style="min-height: 65px">
  <div>Статья #{{article.id}}</div>
  <br>
  <div class="d-flex gap-2">
    <button type="button" class="w-50 btn btn-primary btn-sm" id="read-article">
      🔊 Прочитать
    </button>
    <button type="button" class="w-50 btn btn-outline-secondary btn-sm" id="stop-reading">
      ⏹ Стоп
    </button>
  </div>
</div>
<script type="module">
const container = document.querySelector("#voice-reader-widget")

document.querySelector("#read-article").addEventListener("click", async () => {
  console.log("click")
  const data = await fetch("/api/articles/{{article.id}}/", {
    headers: {
      Authorization: `Bearer ${JSON.parse(localStorage.getItem("access"))}`,
    },
  })

  const article = await data.json()

  const text = (article.body.ru || article.body.en)
  .replace(/!\[.*?\]\(.*?\)/g, "")
  .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
  .replace(/[#*_`>-]/g, "")
  .replace(/\n+/g, " ")
  .trim()

  const utterance = new SpeechSynthesisUtterance(text)

  utterance.lang = article.body.ru ? "ru-RU" : "en-EN"
  utterance.rate = 1
  utterance.pitch = 1
  utterance.volume = 1

  speechSynthesis.cancel()
  speechSynthesis.speak(utterance)
})

document.querySelector("#stop-reading").addEventListener("click", () => {
  speechSynthesis.cancel()
})
</script>

Для чего: дать возможность выполнить расчеты не покидая приложение

Внешний вид:

Код:

<style>
#notes-widget {
  background: #fff8a6;
  border-radius: 10px;
  padding: 12px;
  box-shadow:
    0 3px 10px rgba(0, 0, 0, .15);
}

#notes {
  min-height: 220px;
  max-height: 300px;
  overflow-y: auto;
  outline: none;
  font-family: "Segoe Print", "Comic Sans MS", cursive;
  font-size: 15px;
  line-height: 30px;
  padding: 8px 10px;
  background-color: transparent;
  background-image:
    repeating-linear-gradient(
      to bottom,
      transparent 0,
      transparent 29px,
      rgba(0,0,0,.15) 30px
    );
  white-space: pre-wrap;
  color: black;
}

#notes:empty::before {
  content: "Напишите что-нибудь...";
  color: rgba(0,0,0,.4);
}

#notes::-webkit-scrollbar {
  display: none;
}

#status {
  margin-top: 8px;
  text-align: right;
  font-size: 12px;
  color: #666;
}
</style>

<div id="notes-widget">
  <div id="notes" contenteditable="true"></div>
  <div id="status"></div>
</div>

<script type="module">
const STORAGE_KEY = `quick-notes:{{ticket.id}}`

const notes = document.querySelector("#notes")
const status = document.querySelector("#status")

notes.innerHTML = localStorage.getItem(STORAGE_KEY) ?? ""

let saveTimer
let statusTimer

function showStatus(text) {
  status.textContent = text

  clearTimeout(statusTimer)

  statusTimer = setTimeout(() => {
    status.textContent = ""
  }, 1500)
}

function save() {
  localStorage.setItem(STORAGE_KEY, notes.innerHTML)
  showStatus("Сохранено ✓")
}

notes.addEventListener("input", () => {
  clearTimeout(saveTimer)

  saveTimer = setTimeout(() => {
    save()
  }, 500)
})

notes.addEventListener("keydown", event => {
  if ((event.ctrlKey || event.metaKey) && event.key === "s") {
    event.preventDefault()
    save()
  }
})
</script>
Обновлена: 4 сент. 2026 г.

#1887: Смена группы в закрытых и решенных заявках

Инструкция

1. Авторизуйтесь в Swarmica как Администратор

2. Создайте скрипт в Настройки - Скрипты для роли Администратор

3. Добавьте скрипт set_group_for_tickets.py

4. Настройте Параметры веб-формы:

Для списка заявок:

Название: Список заявок
Название в API: tickets
Тип: текстовое поле
Обязательное поле: вкл
Только для чтения: выкл

Выглядеть должно вот так:

Для выбора группы:

Название: Выберите группу
Название в API: group_uid
Тип: Выпадающий список
Обязательное поле: вкл
Только для чтения: выкл

# Далее необходимо ввести варианты для списка групп в формате:

Название: <ИМЯ ГРУППЫ>
Название в API: <UID ГРУППЫ>

Должно выглядеть вот так:

5. Нажмите Сохранить

В результате должно выглядеть вот так:

6. В Веб-форме справа введите список заявок через запятую, выберите группу, на которую будут назначены заявки и нажмите Отправить:

Диагностика проблем

Отследить процесс можно в логе контейнера celeryworker перед запуском скрипта:

docker logs swarmica-celeryworker-1 -f --tail 0
Обновлена: 6 сент. 2026 г.

#1883: Как настроить отправку уведомлений по нарушению SLA политик

В данной статье описан способ отслеживания SLA показателей и настройки отправки уведомлений о нарушении SLA политик.

Для этого можно использовать индексы SLA: Если индекс SLA для одного показателя больше 1, это означает, что имеет место нарушение SLA по данному показателю. Также сам индекс отображает статус показателя SLA. Например, индекс 0.5 означает, что прошло 50% времени показателя SLA политики.

Инструкция

1. Авторизуйтесь в Swarmica как Администратор

2. Создайте Действие по событию(Исходящее письмо) в Настройки - Действия по событию:

3. На открывшейся странице для Действие нажмите Создать новое, настройте почтовое уведомление и нажмите Сохранить в форме настройки уведомления.

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

нарушен SLA показатель "Время первого ответа"
Заявка: {{event.ticket}} 
Приоритет заявки: {{event.ticket.priority}}
Ответственный: {{event.ticket.assignee}}

4. Выключите опцию Игнорировать события в прошлом и установите следующие условия:

Тип события = Заявка создана
Заявка: SLA: <ИНДЕКС> ≥ 1.01

Вместо <ИНДЕКС> надо выбрать соответствующий индекс, для которого будет формироваться уведомление.

Также в условия можно добавить 1 или несколько определенных SLA политик, если необходимо слать уведомления только для определенной политики SLA. Например:

Заявка: SLA: Политика: UID = Самые важные клиенты

4. Поменяйте Статус на Включен и нажмите Сохранить

Должно выглядеть примерно так:

Обновлена: 11 сент. 2026 г.

#1864: Интеграция Swarmica с внешним Helpdesk

В любом виде интеграции настоятельно рекомендуется настроить ее так, чтобы одна система была ведущей, а вторая ведомой. Это нужно для того, чтобы избежать конфликтных ситуаций между системами и возможных неконсистенций.

Интеграция с внешними системами может быть реализована посредством вебхуков на определенные API ендпоинты.

Для корректной обе системы должны хранить метаданные сущностей внешней системы. Например, ID заявки во внешней системе. Ниже будет представлен пример интеграции Swarmica со сторонним Helpdesk.

Схема взаимодействия:

Конечный пользователь → внешний Helpdesk → API → Swarmica

Конечный пользователь работает только во внешнем Helpdesk и может вообще не знать о существовании Swarmica.

Для подобного взаимодействия необходимо настроить во внешнем Helpdesk вебхуки, которые будут посылать API запросы в Swarmica в зависимости от событий происходящих во внешнем Helpdesk.

Для всех API запросов в Swarmica используются Headers в зависимости от вида токена.

для Bearer токена:

{
  "Content-Type": "application/json",
  "Authorization": "Bearer BEARER_TOKEN"
}

для API token:

{
  "Content-Type": "application/json",
  "Authorization": "Token API_TOKEN"
}

Сценарии взаимодействия

Сценарий 1: Создание тикета в Swarmica

На стороне внешнего Helpdesk должен быть настроен вебхук, который при определенных условиях(например создание заявки во внешнем Helpdesk) шлет в Swarmica API запрос на создание заявки.

Параметры API запроса в Swarmica:

Endpoint: /api/tickets/

Method: POST

Payload:

{
  "subject": "test API ticket 11111",         # тема заявки
  "comment": "Comment 1",                     # Описание проблемы   
  "ext_id": 12345,                            # ID заявки во внешнем Helpdesk
  "requester_email": "jdoe@example.tld",      # email заявителя
  "idempotency_key": "12345678"               # ключ идемпотентности
}

При успешном запросе возвращается подобный ответ(response):

response_text='{"id":56,"subject":"test API ticket 11111","ext_id":12345,"requester":"j9m7sRnuAHFcWD9Q","assignee":null,"group":null,"status":"OPEN","until":null,"priority":"normal","replies":1,"reopens":0,"created_at":"2026-08-20T08:10:00.432419Z","resolved_at":null,"updated_at":"2026-08-20T08:10:00.547359Z","is_fcr":false,"satisfaction_score":"UNOFFERED","custom_fields":[{"uid":"1KZHEyoEPQhaD0Ii","name":{"en":"Can grant access to environment","ru":"Могу дать доступ, если надо"},"mandatory_for":[],"value":null},{"uid":"ouVl3K0SUvirO-LR","name":{"en":"link","ru":"ссылка"},"mandatory_for":[],"value":null},{"uid":"UJB6rUrOwq0Oarjt","name":{"en":"text 1","ru":"текст 1"},"mandatory_for":[],"value":null},{"uid":"Kd6ln4rmOpbVcmxd","name":{"en":"text 2","ru":"текст 2"},"mandatory_for":[],"value":null}],"platform":null,"product":null,"version":null,"edition":null,"ticketsla":{"ticket":56,"policy":"kjmiYksE1_KY0G-2","schedule":"H51qT1YSgKkq5T48","first_response_time":{"target":"08:00:00.000000","diff":"07:59:59.920475","value":"00:00:00.079525","is_running":true,"out_of_schedule":false,"breach_idx":2.761284722208046e-06},"full_resolution_time":null,"next_response_time":null,"agent_update_time":null,"support_resolution_time":null,"customer_wait_time":null,"sla_breach_idx":2.761284722208046e-06},"comment":null,"license":null,"organization":null,"ticket_session":null,"forked_from":null,"tz_offset_minutes":0,"locked":false,"time_spent":null,"time_estimate":null,"article_not_needed":false,"article_links":0,"channels":[{"uid":"4tzzvd-_6OBOggZB","channel":"jEcHl-ZdUbBhSLnD","channel_type":"EMAIL","name":"main mail channel","identity":"AftZP8cnT2IZmnxM","ext_id":"angry_dm@bk.ru","muted":false}],"source":"jEcHl-ZdUbBhSLnD","last_reply_by":"7Cf0WL7sanTeMRIF","last_reply_at":"2026-08-20T08:10:00.536283Z","issue_count":0,"last_comment_by":"7Cf0WL7sanTeMRIF","last_comment_at":"2026-08-20T08:10:00.536283Z","cc":[],"issues":[],"is_external":false,"parent_ticket":null,"locale":"ru"}'

Из этого ответа надо взять id заявки в целевой системе(Swarmica) и сохранить для дальнейших взаимодействий.

Сценарий 2: Обновление существующего тикета через API

По аналогии с предыдущим сценарием необходимо настроить вебхуки, которые будут обновлять тикет в Swarmica в зависимости от событий в исходном тикете. Для примера мы возьмем следующие события:

Добавление комментария в заявке на стороне внешнего Helpdesk

Параметры API запроса в Swarmica:

Endpoint: /api/tickets/{ID}/comments

здесь вместо ID надо вставить ID заявки в Swarmica, полученный при создании заявки(Сценарий 1)

Method: POST

Payload:

{
  "body": "added comment",
  "public": true
}

пример ответа(response):

{"id":205,"ext_id":null,"ticket":56,"author":"7Cf0WL7sanTeMRIF","is_staff":true,"created_at":"2026-08-20T08:59:28.262866Z","public":true,"pinned":false,"body":"added comment","sensitive_data":null,"attachments":[],"mentioned_users":[],"mentioned_groups":[],"is_system":false,"is_autocomment":false,"timelog_date":null,"time_spent":null,"sync_statuses":[],"quoted_comment":null,"deleted":false,"is_ai_generated":false,"ai_agent_links":[]}

Обновление тикета(статус, приоритет)

Endpoint: /api/tickets/{ID}/

здесь вместо ID надо вставить ID заявки в Swarmica, полученный при создании заявки(Сценарий 1)

Method: PATCH

Payload:

{
  "priority": "high",
  "status": "PENDING"
}

пример ответа(response):

{"id":56,"subject":"test API ticket 11111","ext_id":1,"requester":"j9m7sRnuAHFcWD9Q","assignee":null,"group":null,"status":"PENDING","until":null,"priority":"high","replies":4,"reopens":0,"created_at":"2026-08-20T08:10:00.432419Z","resolved_at":null,"updated_at":"2026-08-20T10:13:11.495034Z","is_fcr":false,"satisfaction_score":"UNOFFERED","custom_fields":[{"uid":"1KZHEyoEPQhaD0Ii","name":{"en":"Can grant access to environment","ru":"Могу дать доступ, если надо"},"mandatory_for":[],"value":null},{"uid":"ouVl3K0SUvirO-LR","name":{"en":"link","ru":"ссылка"},"mandatory_for":[],"value":null},{"uid":"UJB6rUrOwq0Oarjt","name":{"en":"text 1","ru":"текст 1"},"mandatory_for":[],"value":null},{"uid":"Kd6ln4rmOpbVcmxd","name":{"en":"text 2","ru":"текст 2"},"mandatory_for":[],"value":null},{"uid":"VJ8qi-vdMKR0Z7qc","name":{"en":"external ID","ru":"Внешний ID"},"mandatory_for":["ticket"],"value":null}],"platform":null,"product":null,"version":null,"edition":null,"ticketsla":{"ticket":56,"policy":"kjmiYksE1_KY0G-2","schedule":"H51qT1YSgKkq5T48","first_response_time":{"target":"02:00:00.000000","diff":"07:59:59.888136","value":"00:00:00.111864","is_running":false,"out_of_schedule":false,"breach_idx":0.0},"full_resolution_time":{"target":"7 00:00:00.000000","diff":null,"value":null,"is_running":false,"out_of_schedule":false,"breach_idx":0.0},"next_response_time":null,"agent_update_time":null,"support_resolution_time":null,"customer_wait_time":null,"sla_breach_idx":0.0},"comment":null,"license":null,"organization":null,"ticket_session":null,"forked_from":null,"tz_offset_minutes":0,"locked":false,"time_spent":null,"time_estimate":null,"article_not_needed":false,"article_links":0,"channels":[{"uid":"4tzzvd-_6OBOggZB","channel":"jEcHl-ZdUbBhSLnD","channel_type":"EMAIL","name":"main mail channel","identity":"AftZP8cnT2IZmnxM","ext_id":"angry_dm@bk.ru","muted":false}],"source":"jEcHl-ZdUbBhSLnD","last_reply_by":"7Cf0WL7sanTeMRIF","last_reply_at":"2026-08-20T10:06:45.406992Z","issue_count":0,"last_comment_by":"7Cf0WL7sanTeMRIF","last_comment_at":"2026-08-20T10:06:45.406992Z","cc":[],"issues":[],"is_external":false,"parent_ticket":null,"locale":"ru"}

Сценарий 3: Исходящий API запрос из Swarmica во внешнем Helpdesk при появлении комментария или изменении в

Здесь мы зависим от ендпоинта на внесение изменений для конкретной заявки внешнего Helpdesk. Такой ендпоинт для изменения конкретной заявки может:

1 - (Чаще всего) содержать ID заявки в самом ендпоинте. Например, как в Swarmica - /api/tickets/{ID}

2 - (Очень редко) не содержать ID заявки в самом ендпоинте. И ID заявки передается в payload'е API запроса.

В случае 1, в Swarmica необходимо создать действие по событию(Скрипт). В скрипте должен формироваться нужный URL ендпоинта, используя ext_id. Далее посылается соответствующий API запрос с необходимыми headers и payload.

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

Обновлена: 24 авг. 2026 г.

#1830: Добавление внутреннего комментария в заявку, переходящую в статус Решение предоставлено и содержащую во внутренних комментариях определенный текст

Инструкция

С помощью скрипта

Когда заявка переходит в статус Решение предоставлено, скрипт проверяет есть ли в комментариях к заявке определенный текст.

В зависимости от результата в заявке создается внутренний комментарий с соответствующим контентом.
Искомый текст и соответствующий контент передаются в доп. контексте скрипта.

1. Авторизуйтесь в Swarmica как администратор

2. Создайте скрипт с ролью Администратор в Настройки - Скрипты:

и на открывшейся странице добавьте скрипт trigger_autonote.py

3. Создайте Действие по событию в Настройки - Действия по событию и в качестве события выберите Скрипт:

4. На открывшейся странице выберите скрипт, добавленный в п.2 и добавьте условия:

Тип события = Статус заявки изменён
Новое значение = Решение предоставлено

5. В поле Добавить данные в контекст введите следующее:

{
  "text": "<ТЕКСТ>",
  "comment_if_text": "<СОДЕРЖИМОЕ_КОММЕНТАРИЯ_1>",
  "comment_ifnot_text": "<СОДЕРЖИМОЕ_КОММЕНТАРИЯ_2>"
}

<ТЕКСТ> - текст, искомый в существующих комментариях
<СОДЕРЖИМОЕ_КОММЕНТАРИЯ_1> - текст нового комментария, если нужный текст встречается во внутренних комментариях заявки
<СОДЕРЖИМОЕ_КОММЕНТАРИЯ_2> - текст нового комментария, если нужный текст НЕ встречается во внутренних комментариях заявки

6. Поменяйте Статус на Включен и нажмите Сохранить

Выглядеть должно так:

С помощью макроса

1. Авторизуйтесь в Swarmica как администратор

2. Создайте макрос с ролью Администратор в Настройки - Макросы

3. В режиме JSON введите Название и Данные такого вида:

{
  "public": false,
  "comment": "<p>{% if event.ticket.comments.filter(body__icontains='<ТЕКСТ>', public=False).exists() %} <СОДЕРЖИМОЕ_КОММЕНТАРИЯ_1> {% else %} <СОДЕРЖИМОЕ_КОММЕНТАРИЯ_2>{% endif %}</p>",
  "subject": "оповещение"
}

<ТЕКСТ> - текст, искомый в существующих комментариях
<СОДЕРЖИМОЕ_КОММЕНТАРИЯ_1> - текст нового комментария, если нужный текст встречается во внутренних комментариях заявки
<СОДЕРЖИМОЕ_КОММЕНТАРИЯ_2> - текст нового комментария, если нужный текст НЕ встречается во внутренних комментариях заявки

4. Создайте Действие по событию в Настройки - Действия по событию и в качестве события выберите Макрос:

5. На открывшейся странице выберите созданный макрос и добавьте условия:

Тип события = Статус заявки изменён
Новое значение = Решение предоставлено

6. Поменяйте Статус на Включен и нажмите Сохранить

Обновлена: 24 авг. 2026 г.

#1819: Как настроить кастомные pending/autosolve сообщения с учетом расписания на Компании

Инструкция

Скрипт реализует кастомную логику для тикетов в статусе Ожидает ответа клиента только для организаций с установленным Режимом работы (расписанием):

- Через заданное время (по умолчанию 1 час) после перевода тикета в статус Ожидает ответа клиента, если нет ответа от клиента и текущее время внутри бизнес-расписания компании, отправляется комментарий с предупреждением о скором закрытии.

- Ещё через заданное время (по умолчанию 1 час), если ответа нет и мы в бизнес-часах, тикет переводится в Решение предоставлено с комментарием.

- Если момент старта или напоминания приходится на нерабочее время, отсчёт начинается с начала следующих бизнес-часов.

- Поддерживается фильтрация по каналам через параметр source_channels в контексте.

Тикеты, организации которых не имеют Режима работы или если заявитель не состоит ни в одной организации, обрабатываются по дефолтной логике обработки тикетов в статусе Ожидает ответа клиента.

1. Авторизуйтесь в Swarmica как Администратор

2. Создайте новый скрипт в Настройки - Скрипты

и добавьте в него скрипт recurring_autonotify_autosolve.py

3. Создайте новое Действие по расписанию в Настройки - Действие по расписанию - Скрипт

4. На открывшейся странице выберите созданный в п.2 скрипт, выставите расписание * * * * *(каждую минуту), включите его и нажмите Сохранить:

По умолчанию, скрипт работает с заявками из всех доступных исходных каналов, время после перевода в Ожидает ответа клиента - 1 час, время после напоминания - 1 час. Однако скрипт поддерживает изменение этих параметров. Для этого можно добавить данные в контекст в виде:

{
  "reminder_hours": <HOURS>,                      # кол-во часов до напомнимания
  "close_hours": <HOURS>,                         # кол-во часов до закрытия
  "source_channels":['SOURCE1',[SOURCE2],...]     # список исходных каналов
}

Возможные значения для "source_channels":

'email'
'telegram'
'whatsapp'
'mango_office'
'widget'
'billmanager'
'vk'
'beeline_pbx'
'max'
'web'
'api'

Пример данных для контекста:

{
  "reminder_hours": 3,
  "close_hours": 3,
  "source_channels": ['email','web','widget']
}
Обновлена: 27 июл. 2026 г.

#1807: Отправка уведомлений из Swarmica во внешние системы

В Swarmica есть возможность отправлять уведомления о разных события во внешние системы с помощью исходящих вебхуков.

Инструкции для настройки уведомлений:

  • Telegram
  • Mattermost
  • Slack
Обновлена: 14 авг. 2026 г.

#1806: Как настроить отправление уведомлений о событиях Swarmica в Slack

Инструкция

Для настройки оповещений в Slack необходимо создать вебхук для соответствующего канала такого вида: https://https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX

1. Авторизуйтесь в Swarmica как Администратор.

2. Создайте вебхук в Настройки - Исходящие вебхуки, введите Название, URL вебхука, метод - POST и в поле Данные введите данные вида:

{
   "text": "<text>",                 # текст сообщения
   "channel": "<channel_name>",      # название канала
   "username": "<username>",         # от чьего имени отправлено сообщение(существование реального пользователя с таки именем необязательно)
   "icon_url": "<icon_url>"          # (опционально) URL иконки, которая будет отображаться перед именем пользователя
}

Более подробную информацию о формате данных можно найти в официальной документации Slack

Подробнее о формировании текста уведомления можно узнать здесь.

3. Создайте действие по событию в Настройки - Действия по событию - Создать - Исходящий вебхук

4. На открывшейся странице введите Название, в качестве Действия выберите созданный вебхук, выберите Тип события, установите необходимые условия, установите Статус - включен и нажмите Сохранить.

Обновлена: 14 авг. 2026 г.

#1804: Как настроить отправление уведомлений о событиях Swarmica в Mattermost

Инструкция

Для настройки оповещений в Mattermost необходимо создать вебхук для соответствующего канала такого вида: https://your.mattermost.com/hooks/xxxxxxxxxxxxxxxxxxxx

1. Авторизуйтесь в Swarmica как Администратор.

2. Создайте вебхук в Настройки - Исходящие вебхуки, введите Название, URL вебхука, метод - POST и в поле Данные введите данные вида:

{
   "text": "<text>",                 # текст сообщения
   "channel": "<channel_name>",      # название канала
   "username": "<username>",         # от чьего имени отправлено сообщение(существование реального пользователя с таки именем необязательно)
   "icon_url": "<icon_url>",         # (опционально) URL иконки, которая будет отображаться перед именем пользователя
   "props": { "card": "<data>" }     # (опционально) добавляет информационную после имени пользователя, при нажатии которой открывается карточка с доп информацией
}

Более подробную информацию о формате данных можно найти в официальной документации Mattermost

Подробнее о формировании текста уведомления можно узнать здесь.

3. Создайте действие по событию в Настройки - Действия по событию - Создать - Исходящий вебхук

4. На открывшейся странице введите Название, в качестве Действия выберите созданный вебхук, выберите Тип события, установите необходимые условия, установите Статус - включен и нажмите Сохранить.

Обновлена: 14 авг. 2026 г.

#1803: Как настроить отправление уведомлений о событиях Swarmica вTelegram

Инструкция

Для настройки оповещений в Telegram необходимо следующее:

  • ссылка на отправку сообщений через чат-бота вида https://api.telegram.org/bot<ТОКЕН>/sendMessage
  • Идентификатор чата(ChatID)

1. Авторизуйтесь в Swarmica как Администратор.

2. Создайте вебхук в Настройки - Исходящие вебхуки, введите Название, URL, метод - POST и в поле Данные введите данные вида:

{
  "text": "<message_text>",                     # шаблон текста уведомления
  "parse_mode": "markdown",                     # тип форматирования текста уведомления
  "disable_web_page_preview": true|false,       # отключение предпросмотра. Можно отключить(установить значение true), чтобы не загромождать экран чата
  "disable_notification": true|false,           # отключение уведомления на конечном устройстве(например, мобильный телефон). Для отключения необходимо установить значение true 
  "reply_to_message_id": null|"<message_id>",   # сообщение будет отправлено, как ответ на другое сообщение. Явно ставится null, чтобы избежать случайного ответа.
  "chat_id": "<ChatID>"                         # идентификатор чата
}

нажмите Сохранить

Выглядеть должно примерно вот так:

Подробнее о формировании текста уведомления можно узнать здесь.

3. На открывшейся странице в поле Заголовки добавьте данные вида:

{
  "accept": "application/json",
  "User-Agent": "<Sender_identificator>",
  "content-type": "application/json"
}

здесь "User-Agent" - это идентификатор источника сообщение. Например, можно указать "Swarmica telegram bot".

и нажмите Сохранить

В итоге выглядеть должно примерно вот так:

4. Создайте действие по событию в Настройки - Действия по событию - Создать - Исходящий вебхук

5. На открывшейся странице введите Название, в качестве Действия выберите созданный вебхук, выберите Тип события, установите необходимые условия, установите Статус - включен и нажмите Сохранить. Например, для события создание новой заявки настройки будут выглядеть так:

Обновлена: 28 сент. 2026 г.

#1805: Как составить текст уведомлений из Swarmica во внешние системы

Инструкция

В Swarmica в тексте уведомлений поддерживается использование параметров, передаваемых с соответствующим событием. Подробное описание параметров передаваемых в соответствующих событиях можно найти в этой статье.

Часто используемые параметры:

{{ event.ticket.id }} - ID заявки
{{ event.ticket.priority }} - приоритет заявки
{{ event.ticket.status }} - статус заявки
{{ event.ticket.subject }} - название заявки
{{ event.ticket.requester.name }} - имя заявителя
{{ event.ticket.requester.organization.name }} - название организации заявителя

Пример шаблона сообщения:

"{{ event.ticket.requester.name }} ({{ event.ticket.requester.organization.name if event.ticket.requester.organization else "None"}}) Создана новая заявка: [#{{event.ticket.id}}: {{ event.ticket.subject }}](https://<SWARMICA_HOSTNAME>/tickets/{{event.ticket.id}})"

Допустим пользователь Иван из организации Фирма создал тикет c ID 1234 и темой "Новая заявка", тогда:

конструкция {{ event.ticket.requester.name }} ({{ event.ticket.requester.organization.name if event.ticket.requester.organization else "None"}}) - преобразуется в маттермосте в "Иван (Фирма)", если же у заявителя нет организации, тогда - "Иван (None)"

конструкция [#{{event.ticket.id}}: {{ event.ticket.subject }}](https://<SWARMICA_HOSTNAME>/tickets/{{event.ticket.id}}) преобразуется в ссылку на созданную заявку. Тут необходимо указать корректный <SWARMICA_HOSTNAME> соответствующей установки Swarmica.

Обновлена: 15 сент. 2026 г.

#306: Как мигрировать/синхронизировать статьи между двумя установками Swarmica

В данной статье описана инструкция для миграции/синхронизации статей между двумя установками Swarmica.

Инструкция

Подготовка

1. Авторизуйтесь в исходную (откуда мигрируются статьи) Swarmica как Администратор

2. Создайте API токен в Настройки - API и интеграции

3. Авторизуйтесь в целевую (куда мигрируется) Swarmica как Администратор

4. Создайте скрипт в Настройки - Скрипты с ролью Администратор

5. Добавьте скрипт onetime_hc_migration.py и такую веб-форму:

[
  {
    "name": "host",
    "type": "string",
    "required": true,
    "displayName": "URL (хост)"
  },
  {
    "name": "token",
    "type": "string",
    "required": true,
    "displayName": "API токен"
  },
  {
    "name": "published",
    "type": "boolean",
    "required": false,
    "displayName": "Опубликованные"
  },
  {
    "name": "draft",
    "type": "boolean",
    "required": false,
    "displayName": "Черновик"
  },
  {
    "name": "archived",
    "type": "boolean",
    "required": false,
    "displayName": "Архив"
  },
  {
    "name": "approved",
    "type": "boolean",
    "required": false,
    "displayName": "Проверенные"
  },
  {
    "name": "unapproved",
    "type": "boolean",
    "required": false,
    "displayName": "Непроверенные"
  },
  {
    "name": "refresh_migrated",
    "type": "boolean",
    "required": false,
    "displayName": "Перезаписать существующие статьи"
  }
]

и нажмите Сохранить

Должно выглядеть вот так:

Запуск миграции/синхронизации

В поле URL(хост) введите URL исходной Swarmica(пример: https://source.swarmica.tld), в поле API токен введите токен из п.2, выберите статусы статей, которые будут перенесены (если не выбран ни один из статусов, будут перенесены только опубликованные статьи). Если требуется синхронизировать уже перенесенные статьи включите параметр Перезаписать существующие статьи и нажмите Отправить.

Обновлена: 20 июл. 2026 г.

#1753: Импорта организаций и клиентов из CSV файла

В данной статье описана инструкция для импорта организаций и клиентов из CSV файла.

Требования к CSV файлу:

  • в файле должны быть 3 колонки: client, email, organization
  • данные должны быть разделены символом ";"

Пример содержимого CSV файла:

client;email;organization
"James Rodriguez";"james_rodriguez_1658@example.tld";"Umbrella Co"
"Thomas Martinez";"thomas_martinez_4320@test.tld";"Hooli"
"Jennifer Lopez";"jennifer_lopez_8285@example.tld";"Massive Dynamic"
...

Инструкция

1. Подключитесь к серверу Swarmica по SSH как root пользователь.

2. Создайте резервную копию Swarmica по этой инструкции.

3. Загрузите CSV файл со списком юзеров в папку uploads в контейнере django:

docker cp <PATH-TO-CSV-FILE> swarmica-django-1:/swarmica/swarmica/uploads

где <PATH-TO-CSV-FILE> - путь к CSV файлу.

4. Авторизуйтесь в веб-интерфейсе Swarmica как Администратор

5. Создайте скрипт для импорта в Swarmica - Настройки - Скрипты для роли Администратор:

6. Загрузите скрипт import_organizations_and_clients.py, добавьте следующие Параметры веб-формы и нажмите Сохранить:

[
  {
    "name": "filename",
    "type": "string",
    "required": true,
    "displayName": "имя CSV-файла"
  }
]

Должно выглядеть вот так:

7. В поле веб-формы введите имя CSV файла, загруженного в п.2, и нажмите Отправить:

Траблшутинг

Отследить процесс можно в логе контейнера celeryworker:

docker logs swarmica-celeryworker-1 -f

Или после отработки скрипта:

docker logs swarmica-celeryworker-1 2>&1 | grep "User import"
Обновлена: 5 июл. 2026 г.

#1741: Как импортировать пользователей с ролью Сотрудник из CSV файла

В данной статье представлена инструкция для импорта пользователей с ролью Сотрудник из .csv файла.

Скрипт требователен к формату CSV файла: обязательно должны быть колонки 'Сотрудник' (имя сотрудника) и 'E-Mail' (email адрес), разделенные символом ";"

Пример содержимого:

Сотрудник;E-Mail
John Doe;john.doe@test.tld
Иван Иванов;ivan.ivanov@test.tld
Thomas Smith;thomas.smith@test.tld
...

Инструкция

1. Подключитесь к серверу Swarmica по SSH как root пользователь.

2. Создайте резервную копию Swarmica по этой инструкции.

3. Загрузите CSV файл со списком юзеров в папку uploads в контейнере django:

docker cp <PATH-TO-CSV-FILE> swarmica-django-1:/swarmica/swarmica/uploads

где <PATH-TO-CSV-FILE> - путь к CSV файлу.

4. Авторизуйтесь в веб-интерфейсе Swarmica как Администратор

5. Создайте скрипт для импорта в Swarmica - Настройки - Скрипты для роли Администратор:

6. Загрузите скрипт import_users.py, добавьте следующие Параметры веб-формы и нажмите Сохранить:

[
  {
    "name": "filename",
    "type": "string",
    "required": true,
    "displayName": "имя CSV файла"
  }
]

Должно выглядеть вот так:

7. В веб-форме введите имя CSV файла, загруженного в п.2, и нажмите Отправить:

Траблшутинг

Отследить процесс можно в логе контейнера celeryworker:

docker logs swarmica-celeryworker-1 -f

Или после отработки скрипта

docker logs swarmica-celeryworker-1 2>&1 | grep "User import"
Обновлена: 30 июн. 2026 г.

#967: Как выделить заявки цветом в списке

Вопрос

Можно ли подсвечивать заявки цветом в списке, в зависимости от приоритета?

Ответ

Расцветку можно добавить через CSS стили в Настройках - Управление брендами.

Например, можно вот так по приоритетам подсветить:

Чтобы это сделать, в Стили для темы в бренде необходимо добавить:

tr:has(td span.ticket-priority-urgent) > * {
    background-color: rgba(255, 0, 0, 0.2) !important;
}

tr:has(td span.ticket-priority-high) > * {
    background-color: rgba(0, 255, 255, 0.2) !important;
}

tr:has(td span.ticket-priority-normal) > * {
    background-color: rgba(0, 255, 0, 0.2) !important;
}

Чтобы подсветка тикетов работала, в фильтре обязательно нужно вывести колонку Приоритет.

Обновлена: 18 июн. 2026 г.

#1661: Миграция статей базы знаний в Swarmica

Импорт статей в Swarmica. В данной статье описано как импортировать статьи в виде .md файлов в Swarmica.

Инструкция

Рекомендация: Назвать .md файл как тема статьи. Например, тема статьи - "Как создать заявку", тогда рекомендуемое имя файла "Как создать заявку.md"

1. Подключитесь к серверу c Swarmica по SSH как root

2. Создайте папку /root/swarmica/old_articles:

mkdir /root/swarmica/old_articles

3. Загрузите статьи(.md файлы) в созданную папку /root/swarmica/old_articles

4. Создайте резервную копию файла /root/swarmica/docker-compose.yml:

cp /root/swarmica/docker-compose.yml{,.backup}

5. Примонтируйте папку со статьями в контейнер django. Для этого добавьте в файл /root/swarmica/docker-compose.yml в секции контейнера django следующую строку:

<...>
  django: &django
    image: reg.gl.swd.im/swarmica/backend:${SW_BACKEND_VERSION}
    depends_on:
      - postgres
      - redis
    volumes:
      - swarmica_ai_assistant:/swarmica/swarmica/ai_assistant/articles/:z
      - swarmica_runtime:/swarmica/swarmica/runtime_scripts/:z
      - swarmica_static:/swarmica/swarmica/static:z
      - swarmica_ugc:/swarmica/swarmica/attachments:z
      - swarmica_uploads:/swarmica/swarmica/uploads/:z
      - /root/swarmica/old_articles/:/swarmica/swarmica/old_articles/:z   # <------ Эту строку
<...>

И перезапустите Swarmica:

docker compose down; docker compose up -d

6. Авторизуйтесь в Swarmica UI как Администратор.

7. Создайте новый скрипт в Swarmica - Настройки - Скрипты с ролью Администратор

8. В настройках скрипта добавьте этот скрипт и следующее в Параметры веб-формы и нажмите Сохранить:

[
  {
    "name": "author_uid",
    "type": "string",
    "required": false,
    "displayName": "UID автора"
  },
  {
    "name": "category_name",
    "type": "string",
    "required": true,
    "displayName": "Имя раздела статьи"
  }
]

Должно выглядеть так:

9. В веб-форме скрипта укажите UID пользователя, который будет назначен автором статей и Имя раздела статей и нажмите Отправить:

10. На сервере Swarmica в файле /root/swarmica/docker-compose.yml удалите строку, добавленную в шаге 5 и перезапустите Swarmica:

docker compose down; docker compose up -d
Обновлена: 14 июн. 2026 г.

#1646: Уведомление клиентов при создании заявки в нерабочее время

Симптомы

Требуется создать автосообщения (отбивки) для отправки клиентам если они заводят заявку в нерабочее время.

Решение

  1. Перейти в Настройки - Действия по событию, нажать Создать , Макрос
  2. Выбрать Тип события = Событие заявки
  3. Добавить значение Тип события = Заявка создана
  4. Добавить Заявка: Создана и выбрать [..]
  5. Создать Макрос справа и написать нужный комментарий.
  6. Не забыть дать названия действию и макросу в соответствующих полях
  7. Включить и сохранить

Обновлена: 1 июн. 2026 г.

#664: Как закрыть пустые заявки из чат-виджета?

Симптомы

Создаются пустые заявки из чатов.

Решение

  1. Создайте новое действие по событию с типом Скрипт в Настройках - Действия по событию.
  2. Отключите опцию Игнорировать события в прошлом.
  3. Укажите следующие условия:
    • Тип события: Заявка создана
    • Заявка: Статус = Новая
    • Количество ответов = 0
    • Исходный канал: Тип канала = Виджет
    • Заявка: Создана: укажите время, через которое заявка будет закрываться, если клиент ничего не написал
  4. В дополнительный контекст добавьте переменную comment_body с текстом ответного сообщения:
    {
      "comment_body": "Здравствуйте! <br/>Вы не написали сообщение, поэтому заявка будет закрыта."
    }
    

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

  5. Прикрепите скрипт из вложения в качестве действия: close_empty_chat.py
  6. Включите триггер.

Причина

Тикет создается по клику Начать чат, даже если клиент ничего не пишет.

Обновлена: 31 авг. 2026 г.

#1614: Как использовать переменные(контекст) для создания скриптов для действия по событию в Swarmica

Вопрос

Как использовать переменные(контекст) для создания скриптов для действия по событию

Ответ

В Swarmica для Действия по событию в качестве действия можно использовать Python скрипты.

Подробнее о написании скриптов можно узнать в этой статье.

В зависимости от сценария в скрипт передаются данные о конкретном событии и дополнительный контекст(если требуется).

Для действий по событию используется 5 видов событий:

ArticleEvent - События Статьи
CustomFieldEvent - События Кастомного поля
KCSEvent - События KCS
TicketEvent - События Заявки
UserEvent - События Пользователя

Для каждого из событий передаются определенные параметры, описанные в этой статье.

Когда срабатывает триггер, передаются данные в таком виде:

{'data': {'event_id': <ID>, 'event': <TicketEvent: TicketEvent object (<ID>)>[, <ДОП_КОНТЕКСТ>]}

Обработка данных События

Для использования данных о Cобытии необходимо обрабатывать Объект event, передаваемый в данных. Можно вывести этот Объект в отдельную переменную. Например:

event = data.get('event', None)

или

event = <EVENT_NAME>.objects.filter(id=data['event_id']).first()

где <EVENT_NAME> - соответствующее событие.

Например, сценарий должен срабатывать по Событию Заявки(TicketEvent). В таком случае код будет следующим:

event = TicketEvent.objects.filter(id=data['event_id']).first()

Далее полученную переменную event можно использовать для обработки параметров передаваемых соответствующим Событием. Структура переменной для параметров выглядит так:

event.Параметр[.Параметр[.<...>]]

Если Параметр является Объектом, содержащим собственные Параметры, то конечная структура переменной усложняется.

Например, используется сценарий по Событию TicketEvent. Событие TicketEvent - само по себе является сущностью(объектом), включающим следующие параметры:

ПараметрЧто делаетЗначения
responsibleпередает Исполнителя СобытияОбъект user
dateпередает дату События
old_valueпередает старые значения События
new_valueпередает новые значения События
ticketпередает Заявку, в которой произошло СобытиеОбъект ticket
ticket_commentПередает комментарии Заявки, в которой произошло СобытиеОбъект ticketcomment

И нам нужно произвести действие с самой Заявкой. В параметрах, как видно, передается Объект ticket. Соответственно надо обрабатывать этот самый объект и переменная будет выглядеть так:

event.ticket

Для удобства можно определить объект в отдельную переменную:

ticket = event.ticket

Например, нам нужно вывести информацию о Заявке(id) и дате её создания(created_at) в лог, тогда код может выглядеть так:

ticket = event.ticket
logger.info(f"Заявка #{ticket.id} создана {ticket.created_at}")

или так:

logger.info(f"Заявка #{event.ticket.id} создана {event.ticket.created_at}")

Обработка дополнительного контекста

Подробнее о контексте можно узнать в этой статье

Также для действия по событию можно добавить дополнительный контекст в Настройки - Действия по Событию - Действие в поле Добавить данные в контекст. Данные добавляются в формате json.

При срабатывании События доп контекст передается вместе с данными о событии. Например в доп. контекст добавлено следующее:

{
  "id": "1234"
}

Тогда данные передаваемые в скрипт будут выглядеть так:

{'data': {'event_id': <ID>, 'event': <TicketEvent: TicketEvent object (<ID>)>, 'id':'1234'}

Для работы с данными доп контекста в самом скрипте можно вывести их в отдельную переменную:

id = data.get('id', None)

Пример

Допустим есть сценарий: при повышении приоритета Заявки до "Критичный" необходимо поместить Заявку в отдельную группу.

В условиях для события устанавливаем:

Тип события=Приоритет заявки изменён
Заявка: Приоритет= Срочный

В данном сценарии используется TicketEvent(События Заявки). Для передачи в скрипт Группы, на которую нужно назначить Заявку мы будет использовать дополнительный контекст.

В дополнительный контекст добавляем UID новой группы:

{
  "new_group_uid": "ABCMWvoee35feOeA"
}

Скрипт будет выглядеть так:

# Подгружаем необходимые модули для запуска скрипта и для работы с задействованными объектами
from runtime.runners import BaseRunner             
from core.models import TicketEvent, Group, Ticket  

# модуль для логирования
import logging                                     
logger = logging.getLogger(__name__)

class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        logger.info(kwargs)
        # Помещаем передаваемые в Событии данные в переменную data
        data = kwargs.get('data', {})

        # Помещаем объект TicketEvent в переменную event c помощью data['event_id'] 
        event = TicketEvent.objects.filter(id=data['event_id']).first()

        # Если Событие не передалось пишем об этом в лог
        if not event:
            logger.error(f"No event with {data['event_id']} found, exiting")
            return

        # Помещаем объект ticket из event в переменную ticket
        ticket=event.ticket

        # Далее удостоверяемся, что заявка передана в Событии
        if ticket:                                       
            ticket_id = event.ticket.id                  # помещаем id Заявки из События в переменную ticket_id
            group_uid = data.get('new_group_uid',None)   # помещаем UID группы из дополнительного контекста в переменную group_uid
            new_group = Group.objects.get(uid=group_uid) # помещаем Объект нужной группы в переменную, используя переменную group_uid
            logger.info(f"Заявка #{ticket_id} должна быть назначена на группу с UID {group_uid}")

            # Далее проверяем, что Заявка находится в другой группе и назначаем на нужную группу
            if ticket.group.uid != group_uid:
                ticket.group = new_group
                ticket.save()
                logger.info(f"ticket group changed")                
            else: # если заявка изначально была уже назначена на нужну группу пишем об этом в лог и ничего не делаем
                logger.info(f"ticket is in proper group already")               
                
        else: # если заявки нет в Событии, то пишем об этом в лог
            logger.warning(f"Ticket is not found in event {data['event_id']},exiting")
            return
Обновлена: 24 июл. 2026 г.

#1605: Описание объектов и параметров для триггеров

В Swarmica есть следующие события, используемые в качестве триггеров:

ArticleEvent - События Статьи
CustomFieldEvent - События Кастомного поля
KCSEvent - События KCS
TicketEvent - События Заявки
UserEvent - События Пользователя

Для каждого из событий передаются определенные параметры, которые можно использовать в скриптах. Ниже описаны эти параметры:

ArticleEvent

ПараметрЧто делаетЗначения
eventпередает какое Событие произошло со Статьёй"0" - "Unknown"
"1" - "Article created"
"2" - "Article user segment added"
"3" - "Article published"
"4" - "Article unpublished"
"5" - "Article deleted"
"6" - "Article undeleted"
"7" - "Article author changed"
"8" - "Article flagged"
"9" - "Article unflagged"
"10" - "Agent commented on article"
"11" - "Customer commented on article"
"12" - "Agent comment changed"
"13" - "Agent comment deleted"
"14" - "Customer comment deleted"
"15" - "Article revision added"
"16" - "Article status changed"
"17" - "Article user segment deleted"
responsibleпередает Исполнителя СобытияОбъект user
dateпередает дату События
old_valueпередает старые значения параметров События
new_valueпередает новые значения параметров События
articleпередает Статью, с которой произошло СобытиеОбъект article

CustomFieldEvent

ПараметрЧто делаетЗначения
responsibleпередает Исполнителя СобытияОбъект user
dateпередает дату События, связанного с Кастомным полем
old_valueпередает старые значения параметров События
new_valueпередает новые значения параметров События
fieldпередает Кастомное поле, с которым произошло СобытиеОбъект customfield
content_objectпередает Объект, для которого произошло СобытиеОбъект ticket|article|user

KCSEvent

ПараметрЧто делаетЗначения
typeпередает тип События"1" - "Capture"
"2" - "Link"
"3" - "Flag"
"4" - "Publish"
"5" - "Unflag"
"6" - "Unpublish"
"7" - "Unlink"
responsibleпередает Исполнителя СобытияОбъект user
dateпередает дату и время События
ticketпередает Заявку, связанную с СобытиемОбъект ticket
articleпередает Статью, связанную с СобытиемОбъект article

TicketEvent

ПараметрЧто делаетЗначения
eventпередает Событие, связанное с Заявкой"0" - "Неизвестно"
"1" - "Статья создана из заявки"
"2" - "Статья добавлена к заявке"
"20" - "Убрана связь со статьёй"
"3" - "Статья помечена к исправлению"
"4" - "Статус заявки изменён"
"5" - "Агент прокомментировал заявку"
"6" - "Клиент прокомментировал заявку"
"7" - "Заявка оценена"
"8" - "Приоритет заявки изменён"
"9" - "Заявка создана"
"10" - "Ответственный изменён"
"11" - "Заявитель изменён"
"12" - "Группа заявки изменена"
"13" - "Платформа заявки изменена"
"14" - "Продукт заявки изменён"
"15" - "Версия заявки изменена"
"16" - "Редакция заявки изменена"
"17" - "Задача добавлена к заявке"
"18" - "Задача удалена из заявки"
"19" - "Пользовательское поле изменено"
"21" - "Навык добавлен"
"22" - "Навык удалён"
"23" - "Связанная заявка добавлена"
"24" - "Связанная заявка удалена"
"25" - "Заявка объединена"
"28" - "Агент добавил внутренний комментарий"
"26" - "Чат-сессия по заявке начата"
"27" - "Чат-сессия по заявке завершена"
"29" - "Отправлено уведомление об ожидании"
"30" - "Политика SLA заявки изменена"
"31" - "Лицензия заявки изменена"
"33" - "Заявка разветвлена"
"34" - "Задача закрыта"
"35" - "Задача открыта"
"36" - "Применено правило автоназначения"
"37" - "Внутренний комментарий изменён"
"38" - "Признак «Статья не нужна» изменён"
"39" - "Система Swarmica добавила автоматический ответ"
"40" - "Оценка времени изменена"
"41" - "Задача удалена"
"42" - "Актив добавлен"
"43" - "Актив удалён"
"44" - "Актив обновлён"
"45" - "Тема изменена"
"46" - "Публичный комментарий изменён"
"47" - "Комментарий удалён"
"48" - "Пользователь добавлен в копию"
"49" - "Пользователь удалён из копии"
"50" - "Наблюдатель добавлен"
"51" - "Наблюдатель удалён"
"52" - "Внешняя заявка добавлена"
"53" - "Публичный комментарий внешней заявки добавлен"
"54" - "Заявка заблокирована"
"55" - "Заявка разблокирована"
responsibleпередает Исполнителя СобытияОбъект user
dateпередает дату События
old_valueпередает старые значения параметров События
new_valueпередает новые значения События
ticketпередает Заявку, в которой произошло СобытиеОбъект ticket
ticket_commentПередает комментарии Заявки, в которой произошло СобытиеОбъект ticketcomment

UserEvent

ПараметрЧто делаетЗначения
eventПередает События, связанные с Пользователем"0" - "Role changed"
"1" - "Password changed"
"2" - "Identity added"
"3" - "Organization changed"
"4" - "Schedule changed"
"5" - "Name changed"
"6" - "Status changed"
"7" - "Merged into"
"8" - "Merged from"
"9" - "User created"
"10" - "User logged into system"
"11" - "Notification enabled"
"12" - "Notification disabled"
"13" - "Identity deleted"
"14" - "2FA method added"
"15" - "2FA method removed"
"16" - "2FA enabled"
"17" - "2FA disabled"
"18" - "2FA bypassed (transport unavailable)"
"19" - "2FA backup code used"
responsibleпередает Исполнителя СобытияОбъект user
dateпередает дату События
old_valueпередает старые Значения События
new_valueпередает старые Значения События
userпередает Пользователя, с которым произошло СобытиеОбъект user

Когда происходит Событие, передается данные об этом Событии. Эта информация содержит информацию о Событии и Объекты, связанные с этим самым Событием. Ниже представлены списки параметров Объектов.

article

Статьи базы знаний

ПараметрЧто делаетЗначения
idпередаёт ID Статьи
statusпередаёт статус СтатьиDRAFT - черновик
UNAPPROVED - Непроверенная
APPROVED - Проверенная
PUBLISHED - Опубликованная
ARCHIVED - Архивная
published_atпередает дату публикации Статьи
visibilityпередает области доступа к СтатьеALL - для всех
AUTHORIZED - для авторизованных пользователей
SEGMENTS - для отдельных сегментов пользователей
segmentsпередает Сегменты пользователейОбъект usersegment
internalпередает является ли Статья внутренней(boolean)TRUE|FALSE
categoryпередает Категорию СтатьиОбъект articlecategory
publisherпередает Редактора СтатьиОбъект user
authorпередает Автора СтатьиОбъект user
is_flaggedпередает помечена ли Статья к исправлению(boolean)TRUE|FALSE
updated_atпередает дату последнего редактирования Статьи
created_atпередает дату создания Статьи
t_linksпередает количество связанных со Статьёй Заявок

articlecategory

Разделы статей базы знаний

ПараметрЧто делаетЗначения
uidпередает UID Раздела статей
publicпередает является ли Раздел публичным(boolean)TRUE|FALSE
orderпередает значение порядкового номера Раздела
created_atпередает дату создания Раздела
updated_atпередает дату последнего редактирования Раздела

articlecomment

Комментарии к Статье

ПараметрЧто делаетЗначения
idпередает id Комментария к статье
authorпередает Автора КомментарияОбъект user
publicпередает является ли Комментарий публичным(boolean)TRUE|FALSE
is_staffпередает является ли Автор комментария Сотрудником(boolean)TRUE|FALSE
created_atпередает дату создания Комментария
updated_atпередает дату последнего редактирования Комментария

articlerevision

Версия статьи базы знаний

ПараметрЧто делаетЗначения
uidпередает UID Версии статьи
authorпередает Автора Версии статьиОбъект user
latestпередает является ли Версия статьи самой свежей(boolean)TRUE|FALSE
restored_fromпередает из какой Версии Статья была восстановленаОбъект articlerevision

channel

Каналы связи

ПараметрЧто делаетЗначения
uidПередает UID Канала связи
channel_typeПередает тип Канала связи"EMAIL"
"TELEGRAM"
"WHATSAPP"
"MANGO_OFFICE"
"WIDGET"
"BILLMANAGER"
"VK"
"BEELINE_PBX"
"MAX"

customfield

Кастомные поля

ПараметрЧто делаетЗначения
uidпередает UID Кастомного поля
field_typeпередает тип Кастомного поля"TEXT" - текст
"TEXTAREA" - текстовое поле
"NUMBER" - целое число
"DECIMAL" - число с плавающей точкой
"CHECKBOX" - чекбокс
"DROPDOWN" - меню
"DATE" - дата
"DATETIME" - дата и время
"REGEX" - регулярное выражение
"OBJECT" - связанная сущность
"BADGE" - бейдж
"LINK" - ссылка

edition

Редакции Продукта

ПараметрЧто делаетЗначения
uidпередает UID Версии

group

Группа пользователей

ПараметрЧто делаетЗначения
uidпередает UID Группы пользователей

issue

Внешние задачи из сторонних систем(Gitlab, Jira и т.п.)

ПараметрЧто делаетЗначения
uidпередает UID Внешней задачи

license

Клиентские ключи

ПараметрЧто делаетЗначения
uidпередает UID Клиентского ключа

organization

Компании(клиенты)

ПараметрЧто делаетЗначения
uidпередает UID Компании

platform

Платформы Продукта

ПараметрЧто делаетЗначения
uidпередает UID Платформы

product

Продукты

ПараметрЧто делаетЗначения
event.product.uidпередает UID Продукта

schedule

Расписания

ПараметрЧто делаетЗначения
event.schedule.uidпередает UID Расписания
event.schedule.nameпередает название Расписания
event.schedule.defaultпередает является ли данное Расписание дефолтным(по умолчанию)(boolean)TRUE|FALSE

skill

Навыки сотрудников

ПараметрЧто делаетЗначения
uidпередает UID навыка

slapolicy

Политики SLA

ПараметрЧто делаетЗначения
uidпередает UID Политики SLA

ticket

Заявки

ПараметрЧто делаетЗначения
idпередает ID Заявки
statusпередает статус Заявки"NEW"
"OPEN"
"PENDING"
"HOLD"
"SOLVED"
"CLOSED"
"DELETED"
"MERGED"
groupпередает Группу, на которую назначена ЗаявкаОбъект group
priorityпередает приоритет Заявки"low"
"normal"
"high"
"urgent"
assigneeпередает Ответственного за ЗаявкуОбъект user
requesterпередает Заявителя ЗаявкиОбъект user
ccпередает список добавленных в CCОбъект user
watchersпередает список Наблюдателей ЗаявкиОбъект user
organizationпередает Компанию ЗаявителяОбъект organization
ticketslaпередает Политику SLA ЗаявкиОбъект ticketsla
licenseпередает пользовательский Ключ ЗаявителяОбъект license
lockedпередает заблокирована ли Заявка(boolean)TRUE|FALSE
tz_offset_minutesпередает часовой пояс Заявки
localeпередает язык(локаль) Заявки
created_atпередает дату создания Заявки
updated_atпередает дату последнего обновления Заявки
resolved_atпередает дату решения Заявки
untilпередает дату, до которой Заявка будет в статусе Ожидание
repliesпередает количество ответов в Заявке
reopensпередает количество переоткрытий Заявки
productпередает Продукт, по которому заведена ЗаявкиОбъект product
versionпередает версию ПродуктаОбъект version
editionпередает редакцию ПродуктаОбъект edition
platformпередает платформу ПродуктаОбъект platform
skillsпередает Навыки, связанные с ЗаявкойОбъект skill
is_fcrпередает решена ли Заявка первым ответом Сотрудника(boolean)TRUE|FALSE
satisfaction_scoreпередает оценку удовлетворенности Заявителя"UNOFFERED"
"OFFERED"
"GOOD"
"BAD"
"NEUTRAL"
time_estimateпередает Оценку трудозатрат Заявки
time_spentпередает фактические Трудозатраты на Заявку
article_not_neededпередает значение параметра "Статья не нужна"(boolean)TRUE|FALSE
article_linksпередает количество связанных с Заявкой Статей
channelsпередает Каналы связи ЗаявкиОбъект ticketchannel
subjectпередает тему Заявки
sourceпередает по какому Каналу связи была получена ЗаявкаОбъект channel
last_replyпередает последний ответ Агента в ЗаявкеОбъект ticketcomment
last_commentпередает последний Комментарий в ЗаявкеОбъект ticketcomment
issue_countпередает количество связанных Внешних задач
linked_issuesпередает связанные Внешние задачи
is_externalпередает является ли Заявка Внешней(boolean)TRUE|FALSE
parent_ticketпередает родительскую заявку

ticketchannel

Каналы связи Заявки

ПараметрЧто делаетЗначения
channelПередает Каналы связи ЗаявкиОбъект channel

ticketcomment

Комментарии Заявки

ПараметрЧто делаетЗначения
idпередает ID комментария
authorпередает Автора комментарияОбъект user
created_atпередает дату создания Комментария
bodyпередает содержимое комментария

ticketissue

Внешние задачи, связанные с Заявкой

ПараметрЧто делаетЗначения
issueпередает Внешнюю задачу, связанную с Заявкой
authorпередает Сотрудника, связавшего/создавшего Внешнюю задачу
ticketпередает саму Заявку

ticketsla

Политики SLA Заявки

ПараметрЧто делаетЗначения
sla_breach_idxпередает индекс превышения SLA
first_response_time_breach_idxпередает индекс превышения времени первого ответа в Заявке
full_resolution_time_breach_idxпередает индекс превышения полного времени решения Заявки
next_response_time_breach_idxпередает индекс превышения времени между ответами
agent_update_time_breach_idxпередает индекс превышения времени ответа Сотрудника
support_resolution_time_breach_idxпередает индекс превышения времени решения в поддержке
customer_wait_time_breach_idxпередает индекс превышения времени решения без ожидания

user

Пользователи

ПараметрЧто делаетЗначения
uidпередает UID пользователя
emailпередает email Пользователя
nameпередает имя Пользователя
roleпередает Роль Пользователя"ADMIN"
"MANAGER"
"AGENT"
"INTERNAL_USER"
"CUSTOMER_ADMIN"
"CUSTOMER"
"BLOCKED"
is_robotпередает является ли Пользователь Роботом(boolean)TRUE|FALSE
is_systemпередает является ли Пользователь системным пользователем(boolean)TRUE|FALSE
is_employeeпередает является ли Пользователь Сотрудником(boolean)TRUE|FALSE
kcs_roleпередает KCS роль Пользователя"NOT SET"
"CANDIDATE"
"CONTRIBUTOR"
"PUBLISHER"
started_atПередает дату начала работы Пользователя
dismissed_atПередает дату увольнения Пользователя
created_atпередает дату создания Пользователя
updated_atпередает дату редактирования Пользователя
aqi_evaluatorпередает является ли Пользователь проверяющим Качество КонтентаTRUE|FALSE
lai_evaluatorпередает является ли Пользователь проверяющим Точности СвязанностиTRUE|FALSE
qa_evaluatorпередает является ли Пользователь проверяющим Качество СервисаTRUE|FALSE
statusпередает Статус Пользователя"OFFLINE"
"ONLINE"
"IN_SESSION"
"WRAPUP"
"AWAY"
"BUSY"
email_formatпередает формат email для Пользователя"HTML"
"TEXT"
timezoneпередает таймзону Пользователя
sourceпередает источник создания Пользователя"system"
"email"
"invite"
"api"
"token"
"script"
"okdesk"
"crm"
"telegram"
"whatsapp"
"vk"
"phone"
"max"
scheduleпередает Расписание ПользователяОбъект schedule
skillsпередает Навыки ПользователяОбъект skill
organizationпередает Компанию ПользователяОбъект organization
group_membershipsпередает список Групп, в которых состоит ПользовательОбъект group

useridentity

Учетные записи Пользователя

ПараметрЧто делаетЗначения
uidпередает UID Пользователя
userпередает Пользователя, связанного с Учетной ЗаписьюОбъект user
ext_idпередает внешний ID Учетной записи
sourceпередает источник Учетной записи"EMAIL"
"TELEGRAM"
"WHATSAPP"
"VK"
"PHONE"
"ANONYMOUS",
"MAX"

usersegment

сегменты Пользователей

ПараметрЧто делаетЗначения
uidпередает UID Сегмента Пользователей
nameпередает название Сегмента Пользователей
created_atпередает дату создания Сегмента Пользователей
updated_atпередает дату последнего редактирования Сегмента Пользователей

version

версии Продукта

ПараметрЧто делаетЗначения
event.version.uidпередает UID версии продукта
Обновлена: 9 июл. 2026 г.

#1449: Почтовое уведомление если тикет находится без ответа

Симптомы

Есть потребность уведомлять агентов и менеджеров, если тикет остается без ответа определенное количество часов и SLA нарушаются

Решение

Задачу можно решить через написания скрипта-плагина , который можно настроить запускаться по определенному расписанию, и который будет проверять тикеты, находящиеся без ответа (например 48 часов с момента последнего ответа) в определенной группе, и отправлять письмо членам этой группы.

Пример скрипта.

Для использования функции скриптов-плагинов требуется лицензия тарифа уровня Премиум

Обновлена: 18 мая 2026 г.

#1287: Как отследить время кастомного события?

Симптомы

Хочется отслеживать время наступления какого либо кастомного события. Например перевод в другую группу или выставление, какого-либо кастомного филда.

Решение

Рассмотрим на примере события смена группы. Для решения проблемы можно использовать макрос, который будет проставлять время в кастомное поле при смене группы.

  1. Создать в настройках новое кастомное поле типа «Дата и время».
  2. Скопировать UID нового поля из URL.
  3. В настройках зайти в «Макросы» и создать новый, где:
  • ID объекта: {{ event.ticket.id }};
  • Данные:
{
  "custom_fields": [
    {
      "uid": "UID_кастомного_поля",
      "value": "{{ event.date }}"
    }
  ]
}
  1. В настройках — «Действия по событию» создать новое, где в качестве действия выбрать макрос из шага 2 и включить триггер.
Обновлена: 18 мая 2026 г.

#1351: Что такое "контекст" в действиях по событию и расписанию?

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

Контекст – это данные и переменные, которые доступны автоматическому действию (макросу, исходяшему письму, исходящему вебхуку или скрипту) в момент выполнения.

Структура контекста

Контекст - это хранилище ключ-значение, в котором могут храниться любые сериализуемые в JSON значения.

Триггер по событию, по умолчанию, передает в ключе event объект события, которое и запустило данный триггер.

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

При необходимости, вы можете добавить переменные в контекст, чтобы использовать их по умолчанию так же в формате ключ-значение в формате JSON:

{
  "my_custom_variable": {
    "foo": "bar"
  }
}

И затем обращаться к этим значениям в шаблонах автоматизаций, например, в вебхуке или емейле:

<p>Hello, {{ my_custom_variable.foo }}</p>
Обновлена: 18 мая 2026 г.

#1333: Как посчитать Handover Rate — коэффициент передач, индекс «футбола» заявки?

Симптомы

Как посчитать Handover Rate — коэффициент передач, индекс «футбола» заявки?

Решение

Вариант 1

В лоб можно смотреть сколько в среднем было Ответственных в тикете, что будет грубым индикатором того сколько раз передавался тикет. Ограничение, что когда Агент А <> Агент Б между собой передают. Но это достаточно выколотый кейс, поэтому на больших данных среднего количества Ответственных должно вполне хватать.

Вариант 2

1 - Создать в Настройках >Кастомные поля новое кастомное поля типа Целое число:

И скопировать его UID из URLа.

2 - В Настройках зайти в Макросы и создать новый, где
ID Объекта: {{ event.ticket.id }}
Данные:

{
  "custom_fields": [
    {
      "uid": "EY1pPecadgSmZnuC",
      "value": "{% set count = event.ticket.custom_fields.get_value('СЮДА ПИСАТЬ ID КАСТОМ ВИЛДА') %}{% if count is not none %}{{ count + 1 }}{% else %}0{% endif %}"
    }
  ]
}

3 - В Настройках - Действия по событию создать новое, где в качестве события выбрать Assignee Changed действия выбрать макрос из шага 2

Дальше уже по этому кастомному полю можно делать фильтры и сводные отчеты.

При большом количестве данных работу макроса возможно придется донастраивать, поэтому для более стабильного и универсального применения рекомендуется вариант 3 со скриптом. См. ниже

Вариант 3

Вместо макроса использовать скрипт.

1 - Создать в Настройках >Кастомные поля новое кастомное поля типа Целое число:

И скопировать его UID из URLа.

2 - Создать новый скрипт в Настройки> Скрипты и загрузите туда пример скрипта

3 - В Настройках - Действия по событию создать новое, где в качестве события выбрать Assignee Changed действия выбрать скрипт из шага 2

4 - В поле "Добавить данные в контекст" прописать следующее:

{
   "cf_uid": "УИД КАСТОМФИЛДА ЧИСЛО КУДА ПИСАТЬ ЗНАЧЕНИЕ"
}

Сохранить и запустить действие.

Дальше уже по этому кастомному полю можно делать фильтры и сводные отчеты.

Обновлена: 18 мая 2026 г.

#1282: Синтаксис crontab для периодических задач

Действия по расписанию и периодическое перераспределение заявок используют crontab синтаксис для формирования расписания периодических задач. В данной статье дается краткая справка о формате crontab.

Формат записи

* * * * * 
- - - - -
| | | | |
| | | | ----- день недели (0—7) (воскресенье = 0 или 7)
| | | ------- месяц (1—12)
| | --------- день месяца (1—31)
| ----------- час (0—23)
------------- минута (0—59)

Проверить синтаксис или подобрать подходящий можно так же с помощью специализированных сервисов, например crontab.guru.

Примеры

Запускать действие раз в три минуты:

*/3 * * * *

Запускать действие каждый час в XX:13:

13 * * * *

Запускать каждое воскресенье в 7:00:

0 7 * * 0

Запускать в 6:30, 12:30 и 18:30 каждый день:

30 6,12,18 * * *
Обновлена: 18 мая 2026 г.

#1274: Настройка автоматических сообщений в веб-чате

Симптомы

Для веб-чата требуется настроить автоматические сообщения:

  1. При старте чата
  2. Если чат запущен в нерабочее время

Решение

Возможна настройка через скрипты и макросы.

  1. Возьмите пример скрипта и отредактируйте в нем тексты сообщений
  2. Убедитесь, что у вас есть созданное расписание, назначенное на компанию/группу или политику SLA
  3. Перейдите в Настройки > Скрипты и нажмите кнопку Создать. Выберите роль Администратор и введите название скрипта.
  4. В открывшемся окне в поле скрипт загрузите отредактированный скрипт из пункта 1. Нажмите Сохранить.
  5. Перейдите в Настройки > Действие по событию и нажмите Создать
  6. Заполните название, Тип события = TicketEvent, , Тип действия = Скрипт и в Действие выберите созданный скрипт. Нажмите Сохранить
  7. В открывшемся окне в поле Выполнять, если событие удовлетворяет условиям вставить
{
  "event": 9,
  "ticket__status__in": [
    "NEW",
    "OPEN"
  ],
  "ticket__source__channel_type": "WIDGET"
}

Сохранить событие и в списке действий нажмите Запустить выбранное действие

Обновлена: 18 мая 2026 г.

#1157: Написание плагинов для автоматизации действий на Python

ВЫ ИСПОЛЬЗУЕТЕ МЕХАНИЗМ СКРИПТОВ, КОТОРЫЙ ЗАПУСКАЕТСЯ ОТ ИМЕНИ ROOT БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ,
НА ВАШЕ УСМОТРЕНИЕ. ЛИЦЕНЗИАР ЯВНО ОТКАЗЫВАЕТСЯ ОТ ВСЕХ ГАРАНТИЙ, ЯВНЫХ, ПОДРАЗУМЕВАЕМЫХ ИЛИ
УСТАНОВЛЕННЫХ ЗАКОНОДАТЕЛЬСТВОМ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ, ПОДРАЗУМЕВАЕМЫМИ ГАРАНТИЯМИ ТОВАРНОЙ
ПРИГОДНОСТИ, ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННОЙ ЦЕЛИ И НЕНАРУШЕНИЯ ПРАВ.

В ПОЛНОЙ МЕРЕ, РАЗРЕШЕННОЙ ЗАКОНОМ, ЛИЦЕНЗИАР НЕ НЕСЕТ ОТВЕТСТВЕННОСТИ ЗА КОСВЕННЫЕ, СЛУЧАЙНЫЕ,
СПЕЦИАЛЬНЫЕ, ПОСЛЕДУЮЩИЕ УБЫТКИ ИЛИ УБЫТКИ В ВИДЕ УПУЩЕННОЙ ВЫГОДЫ ИЛИ ВЫРУЧКИ, ВОЗНИКШИЕ ВСЛЕДСТВИЕ ИЛИ СВЯЗАННЫЕ С ДАННЫМ СОГЛАШЕНИЕМ ИЛИ ВАШИМ ИСПОЛЬЗОВАНИЕМ МЕХАНИЗМА.

Содержание

Часть I. Основы

1. Что можно программировать и какой механизм выбрать.

2. Как устроена среда выполнения.

3. Минимальный скрипт и класс Runner.

4. Параметры, типы данных и дополнительный контекст.

5. Первый рабочий скрипт: загрузка, веб-форма и проверка результата.

Часть II. Триггеры запуска

6. Запуск по событию и работа с event.

7. Запуск по расписанию и рабочее время.

Часть III. Работа с данными

8. Поиск объектов и связанные данные.

9. Заявки, комментарии и чаты.

10. Пользовательские поля и счётчик назначений.

11. Повторные запуски и параллельная обработка.

12. Массовые операции, транзакции и предварительный просмотр.

Часть IV. Действия и интеграции

13. Уведомления, шаблоны и выбор получателей.

14. Отчёты CSV и Excel.

15. Внешние API и постраничная загрузка.

16. Импорт пользователей, компаний и статей.

Часть V. Эксплуатация

17. Диагностика, производительность и эксплуатация.

18. Каталог примеров и маршрут дальнейшего изучения.


Движок скриптов позволяет вам писать и использовать произвольный код Python для запуска автоматизаций, инициируемых событием, по расписанию или даже вручную пользователем через веб-форму.

Загружать скрипты и настраивать их запуск через Веб-форму, Действия по событию, Действия по расписанию могут администраторы системы в разделе Настройки > Автоматизация и маршрутизация > Скрипты

Рекомендуется изучить подробную инструкцию по написанию плагинов и начать с одного из примеров, указанных в статье

Скрипты-плагины позволяют вашей команде создавать серверную логику под процессы поддержки: проверять условия, работать с заявками и связанными объектами, формировать отчёты, выполнять массовые операции и обмениваться данными с внешними системами.

Вы пишете Python-файл, подключаете его в Swarmica и выбираете способ запуска: событие, расписание или веб-форму. Через параметры один и тот же скрипт можно использовать для разных сценариев.

Это руководство рассчитано на разработчика или администратора с базовыми знаниями Python. Оно объясняет механизм и приёмы программирования на основе приложенных к базе знаний примеров.

Описанные ниже сценарии и примеры могут быть неполными и не охватывать всех возможностей скриптов-плагинов Swarmica. Если вы не нашли нужный метод, не понимаете, как реализовать свой сценарий, или не смогли написать работающий скрипт, обратитесь в техподдержку Swarmica со своим вопросом. Опишите, что хотите сделать, укажите версию системы и приложите свой код и текст ошибки, если они есть. Мы поможем разобраться с доступными механизмами и способами реализации.

Версии и проверка. Материал подготовлен по документации и файлам, доступным 17 сентября 2026 года. Внутренние модели и методы могут различаться между версиями. Учебные примеры нужно проверить в тестовой установке вашей версии Swarmica перед рабочим запуском. Метка VERSION внутри файла обозначает версию самого скрипта, а не минимальную версию Swarmica.

Обозначения в руководстве

  • Django — стандартный фреймворк. Такие методы и модули описаны в официальной документации Django.
  • Swarmica — внутренние модели, хелперы и сервисные объекты продукта. Их сигнатуры могут меняться между версиями; сверяйтесь с тестовой установкой и рабочими примерами из статей.
  • Если не указано иное, перед использованием любого фрагмента получайте все переменные (ticket, group, schedule, channel) до строки с примером.

Часть I. Основы

1. Что можно программировать

Собственное расширение полезно, когда нужное поведение требует кода: дополнительных проверок, вычислений, работы с несколькими объектами или обработки ответа внешней системы.

ЗадачаПодход
Изменить поля, формы или простое правилоШтатные настройки и макросы
Выполнить свои проверки и действия с даннымиСерверный Python-скрипт
Добавить панель, форму или мини-приложение на рабочий экранВеб-виджет на HTML / JavaScript / CSS
Передавать данные во внешнюю системуВеб-хук или вызов API из Python
Изменить внутреннюю архитектуру продуктаОтдельное обсуждение с Swarmica

Например, Python-скрипт может подсчитать назначения заявки и записать результат в поле, найти обращения без ответа, сформировать файл отчёта или импортировать данные из CSV.

Веб-виджеты — отдельный механизм расширения интерфейса. Их настройка описана в статье «Собственные виджеты в Swarmica». В этом руководстве рассматривается серверный Python-код.

2. Среда выполнения

Скрипт выполняется внутри установки Swarmica и может импортировать модели и внутренние функции продукта. Поэтому для доступа к локальным данным не обязательно выполнять HTTP-запросы к своему же API: примеры используют Django ORM.

Что нужноМодульТип
Базовый класс запускаruntime.runners.BaseRunnerSwarmica
Заявки, комментарии, группы, статьи, каналыcore.modelsSwarmica
Пользователи и их идентификаторыswarmica_auth.modelsSwarmica
Пользовательские поля и значенияcustom_field.modelsSwarmica
SLA-объектыsla.modelsSwarmica
Константы статусов, событий и ролейswarmica.defsSwarmica
Отправка emailcore.tasks.notifications.send_emailSwarmica
Параметры установкиdjango.conf.settingsDjango
Транзакцииdjango.db.transactionDjango
ContentTypedjango.contrib.contenttypes.modelsDjango
QuerySet-методы (filter, select_related, iterator, values_list, …)Django ORMDjango

Это карта импортов из исследованных файлов, а не обещание неизменного программного интерфейса. Описание многих объектов доступно в статье «Объекты и параметры для триггеров».

Права и ресурсы

В документации указано, что скрипты выполняются от root внутри контейнера, имеют доступ к основной БД и общим файловым разделам и используют ресурсы совместно со Swarmica. Изоляция контейнера не защищает данные продукта от ошибочного кода.

Скрипты выполняются внутри контейнера, поэтому у них нет доступа к аппаратному узлу / виртуальной машине, на которой установлена Swarmica. Однако сценарии имеют доступ к общим разделам диска и основной базе данных, используемой Swarmica, поэтому будьте осторожны при выполнении потенциально опасных операций, таких как удаление и изменение любых данных или файлов.

Кроме того, ресурсы памяти/процессора используются совместно со всем инсталляцией Swarmica, поэтому настоятельно рекомендуется минимизировать импорт библиотек и модулей до только необходимого минимального набора функций. Например, вместо импорта всего модуля import datetime импортируйте только необходимую вам функцию: from datetime import timedelta.

Назначение роли для доступа к веб-форме не превращает прямой запрос ORM в пользовательский запрос с автоматической проверкой видимости всех объектов. Если форму могут запускать разные сотрудники, ограничения на доступные данные и действия нужно учитывать в реализации.

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

Условия использования механизма приведены в документации по Python-плагинам. Для скриптов-плагинов в опубликованных примерах указан тариф Премиум или выше; актуальные условия смотрите в тарифах.

Библиотеки

Документация перечисляет, в частности, requests, openpyxl, pandas, bs4, а также средства работы с JSON, CSV, датами и регулярными выражениями. Импортируйте только то, что нужно. Наличие произвольной сторонней библиотеки и её версию проверяйте в своей установке.

Ряд предустановленных библиотек доступен для импорта и использования в ваших скриптах. Смотрите список доступных методов в документации соответствующей библиотеки:

import re               # Для работы с регулярными выражениями
import os               # Для работы с вещами ОС, такими как пути к файлам и т.д.
import bs4              # Для парсинга HTML, XML и т.д. с помощью BeautifulSoup
import csv              # Для чтения и записи CSV
import json             # Для парсинга JSON
import time             # Для работы с текущим временем, например, генерации временных меток
import pandas           # Для работы с наборами данных
import urllib           # Для парсинга и записи параметров URL, urlencode и т.д.
import dateutil         # Для работы с датами, например, парсинга даты из строки
import datetime         # Для работы с объектами datetime, например, вычисления длительностей и т.д.
import openpyxl         # Для чтения и записи Excel
import requests         # Для работы с удаленными API и вебхуками

Обзор движка

Есть несколько способов запуска сценария:

[Триггер события] ---> +context --\
                                  \
[Расписание] --------> +context----\
                                    >----> Runner.run(kwargs={data: **params, **context})
[CLI] -------------> +params-------/
                                  /
[API/WEB] ---------> +params-----/

Итак, в основном, если сценарий времени выполнения запускается Триггером События или по Расписанию, то данные события и пользовательские контекстные данные, настроенные в соответствующем триггере / расписании, передаются в параметры сценария.

Допустим, сценарий запускается событием UserEvent, тогда контекст можно разобрать следующим образом:

class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        data = kwargs.get('data', {})
        if not data or 'event_id' not in data:
            logger.error(f"No event_id passed, exiting")
            return
        event = UserEvent.objects.filter(id=data['event_id']).first()
        if not event:
            logger.error(f"No event with {data['event_id']} found, exiting")
            return

Если мы настроим Триггер События на передачу дополнительного контекста в скрипт, например, списка email-адресов для получения оповещения при изменении учетной записи пользователя, то он также будет передан в скрипт.

При условии, что пользовательский контекст Триггера События выглядит так:

{
  "recipients": [
    "user1@domain.tld",
    "user2@domain.tld"
  ]
}

Теперь вы можете получить доступ к значению переменной внутри скрипта следующим образом:

data = kwargs.get('data', None)

if data:
    recipients = data.get('recipients', None)

Доступ к объектам Swarmica

Обычно вам нужно манипулировать моделями и наборами запросов (querysets) для объектов Swarmica в ваших скриптах. Поскольку Swarmica использует Django в качестве фреймворка, большинство методов моделей и наборов запросов также работают здесь.

Смотрите полную справку по методам моделей Django и наборов запросов на официальном сайте документации:

Querysets

Models

Большинство объектов, которые вам могут понадобиться, находятся в пакете core, за исключением объектов User - они находятся в пакете swarmica_auth.

Также вам, как правило, потребуется импортировать общие константы, используемые Swarmica, следующим образом:


from swarmica import defs

print(defs.USER_ROLES_INTERNAL)

3. Минимальный скрипт и класс Runner

Любой скрипт — это Python-файл, в котором объявлен класс Runner, унаследованный от runtime.runners.BaseRunner, и метод run. Всё, что делает скрипт, размещается в run или в вызываемых из него функциях.

Минимальный самостоятельный файл:

import logging

from runtime.runners import BaseRunner

logger = logging.getLogger(__name__)


class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        data = kwargs.get("data") or {}
        logger.info(f"Скрипт запущен; ключи параметров: {list(data)}")

Этот скрипт ничего не читает из БД и не меняет данные. Он просто пишет в лог ключи, которые пришли в data. Его удобно использовать как «Hello, world» и как первую проверку подключения: запустить, посмотреть в лог, убедиться, что параметры доходят.

Движок вызывает метод run. Основную работу выполняйте внутри него или в функциях, которые он вызывает. Не размещайте запросы к БД, сетевые вызовы или изменения данных на уровне импорта модуля: они могут произойти ещё до обработки конкретного запуска — в том числе при первом подключении файла.

Что доступно из Runner

В исследованных примерах используются:

ОбъектТипНазначение
kwargs["data"]SwarmicaПараметры конкретного запуска
self.script.nameSwarmicaНазвание скрипта для диагностики
self.script.uidSwarmicaUID подключённого скрипта
self.service_userSwarmicaСервисный пользователь, от имени которого выполняется скрипт.
User.objects.get_swarmica_service()SwarmicaСервисный пользователь Swarmica (используется в примерах)

Полный набор методов BaseRunner здесь не описывается. Наличие перечисленных атрибутов подтверждается приложенными файлами, но это не полный контракт среды выполнения.

Обработка ошибок

Обрабатывайте ожидаемые ошибки параметров явными проверками. Для неожиданных исключений используйте logger.exception(...), чтобы сохранить трассировку. Не скрывайте все ошибки общим except: pass.

Что происходит с исключением, вышедшим из run, зависит от способа запуска и версии. Не полагайтесь на то, что среда автоматически повторит запуск или покажет пользователю понятное сообщение: проверяйте на тестовой установке.

4. Параметры и контекст

Три источника данных

ЗапускЧто использовать в коде
Через веб-формуЗначения полей по их name
По событиюДанные события и дополнительный контекст
По расписаниюЗаданные параметры; нужные объекты скрипт выбирает самостоятельно

Например, дополнительный контекст:

{
  "group_uid": "GROUP_UID",
  "hours": 48,
  "recipients": ["support-lead@example.com"],
  "apply": false
}

Python-фрагмент внутри run:

data = kwargs.get("data") or {}
group_uid = data.get("group_uid")
hours = data.get("hours", 48)
recipients = data.get("recipients", [])

Это позволяет менять группу, порог и получателей без редактирования кода. Переменная не становится параметром автоматически: скрипт должен явно прочитать её и использовать.

Веб-форма с датами

Вы также можете настроить скрипт так, чтобы у него была собственная веб-форма, которая может принимать, проверять и передавать параметры в скрипт.

Допустим, мы пишем скрипт, который генерирует пользовательский отчет в Excel и отправляет его списку email-получателей. И мы хотим, чтобы пользователи передавали даты начала и окончания отчета и список получателей, разделенный запятыми.

В веб-интерфейсе Swarmica в настройках скрипта мы настраиваем следующие параметры веб-формы:

[
  {
    "name": "start_date",
    "type": "datetime",
    "readonly": false,
    "displayName": "Report start date"
  },
  {
    "name": "end_date",
    "type": "datetime",
    "readonly": false,
    "displayName": "Report end date"
  },
  {
    "name": "recipients",
    "type": "text",
    "displayName": "Comma-separated recipients"
  }
]

Теперь мы можем получить доступ к этим параметрам в скрипта следующим образом:

params = kwargs.get('data', {})
if params:
    if 'recipients' in params:
        recipients = params.get('recipients')
        if recipients:
            recipients = [x.strip() for x in recipients.split(',') if '@' in x]
        if not recipients:
            logger.info("Input error: empty/invalid list of 'recipients' specified")
            return
        
        start_date = params.get('start_date', None)
        if isinstance(start_date, str) and start_date.strip():
            report_start_date = parse_datetime(start_date)
            
        end_date = params.get('end_date', None)
        if isinstance(end_date, str) and end_date.strip():
            report_end_date = parse_datetime(end_date)

        if not report_end_date:
            report_end_date = now().replace(hour=23, minute=30, second=0, microsecond=0).replace(tzinfo=default_time_zone)
        if not report_start_date:
            report_start_date = report_end_date - timedelta(days=1)

Различайте JSON и Python

В настройках контекст задаётся корректным JSON: true, false, null, двойные кавычки, без комментариев. В Python используются True, False, None.

Не вставляйте в JSON записи вроде ['email'], <HOURS> или # комментарий. Заполнитель GROUP_UID в примерах замените на настоящий UID.

Дополнительный контекст должен содержать JSON-совместимые значения. При этом среда может добавить к данным запуска объект модели event: весь итоговый словарь data уже не обязан быть сериализуемым в JSON.

Проверяйте типы

Текстовое поле передаёт строку. Дата может потребовать преобразования. В одном из примеров ручного запуска предусмотрена обработка одиночного значения, пришедшего списком или кортежем. Нормализуйте такие формы только для конкретных скалярных параметров; список получателей или каналов должен остаться списком.

Для логического параметра нельзя использовать bool("false"): непустая строка даст True. В примерах есть хелпер Swarmica common.utils.str2bool. Можно также использовать собственную строгую функцию:

def parse_apply(value):
    if value is None:
        return False
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        normalized = value.strip().lower()
        if normalized in {"true", "1", "yes"}:
            return True
        if normalized in {"false", "0", "no", ""}:
            return False
    raise ValueError("apply должен быть логическим значением")

Неизвестное значение не должно случайно разрешать изменение данных.

Имена параметров

Не используйте для собственных настроек event и event_id: эти имена заняты данными события. Для своих параметров выбирайте понятные названия: group_uid, cf_uid, reminder_hours, recipient.

Общее объяснение контекста: статья 1351. Специфика Python-событий: статья 1614.

5. Первый рабочий скрипт: загрузка, веб-форма и проверка результата

Это следующий шаг после «Hello, world»: читаем данные из БД, ничего не меняем, пишем результат в лог.

Задача. Найти группу по UID и показать ограниченный список новых и открытых заявок.
Триггер. Веб-форма.
Параметры. group_uid, limit (1–100).
Ожидаемый результат. Записи вида ticket_id=… status=… assignee_uid=… в логе и итоговая строка показано=N.

Шаг 1. Создайте Python-файл

Сохраните следующий самостоятельный пример как read_group_tickets.py.

import logging

from core.models import Group, Ticket
from runtime.runners import BaseRunner
from swarmica import defs

logger = logging.getLogger(__name__)
VERSION = "1.0.0"


class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        LOG_PREFIX = f"{self.script.name} ({self.script.uid})"
        data = kwargs.get("data") or {}
        if not isinstance(data, dict):
            logger.error(f"{LOG_PREFIX}: data должен быть словарём")
            return

        group_uid = data.get("group_uid")
        if not isinstance(group_uid, str) or not group_uid.strip():
            logger.error(f"{LOG_PREFIX}: укажите group_uid")
            return

        raw_limit = data.get("limit", 10)
        if isinstance(raw_limit, bool) or not isinstance(raw_limit, (int, str)):
            logger.error(f"{LOG_PREFIX}: limit должен быть целым числом")
            return
        try:
            limit = int(raw_limit)
        except ValueError:
            logger.error(f"{LOG_PREFIX}: limit должен быть целым числом")
            return
        if not 1 <= limit <= 100:
            logger.error(f"{LOG_PREFIX}: limit должен быть от 1 до 100")
            return

        group = Group.objects.filter(uid=group_uid.strip()).first()
        if group is None:
            logger.warning(f"{LOG_PREFIX}: группа не найдена")
            return

        tickets = Ticket.objects.filter(
            group=group,
            status__in=[defs.TICKET_STATUSES_NEW, defs.TICKET_STATUSES_OPEN],
        ).select_related("assignee").order_by("id")[:limit]

        shown = 0
        for ticket in tickets:
            assignee_uid = ticket.assignee.uid if ticket.assignee else None
            logger.info(
                f"{LOG_PREFIX}: ticket_id={ticket.id} status={ticket.status} "
                f"assignee_uid={assignee_uid}"
            )
            shown += 1
        logger.info(f"{LOG_PREFIX}: версия={VERSION} показано={shown}")

Что здесь важного для дальнейшего:

  • LOG_PREFIX = f"{self.script.name} ({self.script.uid})" — этот приём стоит копировать во все скрипты. Название скрипта в каждой строке лога облегчает поиск при нескольких активированных расширениях.
  • Валидация параметров — это не паранойя. Текстовое поле формы всегда приходит строкой, а int("") или bool могут сломать логику.
  • Если заявок нет, скрипт штатно завершится с показано=0. Это нормально, не ошибка.
  • select_related("assignee") избавляет от N+1 запросов: без него каждый ticket.assignee — отдельный SQL.

Шаг 2. Подключите файл

  1. Войдите в Swarmica как администратор.
  2. Откройте Настройки → Скрипты.
  3. Создайте скрипт и задайте название.
  4. Для первого теста разрешите запуск только администратору.
  5. Загрузите read_group_tickets.py в поле скрипта.
  6. Сохраните настройки.

Шаг 3. Добавьте веб-форму

В параметры веб-формы внесите:

[
  {
    "name": "group_uid",
    "type": "string",
    "required": true,
    "readonly": false,
    "displayName": "UID группы"
  },
  {
    "name": "limit",
    "type": "string",
    "required": true,
    "readonly": false,
    "displayName": "Сколько заявок показать: от 1 до 100"
  }
]

name — имя, которое код читает через data.get(...). displayName — подпись для пользователя. В этом примере лимит поступает из текстового поля и преобразуется в число самим скриптом.

В исследованных формах также встречаются типы boolean, date, datetime и dropdown. Для выпадающего списка нужны варианты choices; итоговое значение должно соответствовать тому, что ожидает код. Точный набор типов и формат вариантов сверяйте с интерфейсом своей версии и примером смены группы.

Шаг 4. Запустите и посмотрите лог

Во вкладке веб-формы укажите реальный UID группы и лимит 10, затем отправьте форму.

Результат выполнения скрипта можно посмотреть в логе celeryworker:

docker logs swarmica-celeryworker-1 -f --tail 100

В интерфейсе скрипта доступны средства тестирования, состав которых зависит от версии. Запись return в коде сама по себе не означает, что веб-форма покажет пользователю результат: в этом учебном примере результат находится в логе.


Часть II. Триггеры запуска

6. Запуск по событию и работа с event

Объект события и код события

Слово event используется на двух уровнях:

ВыражениеЧто означает
data["event"]Объект события, например TicketEvent
event.eventКод типа события, например смена ответственного
data["event_id"]ID записи события
event.ticketЗаявка, связанная с событием
event.responsibleПользователь, инициировавший событие
event.dateВремя события

События разных сущностей используют разные модели: TicketEvent, UserEvent, ArticleEvent, CustomFieldEvent, KCSEvent. Нельзя искать событие пользователя в таблице событий заявок только потому, что совпал ID.

Имена полей внутри событий могут отличаться между моделями. В одних событиях инициатор — responsible, в других — user или actor. Перед обращением к полю сверяйтесь с моделью вашей версии (см. приём с _meta.get_fields() в разделе 17).

Получение события

Следующий самостоятельный файл записывает факт смены ответственного в лог. Он поддерживает переданный объект и загрузку по ID.

import logging

from core.models import TicketEvent
from runtime.runners import BaseRunner
from swarmica import defs

logger = logging.getLogger(__name__)


class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        LOG_PREFIX = f"{self.script.name} ({self.script.uid})"
        data = kwargs.get("data") or {}
        event = data.get("event")
        if event is not None and not isinstance(event, TicketEvent):
            logger.error(f"{LOG_PREFIX}: ожидался объект TicketEvent")
            return
        if event is None:
            event_id = data.get("event_id")
            if event_id is None:
                logger.warning(f"{LOG_PREFIX}: не переданы event и event_id")
                return
            if isinstance(event_id, bool) or not isinstance(event_id, (int, str)):
                logger.warning(f"{LOG_PREFIX}: некорректный event_id")
                return
            try:
                event_id = int(event_id)
            except ValueError:
                logger.warning(f"{LOG_PREFIX}: некорректный event_id")
                return
            event = TicketEvent.objects.filter(id=event_id).first()
        if event is None:
            logger.warning(f"{LOG_PREFIX}: событие не найдено")
            return
        if event.event != defs.TICKET_EVENTS_ASSIGNEE_CHANGE:
            logger.info(f"{LOG_PREFIX}: событие {event.id} пропущено: другой тип")
            return

        ticket = event.ticket
        logger.info(
            f"{LOG_PREFIX}: смена ответственного: "
            f"event_id={event.id} ticket_id={ticket.id}"
        )

Настройка триггера

  1. Создайте Действие по событию.
  2. Выберите модель TicketEvent и действие типа «Скрипт».
  3. Укажите подключённый скрипт.
  4. Настройте событие «Смена ответственного».
  5. При необходимости задайте дополнительные условия и контекст.
  6. Включите действие и проверьте его на тестовой заявке.

В Python используйте именованную константу defs.TICKET_EVENTS_ASSIGNEE_CHANGE. Коды для настройки и другие события приведены в статье 913.

Старое и новое значения

В примерах встречаются old, new, old_value, new_value. Имена, доступные как атрибуты объекта или поля условий интерфейса, не обязательно являются полями для ORM-фильтра.

Например, счётчик назначений использует ORM-условие new__isnull=False, а пример pending-обработки переводит строковый статус в числовое значение события через defs.TICKET_EVENTS_STATUSES. Не подставляйте строковый статус заявки в любое поле события по аналогии.

Сначала проверьте описание модели и рабочий пример вашей версии. При отложенной обработке также учитывайте: событие описывает прошлое изменение, а event.ticket может уже содержать текущее состояние заявки. Перед действием заново проверьте необходимые условия.

7. Запуск по расписанию и рабочее время

Настройка

  1. Подключите Python-файл.
  2. Создайте Действие по расписанию типа «Скрипт».
  3. Выберите файл и задайте расписание.
  4. Внесите дополнительные параметры, если код их читает.
  5. Включите действие после проверки на небольшой выборке.

В опубликованном примере напоминаний используется * * * * * — запуск каждую минуту. Другие значения и часовой пояс выполнения сверяйте со своей конфигурацией и справкой crontab.

Периодический запуск сам по себе не передаёт конкретную заявку. Выборку нужно сформировать в run. Не ожидайте event только потому, что класс скрипта совпадает с примером событийной обработки.

Календарное и рабочее время

Для календарного порога фрагмент выглядит так:

from datetime import timedelta
from django.utils import timezone

cutoff = timezone.now() - timedelta(hours=48)

Это 48 обычных часов. Для рабочего времени нужно использовать расписание и исключать нерабочие интервалы. Исследованные файлы обращаются к методам Swarmica Schedule.business_time_duration(...), schedule.tz, schedule.is_business_hours(), а также к методам построения рабочих интервалов. Это внутренние методы; проверяйте их на целевой версии.

Пример приветствия

В файле trigger_new_chat_greeting.py проверяется одна секунда с момента создания заявки:

from datetime import timedelta

start = ticket.created_at
end = start + timedelta(seconds=1)
is_working_time = schedule.business_time_duration(start=start, end=end).total_seconds() > 0

Переменные ticket и schedule должны быть получены заранее. Это проверка момента создания, а не текущего времени запуска.

Пример pending-обработки

Файл recurring_autonotify_autosolve.py показывает составной процесс:

  1. Найти заявки в PENDING с расписанием компании заявителя.
  2. Найти момент последнего перехода в PENDING.
  3. Проверить, ответил ли клиент после этого момента.
  4. Если автоматического комментария ещё нет — рассчитать срок напоминания.
  5. Если комментарий один — рассчитать срок перехода в SOLVED.
  6. Выполнить действие, когда наступил срок и текущее время рабочее.

Два интервала задаются параметрами reminder_hours и close_hours; по умолчанию оба равны одному часу.

Особенность приложенного файла: source_channels используется как список UID: source__uid__in=source_channels. Передавайте реальные UID каналов. Если нужна фильтрация по типам, потребуется изменить запрос; строка email не должна автоматически трактоваться как UID.

Основная выборка исключает заявки без расписания компании, хотя вспомогательные функции содержат варианты работы без него. Это различие важно при адаптации: наличие ветки в функции ещё не означает, что основной цикл передаст ей такие заявки.

Также файл считает все автоматические API-комментарии сервисного пользователя после перехода в PENDING. Другая автоматизация может повлиять на этот счётчик. В собственной реализации используйте специфический маркер процесса.

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


Часть III. Работа с данными

8. Поиск объектов и связанные данные

Внутри скрипта доступны модели Django. Ниже — фрагменты для вставки в вашу логику, а не самостоятельные файлы.

Поиск одного объекта

from core.models import Group, Ticket

group = Group.objects.filter(uid="GROUP_UID").first()
ticket = Ticket.objects.filter(id=123).first()

Если объект не найден, first() возвращает None. Метод get(...) требует ровно одного результата и может вызвать исключение — используйте его, только когда отсутствие объекта является ошибкой и вы готовы её обработать.

Выборка заявок

from core.models import Ticket
from swarmica import defs

tickets = Ticket.objects.filter(
    status__in=[defs.TICKET_STATUSES_NEW, defs.TICKET_STATUSES_OPEN],
    group__uid="GROUP_UID",
).order_by("id")

Двойное подчёркивание позволяет обращаться к связанному объекту или задавать условие: group__uid, created_at__gte, body__icontains. Получайте только нужную выборку и ограничивайте её для первых проверок.

Выборка заявок с фильтром по датам

Давайте выведем ID заявки, тему и имя назначенного исполнителя для всех новых и открытых заявок, созданных между указанными датами начала и окончания:


from swarmica import defs
from core.models import Ticket

class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        params = kwargs.get('data', {})
        if not params:
            return
            
        start_date = params.get('start_date', None)
        
        if isinstance(start_date, str) and start_date.strip():
            report_start_date = parse_datetime(start_date)
        end_date = params.get('end_date', None)
        
        if isinstance(end_date, str) and end_date.strip():
            report_end_date = parse_datetime(end_date)

        if not report_end_date:
            report_end_date = now().replace(hour=23, minute=30, second=0, microsecond=0).replace(tzinfo=default_time_zone)
        if not report_start_date:
            report_start_date = report_end_date - timedelta(days=1)
            
        tickets = Ticket.objects.filter(created_at__range=(report_start_date,report_end_date), 
                                        status__in=[defs.TICKET_STATUSES_NEW, defs.TICKET_STATUSES_OPEN])
        
        for ticket in ticket:
            print(f"#{ticket.id}: {ticket.subject} ({ticket.assignee.name})")

Работа с пользователями

Давайте отключим все email-уведомления для заблокированных пользователей:

from swarmica import defs
from swarmica_auth.models import User

class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        
        users = User.objects.filter(role=defs.USER_ROLES_BLOCKED)
        
        for user in users:
            user.email_notifications.clear()

Оптимизация связей

  • select_related("assignee") — для ForeignKey и OneToOne. Убирает N+1, потому что делает JOIN.
  • prefetch_related("comments") — для ManyToMany и обратных ForeignKey. Делает отдельный запрос и сшивает результат в Python.

Без этих вызовов обращение к связанному объекту в цикле выполняет отдельный SQL на каждую итерацию. Для больших выборок это главный источник медленных скриптов.

Для очень больших наборов данных используйте iterator(chunk_size=...) — он читает строки порциями и не держит весь результат в памяти:

for ticket in Ticket.objects.filter(...).iterator(chunk_size=500):
    ...

Проверка наличия комментария

Фрагмент после получения переменной ticket:

has_comment = ticket.comments.filter(
    public=False,
    body__icontains="требуется проверка",
).exists()

public=False ограничивает поиск внутренними комментариями. icontains выполняет поиск без учёта регистра.

Последняя запись

Фрагмент для истории заявки:

last_event = ticket.events.order_by("-date", "-id").first()

При сортировке по убыванию first() возвращает самую позднюю запись, а last() — самую раннюю. Дополнительная сортировка по ID делает выбор определённее при равных датах.

Связанные объекты могут отсутствовать

requester = ticket.requester
organization = requester.organization if requester else None
schedule = organization.schedule if organization else None

Не предполагайте наличие компании, расписания или ответственного у каждой заявки. Для часто используемых связей примеры применяют select_related(...); для больших выборок — iterator(chunk_size=...). Общие методы описаны в справочнике Django QuerySet.

9. Заявки, комментарии и чаты

Изменение статуса

Фрагмент после получения заявки:

from swarmica import defs

ticket.status = defs.TICKET_STATUSES_SOLVED
ticket.save()

TICKET_STATUSES_SOLVED — статус «Решение предоставлено» , а не TICKET_STATUSES_CLOSED. Надпись в тексте уведомления должна соответствовать фактическому статусу, который устанавливает код.

После save() могут выполняться модельные обработчики, события и автоматизации. Проверяйте их влияние на сценарий.

Про save(update_fields=[...]). Если вы ограничиваете запись конкретными полями (ticket.save(update_fields=["status"])), Django не будет вызывать pre_save/post_save для остальных полей, а Swarmica-автоматизации могут не сработать так же, как при полном save(). Это не ошибка, но поведение отличается от обычного сохранения. Проверьте на тестовой заявке, прежде чем использовать update_fields.

Один вызов save() не доказывает эквивалентность любого изменения всем действиям штатного интерфейса.

Создание внутреннего автоматического комментария

Фрагмент с переменной ticket:

from core.models import TicketComment
from swarmica_auth.models import User
from swarmica import defs

author = User.objects.get_swarmica_service()
TicketComment.objects.create(
    ticket=ticket,
    author=author,
    responsible=author,
    body="<p>Дополнительная проверка выполнена.</p>",
    public=False,
    is_staff=True,
    is_autocomment=True,
    source=defs.TICKET_COMMENT_SOURCES_API,
)

User.objects.get_swarmica_service() — хелпер Swarmica, а не Django. Он может бросить исключение, если сервисный пользователь Swarmica не сконфигурирован (по умолчанию создается в процессе установки Swarmica). В production-скрипте оборачивайте его в try/except и пишите в лог понятную причину.

Атрибуты is_staff, is_autocomment, source встречаются в исследованных примерах. Для публичного ответа используется public=True. Доставка клиенту зависит от настроек каналов и обработки комментария — создание записи не следует описывать как универсальную гарантию отправки во все каналы.

Если текст содержит значения от пользователя или внешней системы, экранируйте их перед вставкой в HTML. Шаблон с разрешённой HTML-разметкой и обычный текст — разные типы входных данных.

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

Задача. При переводе заявки в SOLVED проверить внутренние комментарии и добавить автоматическую заметку.
Триггер. Событие «Статус изменён → Решение предоставлено».
Параметры. Три строки: что искать и что написать при обоих исходах.
Ожидаемый результат. Внутренний непубличный комментарий с маркером обработанного события; повторный запуск того же события не создаёт второй заметки.

Этот учебный вариант по мотивам статьи 1830 дополнительно проверяет состояние заявки и повторный запуск того же события.

Подключите файл add_check_note.py и задайте контекст:

{
  "text": "требуется проверка",
  "comment_if_text": "В заявке есть запрос на дополнительную проверку.",
  "comment_ifnot_text": "Запрос на дополнительную проверку не найден."
}
import logging
from html import escape

from django.db import transaction
from core.models import Ticket, TicketComment, TicketEvent
from runtime.runners import BaseRunner
from swarmica import defs
from swarmica_auth.models import User

logger = logging.getLogger(__name__)


class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        LOG_PREFIX = f"{self.script.name} ({self.script.uid})"
        data = kwargs.get("data") or {}
        event = data.get("event")
        if not isinstance(event, TicketEvent):
            logger.warning(f"{LOG_PREFIX}: передайте объект TicketEvent через действие по событию")
            return
        if event.event != defs.TICKET_EVENTS_STATUS_CHANGE:
            return

        names = ("text", "comment_if_text", "comment_ifnot_text")
        if not all(isinstance(data.get(k), str) and data[k].strip() for k in names):
            logger.error(f"{LOG_PREFIX}: укажите непустые строки: {', '.join(names)}")
            return
        marker = f"<!-- custom-check-note:event:{event.id} -->"
        author = User.objects.get_swarmica_service()

        with transaction.atomic():
            ticket = Ticket.objects.select_for_update().filter(id=event.ticket_id).first()
            if ticket is None or ticket.status != defs.TICKET_STATUSES_SOLVED:
                logger.info(f"{LOG_PREFIX}: состояние заявки изменилось; действие пропущено")
                return
            if ticket.comments.filter(author=author, public=False, body__contains=marker).exists():
                logger.info(f"{LOG_PREFIX}: событие {event.id} уже обработано")
                return

            found = ticket.comments.filter(
                public=False,
                is_autocomment=False,
                body__icontains=data["text"].strip(),
            ).exists()
            message = data["comment_if_text"] if found else data["comment_ifnot_text"]
            body = "<p>" + escape(message.strip()).replace("\n", "<br/>") + "</p>" + marker
            TicketComment.objects.create(
                ticket=ticket, author=author, responsible=author,
                body=body, public=False, is_staff=True, is_autocomment=True,
                source=defs.TICKET_COMMENT_SOURCES_API,
            )
        logger.info(
            f"{LOG_PREFIX}: заметка добавлена: "
            f"ticket_id={event.ticket_id} event_id={event.id}"
        )

В этом варианте поиск исключает автоматические комментарии, чтобы собственные заметки не влияли на следующие проверки. Маркер защищает от повторения одного события при сохранении комментария; если его удалить, защиту нужно восстановить другим способом.

Сообщение веб-чата

В примере приветствия используются другая модель и привязка к сессии чата. Фрагмент внутри Runner, после получения заявки:

from core.models import ChatMessage

session = ticket.chat_sessions.last()
if session is not None:
    ChatMessage.objects.create(
        session=session,
        public=True,
        author=self.service_user,
        body="Здравствуйте! Опишите, пожалуйста, ваш вопрос.",
    )

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

Для выбора приветствия по графику файл trigger_new_chat_greeting.py читает schedule_name, message и out_of_schedule_message; тексты — словари по языкам. Нужны существующее расписание и подходящий язык заявителя. Подробная настройка: статья 1274.

Для пустых чатов пример close_empty_chat.py сначала проверяет наличие любого комментария, затем создаёт публичный комментарий и переводит заявку в SOLVED. Наличие даже служебного комментария может остановить такой сценарий. Условия канала, статуса и времени задаются триггером; код не заменяет их настройку. См. статью 664.

10. Кастомные поля и счётчик назначений

Чтение

После получения объекта заявки:

value = ticket.custom_fields.get_value("CUSTOM_FIELD_UID")

custom_fields.get_value(...) — хелпер Swarmica, а не Django ORM. Используйте UID поля своей установки, а не значение из чужого примера. Проверяйте None отдельно от нуля и False, если они имеют разный смысл.

Запись

В примере счётчика назначений значение хранится через CustomFieldValue. Фрагмент с переменной ticket:

from custom_field.models import CustomField, CustomFieldValue
from django.contrib.contenttypes.models import ContentType
from core.models import Ticket

field = CustomField.objects.get(uid="CUSTOM_FIELD_UID")
content_type = ContentType.objects.get_for_model(Ticket)
CustomFieldValue.objects.update_or_create(
    field=field,
    content_type=content_type,
    object_id=ticket.id,
    defaults={"data": {"value": 7}},
)

ContentType и update_or_create — стандартный Django. CustomField / CustomFieldValue — модели Swarmica.

Важно про defaults={"data": {...}}. update_or_create полностью перезаписывает data. Если поле хранит не только value, но и другие ключи (например, служебные метаданные rich-text, идентификаторы вложений), они потеряются. Перед записью проверьте фактическое содержимое data у поля вашего типа и, если нужно, объединяйте словари.

Этот пример относится к числовому полю заявки. Для меню, списков и других типов не предполагается тот же формат значения. Уточните формат и ограничения поля перед записью.

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

Задача. Пересчитать число назначений заявки на сотрудника и записать в пользовательское поле.
Триггер. Событие «Смена ответственного».
Параметры. cf_uid — UID целочисленного поля заявки.
Ожидаемый результат. В поле записано актуальное число; повторная обработка того же события не увеличивает счётчик.

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

Определение метрики: считаются все назначения на сотрудника, включая первое. Снятие ответственного на «Никто» не учитывается. Это не готовый коэффициент передач и не число уникальных инженеров. Если бизнесу нужны именно передачи между сотрудниками, потребуется другая выборка событий.

Создайте целочисленное поле заявки и укажите его UID в дополнительном контексте:

{
  "cf_uid": "CUSTOM_FIELD_UID"
}

Учебный самостоятельный файл update_assignment_count.py:

import logging

from core.models import Ticket, TicketEvent
from custom_field.models import CustomField, CustomFieldValue
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from runtime.runners import BaseRunner
from swarmica import defs

logger = logging.getLogger(__name__)


class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        LOG_PREFIX = f"{self.script.name} ({self.script.uid})"
        data = kwargs.get("data") or {}
        event = data.get("event")
        if not isinstance(event, TicketEvent):
            logger.warning(f"{LOG_PREFIX}: передайте TicketEvent")
            return
        if event.event != defs.TICKET_EVENTS_ASSIGNEE_CHANGE:
            return
        cf_uid = data.get("cf_uid")
        if not isinstance(cf_uid, str) or not cf_uid.strip():
            logger.error(f"{LOG_PREFIX}: не указан cf_uid")
            return
        field = CustomField.objects.filter(uid=cf_uid.strip()).first()
        if field is None:
            logger.error(f"{LOG_PREFIX}: поле не найдено")
            return

        content_type = ContentType.objects.get_for_model(Ticket)
        with transaction.atomic():
            ticket = Ticket.objects.select_for_update().filter(id=event.ticket_id).first()
            if ticket is None:
                return
            count = TicketEvent.objects.filter(
                ticket=ticket,
                event=defs.TICKET_EVENTS_ASSIGNEE_CHANGE,
                new__isnull=False,
            ).count()
            CustomFieldValue.objects.update_or_create(
                field=field, content_type=content_type, object_id=ticket.id,
                defaults={"data": {"value": count}},
            )
        logger.info(f"{LOG_PREFIX}: ticket_id={event.ticket_id} assignments_count={count}")

Перед запуском проверьте, что поле предназначено для заявки и имеет целочисленный тип.

Про блокировку. select_for_update() здесь блокирует строку Ticket от параллельного изменения другими такими же запусками скрипта. Важно понимать, чего блокировка не делает:

  • она не мешает параллельной вставке новых TicketEvent;
  • она не защищает значение поля от записи другим кодом (другим скриптом, ручной правкой в интерфейсе);

Итоговое значение будет корректным до тех пор, пока все изменения поля проходят через этот же скрипт с той же блокировкой. Как только появляется второй писатель — договоритесь об общем правиле или используйте отдельный механизм координации.

Инструкция и исходный файл: «Как посчитать Handover Rate».

11. Повторные запуски и параллельная обработка

Не предполагайте, что событие всегда обработается ровно один раз, а периодические задачи никогда не пересекутся. Даже ручной повторный запуск может создать дубли.

СценарийПодход к повторному запуску
Числовая метрикаПересчитывать результат по исходным данным
Комментарий по событиюХранить маркер обработанного события и проверять его
Импорт объектаИскать по устойчивому идентификатору источника
Периодическое напоминаниеХранить этап конкретного процесса и время отправки
Создание внешнего объектаИспользовать ключ идемпотентности или сопоставление ID

Проверка «комментария ещё нет» и последующая запись должны выполняться согласованно, иначе два процесса увидят отсутствие комментария одновременно. Блокировка заявки в учебном примере заметки помогает, если все экземпляры процесса используют ту же схему.

Для другой логики могут потребоваться отдельная запись состояния или уникальное ограничение. Один transaction.atomic() без блокировки и без уникального ключа не гарантирует отсутствие дублей.

Защита от циклов

Скрипт может породить событие, на которое настроен он сам или другая автоматизация. Например, комментарий создаёт событие комментария, изменение поля — событие поля.

Ограничивайте тип события, проверяйте автора и маркер, меняйте только отличающееся значение. Расчёт метрики назначения должен запускаться по назначению, а не по любому изменению заявки. Проверьте цепочку на тестовой установке.

12. Массовые операции, транзакции и предварительный просмотр

Обязательные этапы

Для операции по списку объектов:

  1. Разберите и проверьте параметры.
  2. Найдите все объекты и покажите отсутствующие ID.
  3. Проверьте допустимые статусы и другие ограничения.
  4. Вычислите, что изменится.
  5. Выполните изменения выбранным способом.
  6. Запишите итог: найдено, изменено, пропущено, ошибок.

Исходный set_group_for_tickets.py реализует этот подход: читает tickets и group_uid, проверяет наличие всех заявок и статусы SOLVED / CLOSED, затем меняет группу только там, где она отличается.

Транзакция

Связанные изменения можно объединить в transaction.atomic(). Если из блока выходит исключение, изменения БД откатываются. Внешнее письмо или HTTP-запрос таким откатом не отменяются. См. документацию Django по транзакциям.

Фрагмент с заранее определёнными ticket_id и group:

from django.db import transaction
from core.models import Ticket

with transaction.atomic():
    ticket = Ticket.objects.select_for_update().get(id=ticket_id)
    if ticket.group_id != group.id:
        ticket.group = group
        ticket.save()

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

save и массовая запись

save() и bulk_update() отличаются. В исходном примере смены группы применяется bulk_update: такой метод не вызывает save() каждой модели и стандартные сигналы pre_save / post_save. Поэтому не предполагается, что он создаст те же события, что ручная операция в интерфейсе. См. раздел bulk_update в Django.

Отдельно: save(update_fields=[...]) — ещё один случай. Он сохранит только перечисленные поля, и pre_save/post_save для остальных полей не сработают. Swarmica-автоматизации, привязанные к изменению других полей, могут не запуститься.

Выбирайте способ записи по требуемому поведению, а не только по скорости. Аналогично проверяйте, что требуется при изменении пользовательского поля напрямую через CustomFieldValue.

apply и режим без изменений

Для административных действий полезен параметр apply, по умолчанию false. Это приём проектирования собственного скрипта; движок не добавляет предварительный просмотр автоматически.

Самостоятельный пример preview_group_move.py использует только изменение группы и по умолчанию ничего не записывает:

import logging

from core.models import Group, Ticket
from django.db import transaction
from runtime.runners import BaseRunner
from swarmica import defs

logger = logging.getLogger(__name__)


def parse_apply(value):
    if value is None:
        return False
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        value = value.strip().lower()
        if value in {"true", "1", "yes"}:
            return True
        if value in {"false", "0", "no", ""}:
            return False
    raise ValueError("Некорректный apply")


class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        LOG_PREFIX = f"{self.script.name} ({self.script.uid})"
        data = kwargs.get("data") or {}
        try:
            apply = parse_apply(data.get("apply"))
            raw_ids = data.get("tickets", "")
            if not isinstance(raw_ids, str):
                raise ValueError("tickets должен быть строкой")
            ticket_ids = sorted({int(x.strip()) for x in raw_ids.split(",") if x.strip()})
            if not ticket_ids or len(ticket_ids) > 100 or any(x <= 0 for x in ticket_ids):
                raise ValueError("Укажите от 1 до 100 положительных ID")
            group_uid = data.get("group_uid")
            if not isinstance(group_uid, str) or not group_uid.strip():
                raise ValueError("Укажите group_uid")
        except ValueError as error:
            logger.error(f"{LOG_PREFIX}: ошибка параметров: {error}")
            return

        group = Group.objects.filter(uid=group_uid.strip()).first()
        if group is None:
            logger.error(f"{LOG_PREFIX}: группа не найдена")
            return

        with transaction.atomic():
            tickets = list(Ticket.objects.select_for_update().filter(id__in=ticket_ids).order_by("id"))
            if {t.id for t in tickets} != set(ticket_ids):
                logger.error(f"{LOG_PREFIX}: часть заявок не найдена; изменений нет")
                return
            allowed = {defs.TICKET_STATUSES_SOLVED, defs.TICKET_STATUSES_CLOSED}
            if any(t.status not in allowed for t in tickets):
                logger.error(f"{LOG_PREFIX}: допустимы только SOLVED и CLOSED; изменений нет")
                return
            changes = [t for t in tickets if t.group_id != group.id]
            logger.info(
                f"{LOG_PREFIX}: найдено={len(tickets)} "
                f"к изменению={len(changes)} apply={apply}"
            )
            if not apply:
                return
            for ticket in changes:
                ticket.group = group
                ticket.save()
        logger.info(f"{LOG_PREFIX}: операция завершена")

Контекст для тестового запуска:

{
  "tickets": "101, 102",
  "group_uid": "GROUP_UID",
  "apply": false
}

Для веб-формы добавьте tickets и group_uid типа string, а apply типа boolean. Изменение apply на true разрешает запись. Код намеренно использует save() вместо bulk_update() из исходного файла; последствия для событий и автоматизаций нужно проверить отдельно.

Проверка предварительного просмотра не фиксирует состояние навсегда: между просмотром и применением данные могут измениться. Поэтому проверки повторяются при каждом запуске. Исходная инструкция по операции: статья 1887.


Часть IV. Действия и интеграции

13. Уведомления, шаблоны и выбор получателей

Выбор email-канала

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

from core.models import EmailChannel

channel = EmailChannel.objects.filter(default=True, is_deleted=False).first()

EmailChannel — модель Swarmica. Если канала нет или send_from не задан, скрипт должен завершиться с понятным сообщением. Для собственного SMTP используется channel.get_smtp_connection(); для системного реле примеры передают connection=None.

Отправка письма

Фрагмент после проверки канала и получателей:

from core.tasks.notifications import send_email

send_email(
    from_email=channel.send_from,
    to=["support-lead@example.com"],
    subject="Результат дополнительной проверки",
    text="Проверка завершена. Подробности доступны в заявке.",
    connection=None if channel.use_system_smtp_relay else channel.get_smtp_connection(),
)

send_email — хелпер Swarmica из core.tasks.notifications. Это сигнатура из приложенных примеров, а не полный справочник. Запись «отправлено» в логе скрипта не доказывает доставку в почтовый ящик: проверяйте результат почтовой отправки и настройки канала.

HTML-шаблон Jinja

Движок шаблонов в файлах получается через engines["jinja2"]. Фрагмент с переменной ticket:

from django.template import engines
from core.models import Instance

template = engines["jinja2"].from_string(
    "<p>Заявка #{{ ticket.id }}. Статус: {{ ticket.status }}</p>"
)
html_body = template.render(context={
    "instance": Instance.get_solo(),
    "ticket": ticket,
})

engines — Django. Instance.get_solo() — хелпер Swarmica.

В письме HTML можно передать через html=html_body вместе с текстовой версией. Проверьте экранирование динамических значений и поведение выбранного шаблона. Существующий базовый шаблон notifications/base_email.html также используется в исходных файлах.

Получатели зависят от причины события

Для CSAT и других классифицированных событий удобно задавать словарь «причина → получатели» в контексте. Обезличенный пример настройки:

{
  "reason_map": {
    "productbug": ["product-owner@example.com"],
    "documentation": ["docs-owner@example.com", "support-lead@example.com"]
  },
  "survey_score_threshold": 79
}

Учебная функция выбора получателей:

def recipients_for_reasons(selected_reasons, reason_map):
    if not isinstance(reason_map, dict):
        raise ValueError("reason_map должен быть словарём")
    if not isinstance(selected_reasons, (list, tuple)):
        raise ValueError("Причины должны быть списком")
    recipients = set()
    for reason in selected_reasons:
        addresses = reason_map.get(reason, [])
        if not isinstance(addresses, list):
            raise ValueError("Получатели для каждой причины должны быть списком")
        for address in addresses:
            if not isinstance(address, str) or not address.strip():
                raise ValueError("Получатель должен быть непустой строкой")
            recipients.add(address.strip())
    return sorted(recipients)

Функция только выбирает адреса: она не валидирует email, не находит опрос и не отправляет письмо. Эти шаги нужно добавить отдельно.

CSAT-пример показывает, как связать TicketEvent, CSATSurvey, дополнительные поля и шаблон письма. Оценка в исследованном файле сравнивается с порогом на шкале 0–100; не переносите число из пятибалльного отображения напрямую. Опрос должен соответствовать именно обрабатываемому событию — выбор последней записи по заявке и автору может быть недостаточен при задержанной обработке.

Уведомления об обращениях без ответа

Файл recurring_ticket_group_notify_noanswer.py показывает выбор группы, получение email её участников, анализ последних событий заявки и отправку письма. LIMIT_HOURS, CHECK_GROUP, MAIL_FROM в нём заданы в коде, а не читаются из контекста.

При адаптации вынесите нужные значения в параметры и определите, что означает «без ответа»: время с создания, последнего ответа инженера или последнего сообщения клиента. Сам по себе возраст заявки не определяет эту метрику. Инструкция: статья 1449.

14. Отчёты CSV и Excel

Выберите смысл отчёта

Сначала определите период, часовой пояс, фильтр объектов и единицу строки. Например, отчёт по событиям должен различать текущую группу заявки и группу в момент события: фильтр ticket__group=group относится к текущей группе.

Файл из статьи 1906 выбирает события за период и формирует CSV; файл из статьи 1434 группирует созданные заявки по часам и дням недели и формирует XLSX.

Полный учебный пример: список активных заявок в CSV

Задача. Выгрузить в CSV максимум 1 000 новых и открытых заявок одной группы и отправить файл письмом.
Триггер. Веб-форма.
Параметры. group_uid, recipient.
Ожидаемый результат. CSV с колонками Ticket ID;Status, приложенный к письму с указанного адреса.

Этот самостоятельный файл send_active_tickets_csv.py использует знакомые модели и отправку файла из исследованных примеров.

Параметры веб-формы:

[
  {
    "name": "group_uid",
    "type": "string",
    "required": true,
    "displayName": "UID группы"
  },
  {
    "name": "recipient",
    "type": "string",
    "required": true,
    "displayName": "Email получателя"
  }
]

Для первого запуска разрешите форму только администратору. Если расширяете доступ, ограничьте допустимые группы и получателей по правилам вашей компании. Для отправки отчётов адресат не должен автоматически получать любые данные только потому, что знает UID группы.

import csv
import logging
import os
import tempfile

from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from core.models import EmailChannel, Group, Ticket
from core.tasks.notifications import send_email
from runtime.runners import BaseRunner
from swarmica import defs

logger = logging.getLogger(__name__)
MAX_ROWS = 1000


class Runner(BaseRunner):
    def run(self, *args, **kwargs):
        LOG_PREFIX = f"{self.script.name} ({self.script.uid})"
        data = kwargs.get("data") or {}
        group_uid = data.get("group_uid")
        recipient = data.get("recipient")
        if not isinstance(group_uid, str) or not isinstance(recipient, str):
            logger.error(f"{LOG_PREFIX}: укажите group_uid и recipient как строки")
            return
        recipient = recipient.strip()
        try:
            validate_email(recipient)
        except ValidationError:
            logger.error(f"{LOG_PREFIX}: некорректный email")
            return
        group = Group.objects.filter(uid=group_uid.strip()).first()
        channel = EmailChannel.objects.filter(default=True, is_deleted=False).first()
        if group is None or channel is None or not channel.send_from:
            logger.error(f"{LOG_PREFIX}: группа или исходящий email-канал не настроены")
            return

        rows = list(Ticket.objects.filter(
            group=group,
            status__in=[defs.TICKET_STATUSES_NEW, defs.TICKET_STATUSES_OPEN],
        ).order_by("id").values_list("id", "status")[:MAX_ROWS + 1])
        if len(rows) > MAX_ROWS:
            logger.error(f"{LOG_PREFIX}: выборка больше {MAX_ROWS} строк; сузьте отчёт")
            return

        path = None
        try:
            with tempfile.NamedTemporaryFile(
                mode="w", encoding="utf-8-sig", newline="",
                suffix=".csv", delete=False,
            ) as output:
                path = output.name
                writer = csv.writer(output, delimiter=";")
                writer.writerow(["Ticket ID", "Status"])
                writer.writerows(rows)

            send_email(
                from_email=channel.send_from,
                to=[recipient],
                subject="Активные заявки группы",
                text=f"В приложении {len(rows)} записей.",
                attachment_files=[path],
                connection=None if channel.use_system_smtp_relay else channel.get_smtp_connection(),
            )
            logger.info(f"{LOG_PREFIX}: отчёт передан на отправку; строк={len(rows)}")
        finally:
            if path is not None and os.path.exists(path):
                os.remove(path)

Пустая выборка даст CSV с заголовком. При превышении лимита скрипт не отправляет усечённый отчёт. Временный файл удаляется в finally.

Этот порядок очистки соответствует прямому вызову send_email в исследованных файлах. Если меняете реализацию на асинхронную очередь, файл должен существовать до чтения обработчиком; немедленное удаление может сломать отправку.

XLSX

Для Excel-файла используется openpyxl. Учебная функция, принимающая список строк или другой итерируемый набор:

from openpyxl import Workbook


def save_xlsx(rows, path):
    workbook = Workbook(write_only=True)
    sheet = workbook.create_sheet("Заявки")
    sheet.append(["Ticket ID", "Status"])
    for ticket_id, status in rows:
        sheet.append([ticket_id, status])
    workbook.save(path)

Для свободного текста из внешних источников учитывайте возможность интерпретации значения как формулы в табличном редакторе. В приведённом примере экспортируются только ID и фиксированный статус.

В часовом отчёте применяются ExtractHour, ExtractWeekDay, Count, Case, When. Группировка выполняется в запросе; затем строки превращаются в Excel-таблицу. Часовой пояс передаётся в функции извлечения времени. Инструкция: статья 1434.

Даты и часовые пояса

Для сравнения дат используйте значения с часовым поясом. Для отчёта за целые дни удобно задать полуоткрытый интервал: от начала первого дня включительно до начала дня после последнего исключительно.

Самостоятельная вспомогательная функция для строк YYYY-MM-DD:

from datetime import date, datetime, time, timedelta
from zoneinfo import ZoneInfo


def report_period(date_from, date_to, timezone_name="Europe/Moscow"):
    start_day = date.fromisoformat(date_from)
    end_day = date.fromisoformat(date_to)
    if end_day < start_day:
        raise ValueError("Конец периода раньше начала")
    tz = ZoneInfo(timezone_name)
    start = datetime.combine(start_day, time.min, tzinfo=tz)
    end_exclusive = datetime.combine(end_day + timedelta(days=1), time.min, tzinfo=tz)
    utc = ZoneInfo("UTC")
    return start.astimezone(utc), end_exclusive.astimezone(utc)

В запросе используйте date__gte=start и date__lt=end_exclusive, либо аналогичные условия для created_at. Для специальных исторических переходов часового пояса отдельно определите правила неоднозначного времени. Общая справка: Python zoneinfo.

Инструкция по исходному отчёту переходов статуса: статья 1906. При адаптации дополнительно ограничьте выборку типом события «Смена статуса» и проверьте ORM-поля старого и нового значения на своей версии.

15. Внешние API и постраничная загрузка

Запрос и обработка ответа

В миграционном примере используется requests. В своей реализации задайте тайм-аут, проверьте HTTP-статус и формат ответа. Следующий фрагмент иллюстрирует вызов; переменные url, headers и payload определяются вашим сценарием:

import requests

response = requests.post(url, json=payload, headers=headers, timeout=(5, 30))
response.raise_for_status()
result = response.json()

json=payload кодирует словарь как JSON. timeout ограничивает ожидание соединения и чтения. Ответ может быть не JSON даже при успешном HTTP-статусе — обработайте это отдельно. Подробнее: официальная документация Requests.

Для сетевых ошибок предусмотрите понятный результат: повторить позже, оставить запись для ручного разбора или остановить сценарий. Повторное создание внешнего объекта после тайм-аута может дать дубликат, если первая попытка фактически сработала. Используйте ключ идемпотентности, когда его поддерживает внешняя система.

Постраничная загрузка

Мигратор статей читает results и переходит по next. Ниже учебная функция для API с таким форматом. Она ограничивает число страниц и не пересылает заголовки авторизации на другой узел.

from urllib.parse import urljoin, urlsplit
import requests


def iter_api_results(start_url, headers, max_pages=100):
    origin = urlsplit(start_url)
    if origin.scheme != "https" or not origin.netloc or origin.username or origin.password:
        raise ValueError("В этом примере требуется HTTPS URL без учётных данных")
    url = start_url
    visited = set()
    page_count = 0

    with requests.Session() as session:
        while url:
            parsed = urlsplit(url)
            if (parsed.scheme, parsed.netloc) != (origin.scheme, origin.netloc):
                raise ValueError("next ведёт на другой узел")
            if url in visited or page_count >= max_pages:
                raise ValueError("Цикл пагинации или превышен лимит страниц")
            visited.add(url)
            page_count += 1
            response = session.get(
                url, headers=headers, timeout=(5, 30), allow_redirects=False,
            )
            if 300 <= response.status_code < 400:
                raise ValueError("Перенаправление требует отдельной проверки")
            response.raise_for_status()
            try:
                page = response.json()
            except ValueError as error:
                raise ValueError(
                    f"Ответ не является JSON (Content-Type={response.headers.get('Content-Type')!r})"
                ) from error
            if not isinstance(page, dict) or not isinstance(page.get("results"), list):
                raise ValueError("Ожидался объект с массивом results")
            for item in page["results"]:
                yield item
            next_url = page.get("next")
            if next_url is not None and not isinstance(next_url, str):
                raise ValueError("next должен быть строкой или null")
            url = urljoin(url, next_url) if next_url else None

В том же файле, внутри run, после получения проверенных host и token:

headers = {"Authorization": f"Token {token}"}
for category in iter_api_results(f"{host.rstrip('/')}/api/categories/", headers):
    logger.info(f"Получена категория id={category.get('id')}")

Схема Token соответствует использованию API-токена в исследованном миграторе Swarmica. Для другой системы способ авторизации может отличаться. Здесь HTTPS — ограничение учебной функции, а не описание всех вариантов установки Swarmica.

Не помещайте реальные секреты в публикуемый код, клиентские виджеты и лог. Способ хранения и выдачи серверного секрета согласуйте с администратором установки.

Перенос статей: почему два прохода

Файл onetime_hc_migration.py выполняет работу в два этапа:

  1. Получает категории и создаёт или находит соответствующие локальные категории.
  2. Создаёт статьи-заготовки с внешними идентификаторами.
  3. Строит соответствие ID исходных и локальных статей.
  4. Загружает вложения и переводы, заменяет ссылки на локальные адреса.

Такой подход нужен, когда статьи ссылаются друг на друга и новые ID заранее неизвестны. refresh_migrated разрешает обновление ранее перенесённой статьи; в исходном файле перед обновлением удаляются её переводы и вложения. Это операция замены, которую нужно проверять на резервной копии.

Основная загрузка исходного файла ограничена visibility=ALL. Она не является универсальным переносом всех вариантов сегментации и доступа. Инструкция: статья 306.

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

16. Импорт пользователей, компаний и статей

Импорт обычно объединяет чтение файла, проверку колонок, нормализацию значений, поиск существующего объекта, создание или обновление и итоговую статистику.

Файлы и контейнеры

Примеры CSV используют settings.UPLOADS_ROOT. Markdown-импорт ожидает каталог /swarmica/swarmica/old_articles, подключённый к контейнеру.

Файл должен быть доступен именно процессу, который выполняет скрипт. Копирование в контейнер django помогает только при общей файловой системе с нужным обработчиком либо при запуске в этом контейнере. Проверьте тома вашей установки, а не только имя каталога.

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

from pathlib import Path


def resolve_csv_file(upload_root, filename):
    if not isinstance(filename, str) or not filename.strip():
        raise ValueError("Укажите имя файла")
    root = Path(upload_root).resolve()
    candidate = (root / filename.strip()).resolve()
    if candidate.parent != root:
        raise ValueError("Файл должен находиться непосредственно в каталоге uploads")
    if candidate.suffix.lower() != ".csv" or not candidate.is_file():
        raise ValueError("CSV-файл не найден")
    return candidate

Чтение CSV

Фрагмент с полученным путём path:

import csv

with open(path, encoding="utf-8-sig", newline="") as source:
    reader = csv.DictReader(source, delimiter=";")
    required = {"Сотрудник", "E-Mail"}
    if not required.issubset(set(reader.fieldnames or [])):
        raise ValueError("Нужны колонки Сотрудник и E-Mail")
    for line_number, row in enumerate(reader, start=2):
        name = row["Сотрудник"].strip()
        email = row["E-Mail"].strip().lower()
        # Проверка строки, поиск объекта и обработка выполняются здесь.

utf-8-sig учитывает возможную BOM-метку. Разделитель и названия колонок должны соответствовать вашему файлу. Фрагмент намеренно не создаёт пользователей.

Пользователи с ролью «Сотрудник»

Файл import_users.py:

  • Читает имя CSV из параметра filename.
  • Ожидает колонки Сотрудник и E-Mail, разделённые ;.
  • Проверяет существующего пользователя по email и email-идентификатор.
  • Создаёт пользователя с defs.USER_ROLES_INTERNAL_USER.
  • Вызывает set_unusable_password(); вход требует отдельной настройки пароля.
  • Считает созданные и пропущенные записи.

Роль «Сотрудник» в этом примере не равна автоматически роли «Агент». Не меняйте роль по названию файла без проверки вашей модели доступа. Инструкция: статья 1741.

Компании и клиенты

Файл import_organizations_and_clients.py ожидает client;email;organization, поддерживает ряд синонимов колонок и использует внутренние сервисы Swarmica data_import со схемами данных.

Это полезный пример выделения импорта в отдельный слой. Но его правила объединения объектов специфичны: организация ищется по имени и внешнему ID, существующий пользователь из другой организации пропускается. Для вашей задачи заранее определите, как обрабатывать совпадающие имена, разные внешние ID и смену компании.

В прочитанной версии существующий пользователь без компании не получает компанию из CSV: ветка считает запись уже обработанной. Если вам нужна такая привязка, добавьте отдельную проверенную логику. Инструкция: статья 1753.

Статьи из Markdown

Файл article_import.py:

  • Читает .md непосредственно из указанного каталога.
  • Использует имя файла без расширения как тему статьи.
  • Находит или создаёт категорию по имени и языку.
  • Создаёт Article и ArticleTranslation.
  • По умолчанию использует русский язык и статус UNAPPROVED.

В нём нет поиска уже импортированной статьи: повторный запуск создаёт новые статьи. Для повторяемого импорта добавьте устойчивый ключ источника и правило обновления. Импорт текста также не означает автоматический перенос всех локальных картинок и связанных файлов. Инструкция: статья 1661.

Отключение сигналов в миграционных примерах

Некоторые импорты используют factory.django.mute_signals(...). Это специальная техника для массового переноса, которая отключает часть модельных обработчиков в процессе выполнения. Она может повлиять на регистрацию событий и связанные действия; в общем процессе сигналы могут быть важны и для других задач.

Не переносите такой декоратор в обычную автоматизацию ради ускорения или защиты от повторов. Для собственной реализации сначала определите, какие штатные обработчики нужны, и проверьте последствия изменения способа записи.


Часть V. Эксплуатация

17. Диагностика, производительность и эксплуатация

Что записывать в лог

  • Название, UID и версию скрипта.
  • ID события и объекта.
  • Причину пропуска или остановки.
  • Количество найденных, изменённых и ошибочных записей.
  • Трассировку неожиданного исключения.

Пример фрагмента обработки одного объекта:

try:
    process_ticket(ticket)
except Exception:
    logger.exception(f"ошибка обработки ticket_id={ticket.id}")

process_ticket здесь — ваша собственная функция. Для независимых объектов можно продолжать обработку после ошибки одного из них. Для единой операции «всё или ничего» исключение должно приводить к откату, а не скрываться внутри транзакции.

Типичные проблемы

СимптомЧто проверить
Скрипт не запускаетсяЗагрузка файла, активность действия, выбранный скрипт, условия и расписание
Нет eventСпособ запуска: веб-форма и расписание не обязаны передавать событие
Есть только event_idЗагрузку события правильной моделью
AttributeErrorОтсутствующий объект, тип параметра, атрибут вашей версии
FieldErrorORM-поле и путь связи; имя в интерфейсе может отличаться
Параметр не влияет на результатЧитает ли код этот ключ; нет ли значения, заданного только в константе
Фильтр каналов не срабатываетUID или тип канала, регистр и фактический ORM-запрос
Повторяются уведомленияМаркер обработки, состояние процесса и параллельные запуски
Письмо не приходитEmail-канал, SMTP, адреса, ошибка отправки и доставка
Вложение недоступноПуть в контейнере-обработчике и момент удаления файла
Метрика отличается от ожиданийОпределение метрики: первое назначение, снятие ответственного, повторы
Импорт создаёт дублиКлюч поиска существующей записи и правила повторного запуска
После обновления возникла ошибкаИзменение внутренних моделей, функций и настроек

Как проверить поля модели на своей версии

Если пример вызывает FieldError, можно временно вывести имена полей модели в тестовом диагностическом скрипте. Фрагмент для размещения внутри run после импорта модели:

from core.models import TicketEvent

field_names = sorted(field.name for field in TicketEvent._meta.get_fields())
logger.info(f"Поля TicketEvent: {field_names}")

Это имена полей и связей ORM, а не все свойства и методы объекта. Наличие атрибута в описании контекста не делает его фильтруемым полем. Для моделей или методов, которых нет в документации, передайте техподдержке конкретную операцию, свою версию и ошибку; не подбирайте названия вслепую.

Производительность и частота запуска

Начинайте с ограниченной выборки. Вынесите повторяющиеся запросы из цикла, загрузите необходимые связи заранее, агрегируйте данные в БД, если это соответствует задаче. Не сохраняйте весь большой набор в память без необходимости.

Частота расписания должна соответствовать длительности обработки. Если задача может выполняться дольше интервала, предусмотрите координацию запусков. Сетевые ожидания ограничивайте тайм-аутами; временные файлы очищайте.

Добавление файлового обработчика логов при каждом импорте модуля может привести к дублированию записей. Для первого скрипта достаточно стандартного logging.getLogger(__name__).

Что проверить перед рабочим запуском

  1. Корректные, отсутствующие и неверно типизированные параметры.
  2. Пустую выборку и отсутствующие связанные объекты.
  3. Повторное выполнение того же события.
  4. Параллельное выполнение и изменение объекта пользователем.
  5. Ошибку внешней системы или SMTP.
  6. Рабочие часы, праздники и часовой пояс для временной логики.
  7. События и другие автоматизации после записи данных.
  8. Доступ к форме и допустимые данные / действия.
  9. Объём обработки и длительность запуска.

Храните исходный код в системе контроля версий, фиксируйте проверенную версию Swarmica и владельца сопровождения. Перед обновлением проверяйте расширения в тестовой установке. Для диагностики подготовьте имя и версию скрипта, способ запуска, обезличенные параметры, ID тестового объекта и трассировку ошибки.

18. Каталог примеров и дальнейшее изучение

Ниже — общедоступные статьи с приложенными Python-файлами, на которых можно изучать отдельные приёмы. Файл из статьи — исходный пример для проверки и адаптации, а не автоматически готовое расширение под любой процесс.

СтатьяФайлЧему учит
Базовый шаблон — 1157sample.ru.py, sample.en.pyBaseRunner, run, логирование; исполняемая часть обоих шаблонов одинакова
Пустые чаты — 664close_empty_chat.pyСобытие, проверка комментариев, сервисный пользователь, публичный комментарий и статус
Приветствие чата — 1274trigger_new_chat_greeting.pyЗагрузка события по ID, расписание, язык и ChatMessage
Счётчик назначений — 1333trigger_update_assigned_count.pyПодсчёт событий, ContentType, CustomFieldValue
Внутренняя заметка — 1830trigger_autonote.pyКонтекст, поиск внутренних комментариев и выбор действия
Заявки без ответа — 1449recurring_ticket_group_notify_noanswer.pyПериодическая выборка, история событий, группа получателей и шаблон письма
Напоминания по расписанию компании — 1819recurring_autonotify_autosolve.pyРабочее время, этапы процесса и поиск ответа клиента
Отчёт по событиям ожидания — 1906onetime_send_csv_with_ticket_status_change_report.pyДаты, CSV, email-канал, вложение и очистка файла
Нагрузка по часам — 1434onetime_download_hourly_report.pyАгрегация, часовой пояс и Excel
Смена группы — 1887set_group_for_tickets.pyВеб-форма, список ID, проверка всех объектов, транзакция и массовая запись
Импорт сотрудников — 1741import_users.pyCSV, проверка существующего пользователя и создание роли «Сотрудник»
Импорт компаний и клиентов — 1753import_organizations_and_clients.pyСхемы и сервисы импорта, нормализация колонок, правила сопоставления
Статьи из Markdown — 1661article_import.pyФайлы, категории, статья и её перевод
Перенос статей между установками — 306onetime_hc_migration.pyAPI, пагинация, внешние ID, два прохода, вложения и замена ссылок

Шаблон скрипта

Скачать шаблон sample.py

Рекомендуемый маршрут:

  1. Запустите минимальный скрипт из раздела 3 и убедитесь, что параметры доходят.
  2. Прогоните первый рабочий скрипт из раздела 5 на тестовой группе.
  3. Настройте скрипт, который только пишет событие в лог (раздел 6).
  4. На тестовой заявке попробуйте пользовательское поле или внутреннюю заметку (разделы 9–10).
  5. Разберите пример по расписанию и определите правила повторного запуска (разделы 7, 11).
  6. Добавьте отчёт или интеграцию под свой конкретный процесс (разделы 13–16).
  7. Согласуйте сопровождение, ограничения доступа и проверку перед обновлениями (разделы 17–18).

Другие инструкции находятся в категории автоматизации. Для запуска через API или командную строку используйте схему вашей версии и согласованную инструкцию: это руководство не задаёт универсальную команду CLI или endpoint для всех версий.

Обновлена: 30 сент. 2026 г.

#949: Автоматизация на базе runtime-scripts

Автоматизация на базе runtime-scripts состоит из двух частей:

a. Непосредственно сам скрипт, который совершает необходимые действия

b. Создание и настройка триггера по событию или триггера по расписанию

I. Создание скриптов

  1. Пишем нужный скрипт на питоне
  2. Логинимся в Свормику и идем в раздел Настройки>Скрипты, нажимаем Создать и создаем сущность скрипта
  3. В поле Скрипт загружаем ранее созданный скрипт

II. Создание и настройка триггера

  1. Перейдите в раздел Настройки>Действие по событию и нажмите создать
  2. Заполните поля тип события , например TicketEvent для событий с тикетами
  3. Типа действия: Скрипт
  4. Действие - выберете из списка ранее созданный и загруженный скрипт
  5. Запустите триггер нажав
Обновлена: 18 мая 2026 г.

#913: Коды событий в Swarmica

Пользователь: UserEvent

API Список событий с пользователем

КодНазваниеЗначение
0USER_EVENTS_ROLE_CHANGEDСмена роли
1USER_EVENTS_PASSWORD_CHANGEDСмена пароля
2USER_EVENTS_IDENTITY_ADDEDДобавлена учётная запись
3USER_EVENTS_ORGANIZATION_CHANGEDСмена компании
4USER_EVENTS_SCHEDULE_CHANGEDСмена расписания
5USER_EVENTS_NAME_CHANGEDСмена имени
6USER_EVENTS_CHAT_STATUS_CHANGEDСмена статуса в чате
7USER_EVENTS_MERGED_INTOОбъединен в пользователя
8USER_EVENTS_MERGED_FROMОбъединен из пользователя
9USER_EVENTS_USER_CREATEDПользователь создан
10USER_EVENTS_LOGINЗапрошен доступ в систему
11USER_EVENTS_NOTIFICATION_ENABLEDПочтовое уведомление включено
12USER_EVENTS_NOTIFICATION_DISABLEDПочтовое уведомление выключено

Статья: ArticleEvent

API Список событий в статье

КодНазваниеЗначение
0ARTICLE_EVENT_UNKNOWNПустое событие
1ARTICLE_EVENT_CREATEDСтатья создана
2ARTICLE_EVENT_SEGMENT_CHANGEDИзменён сегмент публикации статьи
3ARTICLE_EVENT_PUBLISHEDСтатья опубликована
4ARTICLE_EVENT_UNPUBLISHEDСтатья снята с публикации
5ARTICLE_EVENT_DELETEDСтатья заархивирована
6ARTICLE_EVENT_UNDELETEDСтатья восстановлена из архива
7ARTICLE_EVENT_AUTHOR_CHANGEDСмена автора
8ARTICLE_EVENT_FLAGGEDСтатья помечена к исправлению
9ARTICLE_EVENT_UNFLAGGEDПометка к исправлению снята
10ARTICLE_EVENT_AGENT_COMMENTСотрудник добавил комментарий
11ARTICLE_EVENT_CUSTOMER_COMMENTКлиент добавил комментарий
12ARTICLE_EVENT_AGENT_COMMENT_CHANGEDСотрудник отредактировал комментарий

Заявка: TicketEvent

API Список событий в заявке

КодНазваниеЗначение
0TICKET_EVENTS_UNKNOWNПустое событие
1TICKET_EVENTS_KCS_NEWСтатья создана из заявки
2TICKET_EVENTS_KCS_LINKСтатья добавлена к заявке
3TICKET_EVENTS_KCS_FLAGСтатья помечена к исправлению
4TICKET_EVENTS_STATUS_CHANGEСмена статуса
5TICKET_EVENTS_AGENT_COMMENTСотрудник добавил ответ
6TICKET_EVENTS_CUSTOMER_COMMENTКлиент добавил ответ
7TICKET_EVENTS_RATEDЗаполнена оценка качества сервиса
8TICKET_EVENTS_PRIORITY_CHANGEСмена приоритета
9TICKET_EVENTS_CREATEDЗаявка создана
10TICKET_EVENTS_ASSIGNEE_CHANGEСмена ответственного
11TICKET_EVENTS_REQUESTER_CHANGEСмена заявителя
12TICKET_EVENTS_GROUP_CHANGEСмена группы
13TICKET_EVENTS_PLATFORM_CHANGEСмена платформы
14TICKET_EVENTS_PRODUCT_CHANGEСмена продукта
15TICKET_EVENTS_VERSION_CHANGEСмена версии
16TICKET_EVENTS_EDITION_CHANGEСмена компоновки
17TICKET_EVENTS_ISSUE_ADDЗадача добавлена
18TICKET_EVENTS_ISSUE_DELETEЗадача удалена
19TICKET_EVENTS_CF_CHANGEИзменено значение дополнительного атрибута
20TICKET_EVENTS_KCS_UNLINKУбрана связь со статьёй
21TICKET_EVENTS_SKILL_ADDДобавлен навык
22TICKET_EVENTS_SKILL_DELETEУдалён навык
23TICKET_EVENTS_RELATED_TICKET_ADDДобавлена связанная заявка
24TICKET_EVENTS_RELATED_TICKET_DELETEУдалена связь с заявкой
25TICKET_EVENTS_MERGEDЗаявка объединена с другой
26TICKET_EVENTS_CHAT_SESSION_STARTEDНачалась сессия в чате
27TICKET_EVENTS_CHAT_SESSION_ENDEDСессия в чате завершена
28TICKET_EVENTS_AGENT_INTERNAL_COMMENTВнутренний комментарий добавлен
29TICKET_EVENTS_PENDING_NOTIFICATION_SENTОтправлено почтовое уведомление об ожидании ответа
30TICKET_EVENTS_SLA_CHANGEDИзменена политика SLA
31TICKET_EVENTS_LICENSE_CHANGEDИзменена лицензия
32TICKET_EVENTS_ISSUE_STATUS_CHANGED_DEPRECATEDНе используется
33TICKET_EVENTS_FORKEDИз заявки создана новая
34TICKET_EVENTS_ISSUE_CLOSEDСвязанная задача закрыта
35TICKET_EVENTS_ISSUE_OPENEDСвязанная задача открыта
36TICKET_EVENTS_AUTOASSIGN_RULE_APPLIEDПрименено правило автоматического назначения
37TICKET_EVENTS_INTERNAL_COMMENT_CHANGEDИзменён внутренний комметнарий
38TICKET_EVENTS_ARTICLE_NOT_NEEDEDПереключен флаг "Статья не нужна"
39TICKET_EVENTS_AUTORESPONSE_COMMENTДобавлен автоматический ответ системы
40TICKET_EVENTS_TIME_ESTIMATE_CHANGEИзменена оценка времени
41TICKET_EVENTS_ISSUE_DELETEDЗвязанная задача удалена
42TICKET_EVENTS_ASSET_ADDEDДобавлен актив
43TICKET_EVENTS_ASSET_DELETEDАктив удалён
44TICKET_EVENTS_ASSET_UPDATEDАктив изменён
45TICKET_EVENTS_SUBJECT_CHANGEDИзменение заголовка заявки
46TICKET_EVENTS_PUBLIC_COMMENT_CHANGEDПубличный ответ изменён
47TICKET_EVENTS_COMMENT_DELETEDКоментарий удалён
48TICKET_EVENTS_CC_ADDEDПользователь добавлен в CC
49TICKET_EVENTS_CC_DELETEDПользователь убран из CC
50TICKET_EVENTS_WATCHER_ADDEDПользователь добавлен в наблюдатели
51TICKET_EVENTS_WATCHER_DELETEDПользователь убран из CC

Обновлена: 18 мая 2026 г.
Всего результатов: 34
Элементов на странице
  • 5
  • 10(current)
  • 20
Страница
  • 1(current)
  • 2
  • 3
  • 4