Skip to content

Скрипты и выражения

Где встречаются скрипты

МестоДвижокДоступные переменные
Automation-хуки сущности (before_create, after_update…)SafeScriptRunner::execute()$data, $old, $user, $api
BPMN scriptTaskBpmnEngineeval()$api, $payload, $entityId
IncomingAction (трансформация сообщений)SafeScriptRunner::executeIncomingAction()$incoming, $data, $api
BPMN exclusiveGateway условиеSymfony ExpressionLanguageпеременные из process_variables

Automation-хуки сущности

Определяются в конфигурации таблицы (поле automations). Запускаются через SafeScriptRunner::execute().

Триггеры

ТриггерКогда$oldПрерывает операцию при исключении
before_createДо записи в БДnullДа
after_createПосле записиnullНет (только лог)
before_updateДо UPDATEданные до измененияДа
after_updateПосле UPDATEданные до измененияНет (только лог)
before_deleteДо DELETEтекущие данныеДа
after_deleteПосле DELETEданные записиНет (только лог)

Системные поля недоступны

Поля created_by, updated_by, created_at, updated_at удаляются из $data до передачи в хук. Обращаться к ним нельзя.

Переменные контекста

php
$data   // array — текущие/новые данные; в before_* изменения на этом массиве попадают в БД
$old    // array|null — данные до изменения (только в before/after_update и before/after_delete)
$user   // object — текущий пользователь: $user->id, $user->name, $user->email
$api    // PublicApiHelper — работа с БД, уведомления, утилиты

Реальные примеры из пресетов

Автозаполнение поля при создании:

php
// before_create — it_requests
if (empty($data['requested_by'])) {
    $data['requested_by'] = $user->id;
}

Уведомление при смене исполнителя:

php
// after_update — it_requests
if (isset($data['assignee']) && ($old['assignee'] ?? null) != $data['assignee']) {
    $api->sendPushNotification(
        'Новая заявка',
        'Вам назначена заявка: ' . $data['name'],
        '/e/it_requests/' . $data['id'],
        [$data['assignee']]
    );
}

Уведомление при смене статуса (с маппингом текста):

php
// after_update — it_requests
if (isset($data['status']) && ($old['status'] ?? null) != $data['status']
    && !empty($data['requested_by'])) {

    $statusLabels = [
        'assigned' => 'Назначен исполнитель',
        'in_work'  => 'Взята в работу',
        'waiting'  => 'Ожидает ответа от вас',
        'resolved' => 'Решена — пожалуйста, подтвердите',
        'rejected' => 'Отклонена',
        'closed'   => 'Закрыта',
    ];

    if (isset($statusLabels[$data['status']])) {
        $api->sendPushNotification(
            'Заявка: ' . $data['name'],
            $statusLabels[$data['status']],
            '/e/it_requests/' . $data['id'],
            [$data['requested_by']]
        );
    }
}

Расчёт дедлайна по SLA категории:

php
// before_update — it_requests
if (isset($data['status']) && $data['status'] === 'assigned'
    && empty($data['due_date']) && !empty($data['category'])) {

    $cat = $api->get('it_categories', $data['category']);
    if ($cat && !empty($cat['sla_hours'])) {
        $data['due_date'] = date('Y-m-d H:i:s', time() + $cat['sla_hours'] * 3600);
    }
}

Обновление связанной записи при выигрыше сделки:

php
// after_update — crm_deals
if (isset($data['stage']) && $data['stage'] === 'won' && !empty($data['client'])) {
    $client = $api->get('crm_clients', $data['client']);
    if ($client && in_array($client['status'] ?? '', ['lead', 'prospect'])) {
        $api->update('crm_clients', $data['client'], ['status' => 'active']);
    }
}

Валидация дат (бросает исключение — прерывает операцию):

php
// before_create — hr_vacations
if (!empty($data['start_date']) && !empty($data['end_date'])) {
    if (strtotime($data['start_date']) > strtotime($data['end_date'])) {
        throw new \Exception('Дата начала не может быть позже даты окончания.');
    }
}

SafeScriptRunner — ограничения и детали

Чёрный список

Следующие функции и классы запрещены (проверяются через token_get_all(), регистронезависимо):

