Как завершить бизнес-процессы уволенного сотрудника
Scope:
user_brief, user_basic, user, bizprocКто может выполнять методы: администратор
Выберите инструмент для разработки с AI-агентом:
- используйте Битрикс24 Вайбкод, чтобы создать приложение для Битрикс24 по описанию задачи без знания языков программирования. Агент напишет код и разместит приложение на сервере без ручной настройки хостинга
- используйте MCP-сервер, чтобы разрабатывать интеграцию через REST API в своем проекте. Агент будет обращаться к официальной REST-документации
При увольнении сотрудника в Битрикс24 могут остаться незавершенные бизнес-процессы, за которые он был ответственен.
Чтобы завершить активные бизнес-процессы уволенного сотрудника, последовательно выполним три метода:
-
user.get — получим
IDуволенного сотрудника -
bizproc.task.list — получим список заданий процессов, за которые отвечает уволенный сотрудник
-
bizproc.workflow.kill — завершим бизнес-процессы с удалением данных. Если нужно сохранить факт запуска бизнес-процесса, используйте метод bizproc.workflow.terminate. Оба метода вызываются одинаково
1. Получим ID уволенного сотрудника
Используем метод user.get с фильтром:
-
NAME— укажем имя сотрудника -
LAST_NAME— укажем фамилию сотрудника -
ACTIVE— параметр регулирует поиск по активным или уволенным сотрудникам. Если параметр не передавать, поиск будет идти по всем сотрудникам вне зависимости от их статуса. Укажем0для поиска только среди уволенных сотрудников
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl('https://your-domain.bitrix24.ru/rest/1/xxxxxxxxxxxxxxxx/')
const response = await $b24.actions.v2.call.make({
method: 'user.get',
params: {
filter: {
NAME: "employee's name",
LAST_NAME: "employee's last name",
ACTIVE: 0,
},
},
requestId: 'user-get',
})
const users = response.getData().result
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('b24');
$log->pushHandler(new StreamHandler('php://stdout'));
$b24 = (new ServiceBuilderFactory(new EventDispatcher(), $log))
->initFromWebhook('https://your-domain.bitrix24.ru/rest/1/xxxxxxxxxxxxxxxx/');
$users = $b24->getUserScope()->user()->get(
[],
[
'NAME' => "employee's name",
'LAST_NAME' => "employee's last name",
'ACTIVE' => 0,
]
)->getUsers();
from b24pysdk import BitrixWebhook, Client
token = BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
client = Client(token)
result = client.user.get(
filter={
"NAME": "employee's name",
"LAST_NAME": "employee's last name",
"ACTIVE": 0,
}
).response.result
// ACTIVE: 0 отбирает только уволенных. Без этого параметра поиск идёт по
// всем сотрудникам независимо от статуса. Имя и фамилию задают константы
// departedName и departedLastName в начале файла; пустые значения в фильтр
// не кладём — по пустой строке не найдётся никто.
filter := b24.Params{"ACTIVE": 0}
if departedName != "" {
filter["NAME"] = departedName
}
if departedLastName != "" {
filter["LAST_NAME"] = departedLastName
}
res, err := core.Call(ctx, "user.get", b24.Params{"filter": filter}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("user.get: %w", err)
}
// user.get отвечает в UPPER_SNAKE и присылает идентификатор СТРОКОЙ ("29"):
// b24.ID разбирает и число, и строку с числом.
var users []struct {
ID b24.ID `json:"ID"`
Name string `json:"NAME"`
LastName string `json:"LAST_NAME"`
}
if err := json.Unmarshal(res.Result, &users); err != nil {
return fmt.Errorf("разбор сотрудников: %w", err)
}
В результате получим ID уволенного сотрудника.
{
"result": [
{
"ID": "29",
"ACTIVE": false,
"NAME": "employee's name",
"LAST_NAME": "employee's last name",
"EMAIL": "employee_email@gmail.com",
"WORK_POSITION": "Менеджер",
"UF_DEPARTMENT": [
7,
1
],
"USER_TYPE": "employee"
}
],
"total": 1,
}
2. Получим список заданий процессов, за которые отвечает уволенный сотрудник
Используем метод bizproc.task.list с фильтром:
-
USER_ID— идентификатор сотрудника, передаем ID, полученный на шаге 1 -
STATUS— параметр отвечает за статус заданий, укажем0для отбора только невыполненных заданий
const response = await $b24.actions.v2.call.make({
method: 'bizproc.task.list',
params: {
filter: {
USER_ID: 29,
STATUS: 0,
},
},
requestId: 'bizproc-task-list',
})
const tasks = response.getData().result
$tasks = $b24->getBizProcScope()->task()->list(
[],
[
'USER_ID' => 29,
'STATUS' => 0,
]
)->getTasks();
result = client.bizproc.task.list(
filter={
"USER_ID": 29,
"STATUS": 0,
}
).response.result
// STATUS: 0 — только невыполненные задания.
res, err := core.Call(ctx, "bizproc.task.list", b24.Params{
"filter": b24.Params{"USER_ID": userID, "STATUS": 0},
}, b24.WithIdempotent())
if err != nil {
// bizproc.* доступен только администратору портала и только на
// платных тарифах. Код сравнивается через errors.Is, а не строкой:
// опечатка в литерале скомпилируется и молча уведёт в другую ветку.
if errors.Is(err, b24.ErrMethodNotFound) {
return fmt.Errorf("модуль бизнес-процессов недоступен на этом портале: %w", err)
}
return fmt.Errorf("bizproc.task.list: %w", err)
}
// WORKFLOW_ID — это НЕ число: "67e3db8e581121.72266518". Разбор его в
// int уничтожает значение, поэтому поле остаётся строкой от списка до
// команды завершения.
var tasks []struct {
ID b24.ID `json:"ID"`
WorkflowID string `json:"WORKFLOW_ID"`
Name string `json:"NAME"`
DocumentName string `json:"DOCUMENT_NAME"`
}
if err := json.Unmarshal(res.Result, &tasks); err != nil {
return fmt.Errorf("разбор заданий: %w", err)
}
В результате получим список невыполненных заданий. У каждого задания есть параметр WORKFLOW_ID — это ID бизнес-процесса, который мы завершим в следующем шаге.
{
"result": [
{
"ENTITY": "CCrmDocumentContact",
"DOCUMENT_ID": "CONTACT_2437",
"ID": "879",
"WORKFLOW_ID": "67e3db8e581121.72266518",
"DOCUMENT_NAME": "widget contact",
"NAME": "Адрес",
"DOCUMENT_URL": "/crm/contact/details/2437/"
}
],
"total": 1,
}
3. Завершим бизнес-процессы
Используем метод bizproc.workflow.kill с параметром:
ID— идентификатор процесса, передаемWORKFLOW_ID, полученный на шаге 2
const response = await $b24.actions.v2.call.make({
method: 'bizproc.workflow.kill',
params: { ID: '67e3db8e581121.72266518' },
requestId: 'bizproc-workflow-kill',
})
const isKilled = response.getData().result
$isKilled = $b24->getBizProcScope()->workflow()
->kill('67e3db8e581121.72266518')
->isSuccess();
# ID бизнес-процесса — строка, а типизированный client.bizproc.workflow.kill
# ожидает int, поэтому вызываем метод напрямую через token.call_method
result = token.call_method(
"bizproc.workflow.kill",
{"ID": "67e3db8e581121.72266518"},
)
res, err := core.Call(ctx, "bizproc.workflow.kill", b24.Params{"ID": workflowID})
if err != nil {
// Завершить уже завершившийся процесс нельзя — это не отказ
// сценария, остальные процессы завершить всё равно надо.
fmt.Fprintf(os.Stderr, "процесс %s: %v\n", workflowID, err)
continue
}
// Ответ — голое булево, а не объект.
var killed bool
if err := json.Unmarshal(res.Result, &killed); err != nil {
return fmt.Errorf("разбор ответа bizproc.workflow.kill: %w", err)
}
fmt.Printf("процесс %s завершён: %v\n", workflowID, killed)
В b24pysdk типизированный метод client.bizproc.workflow.kill(bitrix_id=...) ожидает целочисленный bitrix_id, а идентификатор бизнес-процесса — строка вида 67e3db8e581121.72266518. Поэтому для завершения процесса используйте универсальный вызов token.call_method("bizproc.workflow.kill", {"ID": workflow_id}), где token — объект BitrixWebhook.
В результате получим true, удаление процесса прошло успешно. Если вы получили ошибку error, изучите описание возможных ошибок в документации метода bizproc.workflow.kill.
{
"result": true,
}
Пример кода
В примере все найденные процессы удаляются в цикле. Если вам требуется удалить большой объем данных, вы можете столкнуться с лимитами на выполнение запросов. Чтобы оптимизировать код под ваш объем работы, используйте рекомендации раздела Производительность.
// npm install @bitrix24/b24jssdk
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl('https://your-domain.bitrix24.ru/rest/1/xxxxxxxxxxxxxxxx/')
async function getUserId(firstName, lastName) {
const response = await $b24.actions.v2.call.make({
method: 'user.get',
params: { filter: { NAME: firstName, LAST_NAME: lastName, ACTIVE: 0 } },
requestId: 'user-get',
})
if (!response.isSuccess) throw new Error(response.getErrorMessages().join('; '))
const users = response.getData().result
return users.length ? users[0].ID : null
}
async function getWorkflowIds(userId) {
const response = await $b24.actions.v2.call.make({
method: 'bizproc.task.list',
params: { filter: { USER_ID: userId, STATUS: 0 } },
requestId: 'bizproc-task-list',
})
if (!response.isSuccess) throw new Error(response.getErrorMessages().join('; '))
return response.getData().result.map((task) => task.WORKFLOW_ID)
}
async function killWorkflows(workflowIds) {
for (const workflowId of workflowIds) {
const response = await $b24.actions.v2.call.make({
method: 'bizproc.workflow.kill',
params: { ID: workflowId },
requestId: `kill-${workflowId}`,
})
console.log(response.isSuccess
? `Workflow ${workflowId} завершен успешно.`
: `Ошибка: ${response.getErrorMessages().join('; ')}`)
}
}
// Имя и фамилия сотрудника передаются аргументами: node kill.mjs Иван Иванов
const [firstName, lastName] = process.argv.slice(2)
const userId = await getUserId(firstName, lastName)
if (userId) {
await killWorkflows(await getWorkflowIds(userId))
}
$b24.destroy()
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Bitrix24\SDK\Services\ServiceBuilder;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('b24');
$log->pushHandler(new StreamHandler('php://stdout'));
$b24 = (new ServiceBuilderFactory(new EventDispatcher(), $log))
->initFromWebhook('https://your-domain.bitrix24.ru/rest/1/xxxxxxxxxxxxxxxx/');
function getUserId(ServiceBuilder $b24, string $firstName, string $lastName): ?int
{
$users = $b24->getUserScope()->user()->get(
[],
['NAME' => $firstName, 'LAST_NAME' => $lastName, 'ACTIVE' => 0]
)->getUsers();
return $users === [] ? null : $users[0]->ID;
}
function getWorkflowIds(ServiceBuilder $b24, int $userId): array
{
$tasks = $b24->getBizProcScope()->task()->list(
[],
['USER_ID' => $userId, 'STATUS' => 0]
)->getTasks();
return array_map(static fn($task) => $task->WORKFLOW_ID, $tasks);
}
function killWorkflows(ServiceBuilder $b24, array $workflowIds): void
{
foreach ($workflowIds as $workflowId) {
$isKilled = $b24->getBizProcScope()->workflow()->kill($workflowId)->isSuccess();
echo $isKilled
? "Workflow {$workflowId} завершен успешно.\n"
: "Ошибка при удалении процесса {$workflowId}\n";
}
}
$firstName = readline('Введите имя сотрудника: ');
$lastName = readline('Введите фамилию сотрудника: ');
$userId = getUserId($b24, $firstName, $lastName);
if ($userId !== null) {
killWorkflows($b24, getWorkflowIds($b24, $userId));
}
from typing import Optional
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError
def get_user_id(client, first_name: str, last_name: str) -> Optional[int]:
try:
users = client.user.get(
filter={
"NAME": first_name,
"LAST_NAME": last_name,
"ACTIVE": 0,
},
).response.result
except BitrixAPIError as error:
print(f"Ошибка: {error}")
return None
if not users:
return None
return int(users[0]["ID"])
def get_user_tasks(client, user_id: int) -> list[str]:
tasks = client.bizproc.task.list(
filter={
"USER_ID": user_id,
"STATUS": 0,
},
).response.result
return [task["WORKFLOW_ID"] for task in tasks]
def kill_workflows(token, workflow_ids: list[str]) -> None:
# ID процесса — строка, поэтому используем универсальный token.call_method,
# а не типизированный client.bizproc.workflow.kill (он ожидает int)
for workflow_id in workflow_ids:
try:
token.call_method("bizproc.workflow.kill", {"ID": workflow_id})
except BitrixAPIError as error:
print(f"Ошибка: {error}")
else:
print(f"Workflow {workflow_id} завершен успешно.")
def process_employee_tasks(client, token, first_name: str, last_name: str) -> None:
user_id = get_user_id(client, first_name, last_name)
if user_id is None:
return
workflow_ids = get_user_tasks(client, user_id)
kill_workflows(token, workflow_ids)
token = BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
client = Client(token)
first_name = input("Введите имя сотрудника: ")
last_name = input("Введите фамилию сотрудника: ")
process_employee_tasks(client, token, first_name, last_name)
// Подготовка в пустом каталоге — go get без go mod init не сработает:
//
// go mod init example && go get github.com/bitrix24/b24gosdk
//
// Запуск:
//
// export B24_WEBHOOK_URL='https://ваш-портал.bitrix24.ru/rest/1/токен/' && go run .
//
// Пример самодостаточный и безопасный: он создаёт СВОЮ сделку, завершает
// только те процессы, которые запустились на ней, и удаляет сделку за собой.
// Уволенного сотрудника создать и «уволить» нельзя, поэтому шаги 1 и 2 он
// выполняет по-настоящему и показывает, что нашёл, а завершает — своё.
// Запускается на любом портале, ничего править не нужно.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"time"
b24 "github.com/bitrix24/b24gosdk"
)
// Имя и фамилия уволенного сотрудника для шага 1. Пустые значения означают
// «все уволенные»: на чужом портале конкретного человека не найти, а пример
// должен запускаться везде без правок.
const (
departedName = ""
departedLastName = ""
)
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
// Путь вебхука — это секрет, поэтому он приходит из окружения, а не из кода.
core := b24.NewClient(os.Getenv("B24_WEBHOOK_URL")).Core()
// --- подготовка: своя сделка и запущенные на ней процессы
dealID, err := addDeal(ctx, core)
if err != nil {
return err
}
// Удаление сделки уносит и незавершённые процессы по ней.
defer del(ctx, core, "crm.deal.delete", b24.Params{"id": dealID})
// Процессы стартуют не мгновенно.
time.Sleep(3 * time.Second)
mine, err := workflowsOfDeal(ctx, core, dealID)
if err != nil {
return err
}
fmt.Printf("на сделке %d запущено процессов: %d\n", dealID, len(mine))
// --- шаг 1: идентификатор уволенного сотрудника
// ACTIVE: 0 отбирает только уволенных. Без этого параметра поиск идёт по
// всем сотрудникам независимо от статуса. Имя и фамилию задают константы
// departedName и departedLastName в начале файла; пустые значения в фильтр
// не кладём — по пустой строке не найдётся никто.
filter := b24.Params{"ACTIVE": 0}
if departedName != "" {
filter["NAME"] = departedName
}
if departedLastName != "" {
filter["LAST_NAME"] = departedLastName
}
res, err := core.Call(ctx, "user.get", b24.Params{"filter": filter}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("user.get: %w", err)
}
// user.get отвечает в UPPER_SNAKE и присылает идентификатор СТРОКОЙ ("29"):
// b24.ID разбирает и число, и строку с числом.
var users []struct {
ID b24.ID `json:"ID"`
Name string `json:"NAME"`
LastName string `json:"LAST_NAME"`
}
if err := json.Unmarshal(res.Result, &users); err != nil {
return fmt.Errorf("разбор сотрудников: %w", err)
}
fmt.Printf("уволенных сотрудников найдено: %d\n", len(users))
// --- шаг 2: задания процессов, за которые отвечает сотрудник
// Если уволенных нет, спрашиваем задания текущего пользователя: сам вызов
// от этого не меняется, а сценарий остаётся выполнимым на любом портале.
targets := make([]b24.ID, 0, len(users))
for _, u := range users {
targets = append(targets, u.ID)
}
if len(targets) == 0 {
me, err := currentUser(ctx, core)
if err != nil {
return err
}
targets = append(targets, me)
}
for _, userID := range targets {
// STATUS: 0 — только невыполненные задания.
res, err := core.Call(ctx, "bizproc.task.list", b24.Params{
"filter": b24.Params{"USER_ID": userID, "STATUS": 0},
}, b24.WithIdempotent())
if err != nil {
// bizproc.* доступен только администратору портала и только на
// платных тарифах. Код сравнивается через errors.Is, а не строкой:
// опечатка в литерале скомпилируется и молча уведёт в другую ветку.
if errors.Is(err, b24.ErrMethodNotFound) {
return fmt.Errorf("модуль бизнес-процессов недоступен на этом портале: %w", err)
}
return fmt.Errorf("bizproc.task.list: %w", err)
}
// WORKFLOW_ID — это НЕ число: "67e3db8e581121.72266518". Разбор его в
// int уничтожает значение, поэтому поле остаётся строкой от списка до
// команды завершения.
var tasks []struct {
ID b24.ID `json:"ID"`
WorkflowID string `json:"WORKFLOW_ID"`
Name string `json:"NAME"`
DocumentName string `json:"DOCUMENT_NAME"`
}
if err := json.Unmarshal(res.Result, &tasks); err != nil {
return fmt.Errorf("разбор заданий: %w", err)
}
fmt.Printf("сотрудник %d, невыполненных заданий: %d\n", userID, len(tasks))
for _, t := range tasks {
fmt.Printf(" задание %d %q, процесс %s (%s)\n",
t.ID, t.Name, t.WorkflowID, t.DocumentName)
}
}
// --- шаг 3: завершаем процессы
// На боевом портале сюда подставляют WORKFLOW_ID из шага 2. Пример
// ограничивается процессами СВОЕЙ сделки: он не имеет права завершить
// чужой процесс на вашем портале.
if len(mine) == 0 {
fmt.Println("на сделке примера процессов не запустилось — завершать нечего")
return nil
}
for _, workflowID := range mine {
res, err := core.Call(ctx, "bizproc.workflow.kill", b24.Params{"ID": workflowID})
if err != nil {
// Завершить уже завершившийся процесс нельзя — это не отказ
// сценария, остальные процессы завершить всё равно надо.
fmt.Fprintf(os.Stderr, "процесс %s: %v\n", workflowID, err)
continue
}
// Ответ — голое булево, а не объект.
var killed bool
if err := json.Unmarshal(res.Result, &killed); err != nil {
return fmt.Errorf("разбор ответа bizproc.workflow.kill: %w", err)
}
fmt.Printf("процесс %s завершён: %v\n", workflowID, killed)
}
// bizproc.workflow.terminate останавливает процесс, но СОХРАНЯЕТ запись о
// нём; kill удаляет процесс вместе с данными. Вызываются они одинаково.
return nil
}
// --- вспомогательное: подготовка данных и уборка
func addDeal(ctx context.Context, core *b24.Core) (b24.ID, error) {
res, err := core.Call(ctx, "crm.deal.add", b24.Params{
"fields": b24.Params{"TITLE": "Сделка для примера b24gosdk"},
})
if err != nil {
return 0, fmt.Errorf("crm.deal.add: %w", err)
}
var id b24.ID
return id, json.Unmarshal(res.Result, &id)
}
// workflowsOfDeal возвращает идентификаторы процессов, запущенных на сделке.
func workflowsOfDeal(ctx context.Context, core *b24.Core, dealID b24.ID) ([]string, error) {
// bizproc.workflow.instances принимает параметры В ВЕРХНЕМ РЕГИСТРЕ. SELECT
// здесь не украшение: по умолчанию метод отдаёт лишь ID, MODIFIED и
// OWNED_UNTIL, а отсутствующее поле разберётся в нулевое значение молча.
res, err := core.Call(ctx, "bizproc.workflow.instances", b24.Params{
"SELECT": []string{"ID", "TEMPLATE_ID", "DOCUMENT_ID", "STARTED"},
"FILTER": b24.Params{"DOCUMENT_ID": fmt.Sprintf("DEAL_%d", dealID)},
}, b24.WithIdempotent())
if err != nil {
if errors.Is(err, b24.ErrAccessDenied) {
return nil, fmt.Errorf("bizproc.* доступен только администратору портала: %w", err)
}
if errors.Is(err, b24.ErrMethodNotFound) {
return nil, fmt.Errorf("модуль бизнес-процессов недоступен на этом портале: %w", err)
}
return nil, fmt.Errorf("bizproc.workflow.instances: %w", err)
}
var instances []struct {
ID string `json:"ID"`
}
if err := json.Unmarshal(res.Result, &instances); err != nil {
return nil, fmt.Errorf("разбор процессов: %w", err)
}
ids := make([]string, 0, len(instances))
for _, i := range instances {
if i.ID != "" {
ids = append(ids, i.ID)
}
}
return ids, nil
}
func currentUser(ctx context.Context, core *b24.Core) (b24.ID, error) {
res, err := core.Call(ctx, "user.current", nil, b24.WithIdempotent())
if err != nil {
return 0, fmt.Errorf("user.current: %w", err)
}
var u struct {
ID b24.ID `json:"ID"`
}
if err := json.Unmarshal(res.Result, &u); err != nil {
return 0, err
}
return u.ID, nil
}
// del убирает созданное. Ошибку уборки печатаем, но не возвращаем: она не
// должна подменить собой настоящую ошибку сценария.
func del(ctx context.Context, core *b24.Core, method string, params b24.Params) {
if _, err := core.Call(ctx, method, params); err != nil {
fmt.Fprintf(os.Stderr, "уборка, %s: %v\n", method, err)
}
}