Битрикс24: запуск бизнес-процессов через API

Программный запуск бизнес-процессов для сделок, лидов, контактов и других сущностей CRM. Передача параметров и обработка ошибок.

Дмитрий Мещеряков
Дмитрий Мещеряков
📅 20 октября 2022 г.📖 7 мин чтения

Программный запуск бизнес-процессов — та часть Битрикс24, где документация заканчивается на сигнатуре метода, а всё остальное выясняется опытным путём. Разберём CBPDocument::StartWorkflow() по аргументам, и отдельно — три вещи, которые определяют, будет ли это работать в продакшене: контекст прав, асинхронность и защита от повторных запусков.

Зачем запускать бизнес-процессы программно

Бизнес-процессы в Битрикс24 можно запускать не только вручную, но и программно через API. Это позволяет:

  • Автоматизировать запуск процессов при определённых событиях
  • Передавать параметры в процесс динамически
  • Интегрировать БП с внешними системами

Базовый запуск процесса

php
<?php
use Bitrix\Main\Loader;

Loader::includeModule('bizproc');
Loader::includeModule('crm');

$workflowTemplateId = 423;  // ID шаблона бизнес-процесса
$dealId = 962558;           // ID сделки

$errors = [];

$workflowId = \CBPDocument::StartWorkflow(
    $workflowTemplateId,
    ['crm', 'CCrmDocumentDeal', 'DEAL_' . $dealId],
    [
        // Параметры для процесса
        'phone' => '+79991234567',
    ],
    $errors
);

if (!empty($errors)) {
    foreach ($errors as $error) {
        echo "[{$error['code']}] {$error['message']}\n";
    }
} else {
    echo "Запущен процесс: {$workflowId}\n";
}
💡 Совет

Три элемента во втором аргументе — это адрес документа, и путать их местами получается легко. ['crm', 'CCrmDocumentDeal', 'DEAL_962558']: модуль, класс документа, идентификатор сущности с типовым префиксом. Префикс обязателен — '962558' вместо 'DEAL_962558' даёт ошибку «документ не найден», а не подсказку.

Проверить, что адрес собран правильно, проще всего запуском того же процесса вручную из интерфейса: если руками процесс стартует, а кодом нет — дело почти всегда в этом аргументе.

Типы документов CRM

СущностьКласс документаФормат ID
СделкаCCrmDocumentDealDEAL_123
ЛидCCrmDocumentLeadLEAD_123
КонтактCCrmDocumentContactCONTACT_123
КомпанияCCrmDocumentCompanyCOMPANY_123
СчётCCrmDocumentInvoiceINVOICE_123
Смарт-процессCCrmDocumentSmartInvoiceSMART_INVOICE_123

Класс для работы с бизнес-процессами

php
<?php

namespace Local\Bizproc;

use Bitrix\Main\Loader;

class WorkflowLauncher
{
    /**
     * Типы документов CRM
     */
    private const DOCUMENT_TYPES = [
        'deal' => ['crm', 'CCrmDocumentDeal', 'DEAL_'],
        'lead' => ['crm', 'CCrmDocumentLead', 'LEAD_'],
        'contact' => ['crm', 'CCrmDocumentContact', 'CONTACT_'],
        'company' => ['crm', 'CCrmDocumentCompany', 'COMPANY_'],
        'invoice' => ['crm', 'CCrmDocumentInvoice', 'INVOICE_'],
    ];

    public function __construct()
    {
        if (!Loader::includeModule('bizproc')) {
            throw new \RuntimeException('Модуль bizproc не установлен');
        }
        Loader::includeModule('crm');
    }

    /**
     * Запуск бизнес-процесса для сделки
     */
    public function startForDeal(int $templateId, int $dealId, array $params = []): WorkflowResult
    {
        return $this->start($templateId, 'deal', $dealId, $params);
    }

    /**
     * Запуск бизнес-процесса для лида
     */
    public function startForLead(int $templateId, int $leadId, array $params = []): WorkflowResult
    {
        return $this->start($templateId, 'lead', $leadId, $params);
    }

    /**
     * Запуск бизнес-процесса для контакта
     */
    public function startForContact(int $templateId, int $contactId, array $params = []): WorkflowResult
    {
        return $this->start($templateId, 'contact', $contactId, $params);
    }