exec, passthru, system, shell_exec, popen, proc_open, pcntl_exec,
eval, assert, create_function,
include, require,
file_get_contents, file_put_contents, unlink, rmdir, mkdir,
phpinfo,
Database, DB, Schema, Artisan, Storage,
env

Также запрещены backtick-команды `ls`.

Как выполняется код

  1. Стрипаются PHP-теги (<?php, ?>).
  2. Проверяется чёрный список — выбрасывается исключение при нарушении.
  3. Код оборачивается в замыкание с инъекцией контекстных переменных через extract().
  4. Выполняется через eval().
  5. Возвращается изменённый $data.

PHP-теги

Можно писать скрипты как с <?php, так и без — runner их удаляет.

Различия контекстов

execute() — automation-хуки:

php
// Всегда доступны:
$data   // модифицируемый массив данных записи
$old    // данные до изменения (или null)
$user   // текущий пользователь
$api    // PublicApiHelper

executeIncomingAction() — IncomingAction:

php
$incoming  // данные входящего сообщения
$data      // формируемые данные для записи
$api       // PublicApiHelper
// Чёрный список НЕ применяется!

IncomingAction скрипты

В скриптах IncomingAction чёрный список не проверяется. Ограничивайте доступ к настройке IncomingAction через права.


$api — PublicApiHelper: полный справочник

Чтение данных

$api->get(string $tableName, $id): ?array

Получить запись по ID.

php
$record = $api->get('it_requests', 42);
// ['id' => 42, 'name' => 'VPN не работает', 'status' => 'new', ...]
// null — если запись не найдена или $id пустой

Работает как со стандартными таблицами (users, sys_companies, sys_org_units, sys_roles), так и с динамическими. Для динамических декодирует JSON из колонки data и добавляет id, created_at, updated_at, created_by, updated_by.

$api->find(string $tableName, string $field, $value): ?array

Найти одну запись по значению поля.

php
$user = $api->find('users', 'email', 'ivan@example.com');
$category = $api->find('it_categories', 'name', 'Сеть');

Для динамических таблиц использует PostgreSQL-синтаксис data->>'field' = ?.

$api->getRelated(int $relationId, string $tablePosition, $instanceId): array

Получить связанные записи (M2M).

php
// tablePosition: 'l_table' или 'r_table' — позиция текущей таблицы в связи
$relatedTasks = $api->getRelated($relationId, 'l_table', $projectId);

$api->getRelatedByTable(string $sourceTable, string $targetTable, $instanceId, ?string $relationName = null): array

Удобная обёртка — не нужно знать ID связи:

php
$contacts = $api->getRelatedByTable('crm_clients', 'crm_contacts', $clientId);

$api->findRelationInfo(string $sourceTable, string $targetTable, ?string $relationName = null): ?array

Найти метаданные связи. Возвращает ['relation_id', 'position', 'relation'] или null.

$api->getEntityRelations(string $tableName): array

Все связи, в которых участвует таблица.


Изменение данных

$api->update(string $tableName, int $id, array $data): bool

Обновить запись.

php
$api->update('it_requests', $entityId, [
    'status'     => 'resolved',
    'resolution' => 'Обновлён драйвер'
]);

Только стандартные таблицы

update() работает только со стандартными таблицами (users, sys_companies и т.д.). Для динамических сущностей (кастомные таблицы) используйте прямой вызов через BPMN serviceTask или update_field_* свойство, либо создавайте отдельный API-запрос. В контексте BPMN scriptTask$api->update() на динамической таблице вернёт false.

Обновление динамических записей из scriptTask: движок делает это через прямой SQL-запрос внутри executeNode() при обработке serviceTask. В scriptTask для обновления используйте свойство update_field_* вместо $api->update().

$api->addRelated(int $relationId, string $tablePosition, $instanceId, array $relatedIds): bool

Привязать записи к связи.

php
$api->addRelated($relationId, 'l_table', $projectId, [5, 7, 12]);

Проверяет дубликаты перед вставкой. Возвращает false при отсутствии связи/таблицы или если ничего не вставлено.

$api->addRelatedByTable(string $sourceTable, string $targetTable, $instanceId, array $relatedIds, ?string $relationName = null): bool

