How to Delegate Incomplete Tasks of a Terminated Employee
Scope:
task,user_basicWho can execute the methods: to complete the entire scenario, you need permission to view and delegate tasks
- user.get - any user
- tasks.task.list - a user with access to the tasks from the filter
- tasks.task.getaccess - any user
- tasks.task.delegate - a user with permission to delegate the task
If you are developing integrations for Bitrix24 using AI tools (Codex, Claude Code, Cursor), connect to the MCP server so that the assistant can utilize the official REST documentation.
After an employee is terminated, their incomplete tasks can be delegated to another responsible person. To do this, find the terminated employee identifier, retrieve the tasks where this employee is responsible, and delegate each task to the new employee.
Delegation depends on the permissions of the user who created the webhook. Before calling the method that changes the responsible person, first retrieve the tasks and then check the DELEGATE permission for each of them.
The scenario consists of four steps.
- Find the terminated employee and the new responsible person using user.get
- Retrieve the terminated employee's incomplete tasks using tasks.task.list
- Check delegation permission using tasks.task.getaccess
- Delegate each accessible task using tasks.task.delegate
As a result, the responsible person will change for tasks where the user who created the webhook has the DELEGATE permission. Completed tasks are not delegated: the delegation method returns the Task already completed error.
Before You Start
Check access conditions and prepare the input data:
-
an incoming webhook is created for a user who can see the terminated employee's tasks and has permission to delegate these tasks
-
the webhook permissions include the
taskanduser_basicscopes -
you know the name, last name, e-mail, or another attribute of the terminated employee
-
you know the name, last name, e-mail, or identifier of the new responsible person
The webhook runs requests with the permissions of the user who created it. An administrator sees all tasks, a manager sees their employees' tasks, and other users see only tasks available to them.
Store the webhook URL in an environment variable and do not publish it in open code.
In the examples below, we search for the terminated employee by the last name Weber and for the new responsible person by the e-mail new.responsible@example.com. Replace these values with your own values in your Bitrix24.
Server-side JS examples with B24Hook require Node.js 18, 20, 22, or later. For new projects, use 22 or later. B24JsSDK is an ES module: save the code in an .mjs file or add "type": "module" to package.json.
Examples with b24pysdk require Python 3.9 or later.
1. Find Employees
The user.get method retrieves users by filter. To find the terminated employee, pass ACTIVE: false and an additional attribute to the filter, such as a last name or e-mail. To find the new responsible person, pass ACTIVE: true.
Save these values from the response:
IDof the terminated employee - we will pass it to theRESPONSIBLE_IDtask filterIDof the new responsible person - we will pass it to theuserIdparameter of the delegation method
How to Use Examples in Documentation
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
async function callMethod(method, params, requestId) {
const response = await $b24.actions.v2.call.make({
method,
params,
requestId
})
if (!response.isSuccess) {
throw new Error(response.getErrorMessages().join('; '))
}
return response.getData().result
}
async function findOneUser(filter, requestId) {
const result = await callMethod(
'user.get',
{
FILTER: filter,
SELECT: ['ID', 'ACTIVE', 'NAME', 'LAST_NAME', 'EMAIL']
},
requestId
)
if (!Array.isArray(result) || result.length !== 1) {
throw new Error(`Expected one user, received: ${Array.isArray(result) ? result.length : 0}`)
}
return result[0]
}
const firedUser = await findOneUser(
{
ACTIVE: false,
LAST_NAME: 'Weber'
},
'user-get-fired'
)
const newResponsible = await findOneUser(
{
ACTIVE: true,
EMAIL: 'new.responsible@example.com'
},
'user-get-new-responsible'
)
<?php
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\EventDispatcher;
$serviceBuilder = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
->initFromWebhook(getenv('B24_HOOK'));
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
function callMethod($serviceBuilder, string $method, array $params): array
{
return $serviceBuilder
->core
->call($method, $params)
->getResponseData()
->getResult();
}
function findOneUser($serviceBuilder, array $filter): array
{
$result = callMethod(
$serviceBuilder,
'user.get',
[
'FILTER' => $filter,
'SELECT' => ['ID', 'ACTIVE', 'NAME', 'LAST_NAME', 'EMAIL'],
]
);
if (count($result) !== 1) {
throw new RuntimeException('Expected one user, received: ' . count($result));
}
return $result[0];
}
$firedUser = findOneUser(
$serviceBuilder,
[
'ACTIVE' => false,
'LAST_NAME' => 'Weber',
]
);
$newResponsible = findOneUser(
$serviceBuilder,
[
'ACTIVE' => true,
'EMAIL' => 'new.responsible@example.com',
]
);
import os
from b24pysdk import BitrixWebhook, Client
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token=os.environ["B24_HOOK_TOKEN"],
)
)
# B24_HOOK_TOKEN = 'user_id/webhook_key'
def find_one_user(filter):
result = client.user.get(
filter=filter,
select=["ID", "ACTIVE", "NAME", "LAST_NAME", "EMAIL"],
).response.result
if len(result) != 1:
raise RuntimeError(f"Expected one user, received: {len(result)}")
return result[0]
fired_user = find_one_user(
{
"ACTIVE": False,
"LAST_NAME": "Weber",
}
)
new_responsible = find_one_user(
{
"ACTIVE": True,
"EMAIL": "new.responsible@example.com",
}
)
Short response for the terminated employee:
{
"result": [
{
"ID": "37",
"ACTIVE": false,
"NAME": "Klaus",
"LAST_NAME": "Weber",
"EMAIL": "k.weber@example.com"
}
],
"total": 1
}
Short response for the new responsible person:
{
"result": [
{
"ID": "547",
"ACTIVE": true,
"NAME": "Anna",
"LAST_NAME": "Schmidt",
"EMAIL": "new.responsible@example.com"
}
],
"total": 1
}
As a result, we retrieved the terminated employee ID 37 and the new responsible person ID 547. These values are required in the next steps.
2. Retrieve the Terminated Employee's Tasks
The tasks.task.list method retrieves tasks with page navigation. Pass these values to the filter:
RESPONSIBLE_ID- terminated employee identifier!REAL_STATUS: 5- exclude completed tasks
Completed tasks must be excluded before delegation. The tasks.task.delegate method does not perform the action on a completed task.
async function getEmployeeTasks(employeeId) {
const tasks = []
let start = 0
while (true) {
const result = await callMethod(
'tasks.task.list',
{
order: { ID: 'asc' },
filter: {
RESPONSIBLE_ID: Number(employeeId),
'!REAL_STATUS': 5
},
select: ['ID', 'TITLE', 'RESPONSIBLE_ID', 'STATUS'],
start
},
`tasks-task-list-${start}`
)
const page = result.tasks ?? []
tasks.push(...page)
if (page.length < 50) {
break
}
start += 50
}
return tasks
}
const tasks = await getEmployeeTasks(firedUser.ID)
function getEmployeeTasks($serviceBuilder, int $employeeId): array
{
$tasks = [];
$start = 0;
do {
$result = callMethod(
$serviceBuilder,
'tasks.task.list',
[
'order' => ['ID' => 'asc'],
'filter' => [
'RESPONSIBLE_ID' => $employeeId,
'!REAL_STATUS' => 5,
],
'select' => ['ID', 'TITLE', 'RESPONSIBLE_ID', 'STATUS'],
'start' => $start,
]
);
$page = $result['tasks'] ?? [];
$tasks = array_merge($tasks, $page);
$start += 50;
} while (count($page) === 50);
return $tasks;
}
$tasks = getEmployeeTasks($serviceBuilder, (int)$firedUser['ID']);
def get_employee_tasks(employee_id):
tasks = []
start = 0
while True:
result = client.tasks.task.list(
order={"ID": "asc"},
filter={
"RESPONSIBLE_ID": int(employee_id),
"!REAL_STATUS": 5,
},
select=["ID", "TITLE", "RESPONSIBLE_ID", "STATUS"],
start=start,
).response.result
page = result.get("tasks", [])
tasks.extend(page)
if len(page) < 50:
break
start += 50
return tasks
tasks = get_employee_tasks(fired_user["ID"])
Short response:
{
"result": {
"tasks": [
{
"id": "8017",
"title": "Prepare the presentation",
"responsibleId": "37",
"status": "2"
}
]
},
"total": 1
}
As a result, we retrieved an array of tasks. For the next step, you need each task's id. If the method returns 50 tasks, request the next page with start: 50.
3. Check Delegation Permission
The tasks.task.getaccess method checks which actions are available to the user who created the webhook. Before changing tasks in bulk, check the DELEGATE action for each task from step 2.
If DELEGATE is false for a task, do not pass it to tasks.task.delegate. This task will remain assigned to the previous responsible person until a user with the required permissions delegates it manually or through another webhook.
async function filterTasksAllowedToDelegate(tasks) {
const allowed = []
for (const task of tasks) {
const result = await callMethod(
'tasks.task.getaccess',
{
taskId: Number(task.id)
},
`tasks-task-getaccess-${task.id}`
)
const currentUserActions = Object.values(result.allowedActions ?? {})[0] ?? {}
if (currentUserActions.DELEGATE === true) {
allowed.push(task)
}
}
return allowed
}
const allowedTasks = await filterTasksAllowedToDelegate(tasks)
function filterTasksAllowedToDelegate($serviceBuilder, array $tasks): array
{
$allowed = [];
foreach ($tasks as $task) {
$result = callMethod(
$serviceBuilder,
'tasks.task.getaccess',
[
'taskId' => (int)$task['id'],
]
);
$allowedActions = $result['allowedActions'] ?? [];
$currentUserActions = reset($allowedActions);
if (is_array($currentUserActions) && ($currentUserActions['DELEGATE'] ?? false) === true) {
$allowed[] = $task;
}
}
return $allowed;
}
$allowedTasks = filterTasksAllowedToDelegate($serviceBuilder, $tasks);
def filter_tasks_allowed_to_delegate(tasks):
allowed = []
for task in tasks:
result = client.tasks.task.getaccess(
task_id=int(task["id"]),
).response.result
allowed_actions = result.get("allowedActions", {})
current_user_actions = next(iter(allowed_actions.values()), {})
if current_user_actions.get("DELEGATE") is True:
allowed.append(task)
return allowed
allowed_tasks = filter_tasks_allowed_to_delegate(tasks)
Short response:
{
"result": {
"allowedActions": {
"1269": {
"DELEGATE": true
}
}
}
}
The method returns the allowedActions object with the list of available actions for the task. In the example, the code checks the DELEGATE action: if the value is true, the task remains in the allowedTasks array.
Pass the allowedTasks array to step 4. If it is empty after the check, finish the scenario without calling tasks.task.delegate.
4. Delegate Tasks to the New Responsible Person
The tasks.task.delegate method changes the responsible person in one task. To delegate all accessible tasks, call the method for each id from the allowedTasks array.
Pass these parameters:
taskId- task identifieruserId- new responsible person identifier
async function delegateTasks(tasks, userId) {
const delegated = []
for (const task of tasks) {
const result = await callMethod(
'tasks.task.delegate',
{
taskId: Number(task.id),
userId: Number(userId)
},
`tasks-task-delegate-${task.id}`
)
delegated.push(result.task)
}
return delegated
}
const delegatedTasks = await delegateTasks(allowedTasks, newResponsible.ID)
console.table(delegatedTasks.map((task) => ({
id: task.id,
title: task.title,
responsibleId: task.responsibleId
})))
function delegateTasks($serviceBuilder, array $tasks, int $userId): array
{
$delegated = [];
foreach ($tasks as $task) {
$result = callMethod(
$serviceBuilder,
'tasks.task.delegate',
[
'taskId' => (int)$task['id'],
'userId' => $userId,
]
);
$delegated[] = $result['task'];
}
return $delegated;
}
$delegatedTasks = delegateTasks($serviceBuilder, $allowedTasks, (int)$newResponsible['ID']);
print_r($delegatedTasks);
def delegate_tasks(tasks, user_id):
delegated = []
for task in tasks:
result = client.tasks.task.delegate(
task_id=int(task["id"]),
user_id=int(user_id),
).response.result
delegated.append(result["task"])
return delegated
delegated_tasks = delegate_tasks(allowed_tasks, new_responsible["ID"])
print(delegated_tasks)
Short response:
{
"result": {
"task": {
"id": "8017",
"title": "Prepare the presentation",
"responsibleId": "547",
"status": "2"
}
}
}
The method returns the task object with updated task data. If responsibleId in the response equals the identifier of the new responsible person, the task has been delegated.
In the example, the code stores delegated tasks in the delegatedTasks array. This array is required to verify the result.
Verify the Result
Verify the result by calling tasks.task.list again: select tasks by the RESPONSIBLE_ID of the new responsible person and by the delegated task identifiers.
const delegatedTaskIds = delegatedTasks.map((task) => task.id)
if (delegatedTaskIds.length > 0) {
const checkResult = await callMethod(
'tasks.task.list',
{
filter: {
'@ID': delegatedTaskIds,
RESPONSIBLE_ID: Number(newResponsible.ID)
},
select: ['ID', 'TITLE', 'RESPONSIBLE_ID']
},
'tasks-task-list-check'
)
console.table(checkResult.tasks)
}
$delegatedTaskIds = array_map(
static fn(array $task): string => (string)$task['id'],
$delegatedTasks
);
if (!empty($delegatedTaskIds)) {
$checkResult = callMethod(
$serviceBuilder,
'tasks.task.list',
[
'filter' => [
'@ID' => $delegatedTaskIds,
'RESPONSIBLE_ID' => (int)$newResponsible['ID'],
],
'select' => ['ID', 'TITLE', 'RESPONSIBLE_ID'],
]
);
print_r($checkResult['tasks'] ?? []);
}
delegated_task_ids = [task["id"] for task in delegated_tasks]
if delegated_task_ids:
check_result = client.tasks.task.list(
filter={
"@ID": delegated_task_ids,
"RESPONSIBLE_ID": int(new_responsible["ID"]),
},
select=["ID", "TITLE", "RESPONSIBLE_ID"],
).response.result
print(check_result.get("tasks", []))
The scenario is successful if the number of tasks in the verification response matches the number of delegated tasks and each task's responsibleId equals the new responsible person ID.
Check these values in the responses:
-
all tasks from the
allowedTasksarray are present in thedelegatedTasksarray -
each
responsibleIdvalue in the tasks.task.delegate response equals the new responsible personID -
the verification call to tasks.task.list returned the same tasks by the
@IDandRESPONSIBLE_IDfilters
In the interface, the tasks will appear for the new responsible person.
Errors and Troubleshooting
If the method returns an error, check the request data.
|
Code or Error Text |
Reason and Action |
|
|
The webhook does not have the required scope. Task methods require the |
|
|
The user who created the webhook does not have permission to delegate the task. Check action permission using tasks.task.getaccess |
|
|
The task is completed. Exclude completed tasks using the |
|
|
The new responsible person identifier was not passed in the |
|
|
An invalid task identifier was passed in the |
If the task list is empty, check:
- the terminated employee was found using user.get, and their
IDwas passed toRESPONSIBLE_ID - the user who created the webhook can see the terminated employee's tasks
- the tasks.task.list filter does not contain extra conditions
- the terminated employee has incomplete tasks that can be delegated
If allowedTasks is empty while the task list is not empty, the user who created the webhook does not have the DELEGATE permission for these tasks. Run the scenario through a webhook created by a user with the required permissions, or delegate the tasks manually in the interface.
If some tasks have already been delegated, restart the scenario from step 2. The tasks.task.list method will no longer return delegated tasks by the terminated employee RESPONSIBLE_ID filter.
Key Points
Before running the scenario, consider method and access permission limitations.
- tasks.task.list returns tasks in pages of 50 items. To retrieve all tasks, iterate over
start:0,50,100 - tasks.task.delegate delegates one task per call. To delegate a list of tasks, call the method for each task
- completed tasks cannot be delegated. The scenario delegates only incomplete tasks
- delegation depends on the permissions of the user who created the webhook. If the user does not have access to the task or permission to delegate it, the method returns an error
- if several employees are found by the user.get filter, refine the filter: add
EMAIL,ID, or another exact attribute