    /**
     * Запуск бизнес-процесса для компании
     */
    public function startForCompany(int $templateId, int $companyId, array $params = []): WorkflowResult
    {
        return $this->start($templateId, 'company', $companyId, $params);
    }

    /**
     * Универсальный запуск процесса
     */
    public function start(int $templateId, string $entityType, int $entityId, array $params = []): WorkflowResult
    {
        if (!isset(self::DOCUMENT_TYPES[$entityType])) {
            throw new \InvalidArgumentException("Неизвестный тип сущности: {$entityType}");
        }

        $docType = self::DOCUMENT_TYPES[$entityType];
        $documentId = [$docType[0], $docType[1], $docType[2] . $entityId];

        $errors = [];
        
        $workflowId = \CBPDocument::StartWorkflow(
            $templateId,
            $documentId,
            $params,
            $errors
        );

        return new WorkflowResult($workflowId, $errors);
    }

    /**
     * Получение списка шаблонов БП для типа сущности
     */
    public function getTemplates(string $entityType): array
    {
        if (!isset(self::DOCUMENT_TYPES[$entityType])) {
            return [];
        }

        $docType = self::DOCUMENT_TYPES[$entityType];

        return \CBPWorkflowTemplateLoader::GetList(
            ['ID' => 'DESC'],
            [
                'DOCUMENT_TYPE' => $docType,
                'ACTIVE' => 'Y',
            ],
            false,
            false,
            ['ID', 'NAME', 'DESCRIPTION', 'MODIFIED']
        );
    }

    /**
     * Проверка статуса запущенного процесса
     */
    public function getWorkflowState(string $workflowId): ?array
    {
        $state = \CBPStateService::GetWorkflowState($workflowId);
        
        if (!$state) {
            return null;
        }

        return [
            'id' => $state['ID'],
            'template_id' => $state['WORKFLOW_TEMPLATE_ID'],
            'document_id' => $state['DOCUMENT_ID'],
            'state' => $state['STATE'],
            'state_title' => $state['STATE_TITLE'],
            'started' => $state['STARTED'],
            'modified' => $state['MODIFIED'],
        ];
    }

    /**
     * Остановка процесса
     */
    public function terminate(string $workflowId): bool
    {
        $errors = [];
        \CBPDocument::TerminateWorkflow($workflowId, [], $errors);
        
        return empty($errors);
    }
}

/**
 * Результат запуска процесса
 */
class WorkflowResult
{
    public readonly ?string $workflowId;
    public readonly array $errors;
    public readonly bool $success;

    public function __construct(?string $workflowId, array $errors)
    {
        $this->workflowId = $workflowId;
        $this->errors = $errors;
        $this->success = !empty($workflowId) && empty($errors);
    }

    public function getErrorMessages(): array
    {
        return array_map(
            fn($e) => "[{$e['code']}] {$e['message']}",
            $this->errors
        );
    }
}

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

Запуск при создании сделки

php
<?php
use Bitrix\Main\EventManager;
use Local\Bizproc\WorkflowLauncher;

EventManager::getInstance()->addEventHandler(
    'crm',
    'OnAfterCrmDealAdd',
    function ($fields) {
        // Запускаем БП только для сделок из определённого источника
        if ($fields['SOURCE_ID'] !== 'WEB') {
            return;
        }

        $launcher = new WorkflowLauncher();
        
        $result = $launcher->startForDeal(
            423, // ID шаблона
            $fields['ID'],
            [
                'notify_manager' => true,
                'priority' => 'high',
            ]
        );

        if (!$result->success) {
            AddMessage2Log(
                "Ошибка запуска БП: " . implode(', ', $result->getErrorMessages()),
                'bizproc'
            );
        }
    }
);

Запуск из REST-обработчика

php
<?php
// Обработчик входящего вебхука
$dealId = (int) $_POST['deal_id'];
$action = $_POST['action'] ?? '';

if ($action === 'approve') {
    $launcher = new WorkflowLauncher();
    
    $result = $launcher->startForDeal(
        450, // БП "Согласование"
        $dealId,
        [
            'approved_by' => $_POST['user_id'],
            'comment' => $_POST['comment'] ?? '',
        ]
    );

    echo json_encode([
        'success' => $result->success,
        'workflow_id' => $result->workflowId,
        'errors' => $result->getErrorMessages(),
    ]);
}

Получение списка доступных БП

php
<?php
$launcher = new WorkflowLauncher();
$templates = $launcher->getTemplates('deal');