Удобная обёртка с именами таблиц:

php
$api->addRelatedByTable('crm_clients', 'crm_contacts', $clientId, [8, 9]);

$api->unlinkRelated(int $relationId, string $tablePosition, $instanceId, $targetId): bool

Удалить одну запись из связи.

$api->unlinkRelatedByTable(string $sourceTable, string $targetTable, $instanceId, $targetId, ?string $relationName = null): bool

Обёртка с именами таблиц.


Уведомления

$api->sendPushNotification(string $title, string $message, string $route, array $userIds, string $category = 'system'): void

Внутреннее уведомление (колокольчик 🔔).

php
$api->sendPushNotification(
    'Заявка решена',
    'Пожалуйста, подтвердите решение.',
    '/e/it_requests/' . $entityId,
    [$record['requested_by']],
    'system'   // 'system' или 'message'
);

category = 'message' — попадает в ленту сообщений, а не системных оповещений.

$api->sendTelegram(string $message, array $userIds, int|string|null $channel = null): void

Telegram-сообщение пользователям.

php
// бот по умолчанию
$api->sendTelegram('Новая заявка требует рассмотрения', [3, 5]);

// конкретный бот при нескольких TG-каналах — по названию или id канала
$api->sendTelegram('VIP-заявка', [3], 'Support Bot');
$api->sendTelegram('VIP-заявка', [3], 7);

Отправляется только пользователям, у которых заполнено telegram_chat_id и включено notify_via_telegram. Уходит через исходящий Telegram-канал (раздел «Каналы» → «Исходящие») с ретраями и историей доставки: без третьего аргумента — канал по умолчанию, иначе указанный по названию (строка) или id (число). Если запрошенный канал не найден/неактивен — сообщение не отправляется (ошибка в лог), чтобы не уйти через чужого бота. Fallback на legacy-токен настроек срабатывает только когда default-канала нет вовсе.

$api->replyTelegram(string $chatId, string $text, ?int $channel = null): void

Прямой ответ в конкретный чат (не требует поиска пользователя по ID).

php
$api->replyTelegram($incoming['from_id'], 'Ваша заявка принята в обработку.');

В скриптах входящих действий ответ по умолчанию уходит через того же бота, который принял сообщение (канал берётся из $incoming), — важно при нескольких ботах ТГ. Явно переопределить бота можно третьим аргументом — id входящего telegram-канала. Таймаут: 5 секунд.

$api->sendEmail(string $templateCode, $to, array $params = [], int|string|null $channel = null): bool

Email по шаблону.

php
$api->sendEmail('ticket_resolved', 'user@example.com', [
    'ticket_name' => $record['name'],
    'resolution'  => $record['resolution'],
]);

// через конкретный SMTP-канал (по названию или id) при нескольких почтовых каналах
$api->sendEmail('invoice_ready', $record['email'], $params, 'Billing');

$to — строка/user id или массив того и другого. Шаблон ищется по code в таблице sys_notification_templates. Плейсхолдеры заменяются значениями $params. Четвёртый аргумент $channel выбирает конкретный SMTP-канал (по умолчанию — default smtp-канал тенанта). Возвращает false при ошибке (также логируется).

$api->sendToChannel(int|string $channel, array $envelope): bool

Низкоуровневая отправка в любой исходящий канал — webhook, kafka, amqp, sms, а также telegram/smtp напрямую. Нужен, когда получатель — не пользователь системы (внешний вебхук, произвольный chat_id/телефон) или требуется payload-канал.

php
// в канал по id
$api->sendToChannel(7, [
    'recipients' => ['79991234567'],           // адреса; для Telegram — chat_id
    'body'       => 'Заявка #' . $data['id'] . ' в работе',
]);

// в канал по умолчанию заданного типа
$api->sendToChannel('webhook', [
    'payload' => ['event' => 'ticket.updated', 'id' => $data['id']],
]);

$channel — id канала или его тип для канала по умолчанию. $envelope: recipients (адреса или ['address' => ..., 'user_id' => ...]), subject, body, payload. В отличие от sendTelegram/sendEmail, получателей резолвит не система — chat_id/адреса передаются как есть.