echo "Доступные бизнес-процессы для сделок:\n";
while ($template = $templates->Fetch()) {
    echo "  [{$template['ID']}] {$template['NAME']}\n";
}

Передача параметров в БП

Параметры передаются третьим аргументом в StartWorkflow(). Они доступны в процессе через переменные:

php
$params = [
    'phone' => '+79991234567',        // Строка
    'amount' => 15000.50,             // Число
    'send_email' => true,             // Да/Нет
    'manager_ids' => [1, 5, 8],       // Множественное значение
    'deadline' => '2026-12-31',       // Дата
];

В редакторе БП создайте параметры с соответствующими кодами и типами.

Что ломается при запуске БП в продакшене

Запуск идёт от имени текущего пользователя. Если код выполняется в контексте посетителя сайта, агента или консольного скрипта, прав на документ у него может не быть — и вы получите ACCESS_DENIED там, где вручную всё работает. Совет «запускайте от имени администратора» верен, но с оговоркой: подмена пользователя должна быть явной и локальной, а не постоянной авторизацией администратора в фоновом скрипте.

Процесс запускается асинхронно. StartWorkflow() возвращает идентификатор запущенного экземпляра, а не результат работы: сам процесс отработает позже, шагами. Код вида «запустили процесс и сразу читаем изменённое им поле сделки» будет иногда работать, а иногда нет — в зависимости от того, что успело выполниться. Это самый неприятный класс ошибок, потому что на тестовых данных он не воспроизводится.

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

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

Типичные ошибки при запуске:

КодОписаниеРешение
BPWT001Шаблон не найденПроверьте ID шаблона
BPWF001Документ не найденПроверьте ID сущности
ACCESS_DENIEDНет правЗапустите от имени администратора
💡 Совет

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

⚠️ Важно

Массовый запуск требует отдельного подхода. Экземпляр бизнес-процесса — это записи в нескольких таблицах плюс очередь шагов, которые будут выполняться на хитах или по расписанию. Запуск процесса для тысячи сделок одной командой не «займёт ресурсы», а положит портал: очередь бизнес-процессов забьётся, и остановить это будет сложнее, чем запустить.

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

Итоги

Проверяйте адрес документа первым делом — большинство ошибок «шаблон не найден» и «документ не найден» именно здесь.

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

Не рассчитывайте на синхронность. Процесс запущен — не значит выполнен. Всё, что должно произойти после его завершения, делайте внутри процесса или по его событию.

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

💡 Совет

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

Частые вопросы

Как запустить бизнес-процесс Битрикс24 из кода?

Методом CBPDocument::StartWorkflow. Первым аргументом идёт тип процесса, вторым — адрес документа массивом из трёх элементов: модуль, класс документа и идентификатор сущности с типовым префиксом. Третьим передаются параметры процесса, четвёртым — массив под ошибки. Метод возвращает идентификатор запущенного экземпляра.

Почему при запуске БП возникает ошибка «документ не найден»?

Чаще всего из-за неверно собранного адреса документа. Идентификатор нужен с типовым префиксом: DEAL_962558, а не 962558, и класс должен соответствовать сущности — CCrmDocumentDeal для сделки, CCrmDocumentLead для лида, CCrmDocumentContact для контакта, CCrmDocumentCompany для компании. Проверить, что адрес собран правильно, проще всего запуском того же процесса вручную из интерфейса: если руками стартует, а кодом нет — дело почти всегда в этом аргументе.

Почему появляется ACCESS_DENIED при программном запуске?

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

Можно ли сразу после запуска прочитать результат процесса?

Нет. Процесс запускается асинхронно: метод возвращает идентификатор запущенного экземпляра, а не результат работы, и сам процесс отработает позже, шагами. Код вида «запустили процесс и сразу читаем изменённое им поле сделки» будет иногда работать, а иногда нет — в зависимости от того, что успело выполниться. Это самый неприятный класс ошибок, потому что на тестовых данных он не воспроизводится.

Как защититься от повторного запуска процесса?

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

Как отлаживать запуск бизнес-процессов?

Включить логирование бизнес-процессов в настройках модуля — логи покажут, на каком этапе возникла ошибка. Типовые коды: BPWT001 означает, что не найден шаблон (проверьте идентификатор шаблона), BPWF001 — не найден документ (проверьте адрес сущности), ACCESS_DENIED — недостаточно прав у пользователя, от имени которого идёт запуск.