Для типовых каналов удобнее специализированные методы ниже — у каждого свой естественный набор аргументов.

$api->sendSms(string|array $phones, string $text, int|string|null $channel = null): bool

SMS на один номер или массив номеров (fanout — каждому своя доставка). $channel — конкретный SMS-канал (id/название) при нескольких провайдерах, null → канал по умолчанию типа sms.

php
$api->sendSms($record['phone'], "Код подтверждения: 1234");
$api->sendSms(['79990000001', '79990000002'], 'Массовая рассылка', 'SMSC-резерв');

$api->sendWebhook(array $payload, int|string|null $channel = null): bool

POST JSON во внешний HTTP-вебхук (HMAC-подпись, auth и URL настроены на канале). null → канал по умолчанию типа webhook.

php
$api->sendWebhook([
    'event' => 'ticket.created',
    'id'    => $data['id'],
    'title' => $data['name'],
]);

$api->publish(array $payload, int|string $channel, ?string $key = null): bool

Публикация payload в брокер сообщений (Kafka / AMQP) или любой payload-канал. $channel обязателен — id канала или его тип (единого «default» среди брокеров нет). $key — ключ партиции Kafka (по умолчанию — id привязанной записи, что даёт стабильный порядок событий записи).

php
$api->publish(['id' => $data['id'], 'status' => $data['status']], 'kafka');
$api->publish($payload, 12, key: $data['order_no']);   // конкретный канал + свой ключ
Тип каналаМетодКлючевые аргументы
telegramsendTelegramтекст, user id, бот
smtpsendEmailшаблон, адреса, канал
smssendSmsномера, текст, канал
webhooksendWebhookpayload, канал
kafka / amqppublishpayload, канал, ключ
любойsendToChannelканал, конверт

Состояние Telegram-бота (для пошаговых диалогов)

php
$api->setBotState(string $chatId, string $state): void  // TTL: 3600 сек
$api->getBotState(string $chatId): ?string
$api->clearBotState(string $chatId): void

Пример пошагового диалога в IncomingAction-скрипте:

php
$state = $api->getBotState($incoming['from_id']);

if ($state === 'waiting_description') {
    $data['details'] = $incoming['body'];
    $data['name']    = $api->getBotState($incoming['from_id'] . '_name');
    $api->clearBotState($incoming['from_id']);
    $api->clearBotState($incoming['from_id'] . '_name');
    // $data готов — создаём запись

} elseif ($state === 'waiting_name') {
    $api->setBotState($incoming['from_id'] . '_name', $incoming['body']);
    $api->setBotState($incoming['from_id'], 'waiting_description');
    $api->replyTelegram($incoming['from_id'], 'Опишите проблему подробнее:');
    return; // не создаём запись

} else {
    $api->setBotState($incoming['from_id'], 'waiting_name');
    $api->replyTelegram($incoming['from_id'], 'Введите тему обращения:');
    return;
}

Утилиты

php
$api->now(): Carbon                  // Carbon::now()
$api->parseDate(string $date): Carbon // Carbon::parse($date)
$api->log($message): void            // Пишет в laravel.log с префиксом [Automation Log]
                                     // Массивы/объекты — JSON_UNESCAPED_UNICODE

Условия в BPMN Gateway (ExpressionLanguage)

Symfony ExpressionLanguage. Переменные — все ключи из process_variables.

Синтаксис

${переменная оператор значение}

Поддерживаются ${} и #{}.

Операторы

ТипСинтаксис
Равенство== !=
Сравнение> < >= <=
Логикаand or not (или && || !)
Строкиstatus == 'approved'
null-проверкаassignee == null

Примеры

${approved}
${approved == true}
${status == 'won'}
${amount > 100000}
${priority == 'critical' or priority == 'high'}
${approved and not rejected}
${assignee != null}
${stage == 'negotiation' and amount >= 500000}

Булевы переменные

${approved} и ${approved == true} эквивалентны, если approved — это true или false.

Ошибки в условиях

При ошибке разбора условия evaluateCondition() возвращает false и логирует исключение. Процесс не падает, но ветка не выбирается — убедитесь в корректности атрибута default на gateway.

Документация Orbita ITSM