Как добавить комментарий в таймлайн смарт-процесса
Scope:
crmКто может выполнять метод: пользователи с правом на изменение элемента CRM
Выберите инструмент для разработки с AI-агентом:
- используйте Битрикс24 Вайбкод, чтобы создать приложение для Битрикс24 по описанию задачи без знания языков программирования. Агент напишет код и разместит приложение на сервере без ручной настройки хостинга
- используйте MCP-сервер, чтобы разрабатывать интеграцию через REST API в своем проекте. Агент будет обращаться к официальной REST-документации
Ключевой параметр для добавления комментария в элемент CRM — идентификатор типа объекта. Идентификатор показывает, в какой тип объекта комментарий будет добавлен: в сделку, в лид, в определенный смарт-процесс.
Идентификатор используется в параметрах OWNER_TYPE, OWNER_TYPE_ID и ENTITY_TYPE, ENTITY_TYPE_ID групп методов crm.item.*, crm.timeline.*, crm.activity.*.
В CRM есть два типа идентификаторов объектов:
- Предустановленные — это идентификаторы лидов, сделок, компаний, контактов, счетов, предложений. Идентификаторы предустановленных объектов есть в документации
- Динамические — это идентификаторы смарт-процессов. Идентификатор смарт-процесса генерируется в момент создания, он не зависит от названия смарт-процесса
Получить идентификатор смарт-процесса можно двумя методами:
- crm.enum.ownertype — метод без параметров, возвращает перечисление типов объектов CRM, как предустановленных, так и динамических
- crm.type.list — метод с фильтром, возвращает только динамические объекты CRM
Для создания комментария в элементе смарт-процесса последовательно выполним два метода:
- crm.type.list — получаем смарт-процесс по фильтру
- crm.timeline.comment.add — создаем комментарий
1. Получаем идентификатор типа смарт-процесса
Для получения идентификатора типа используем метод crm.type.list с фильтром:
title— укажем название смарт-процесса
Как использовать примеры в документации
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'
const response = await $b24.actions.v2.call.make({
method: 'crm.type.list',
params: {
filter: {
"title": "Закупка оборудования"
}
},
requestId: 'type-list'
});
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Psr\Log\NullLogger;
$sb = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
->initFromWebhook('https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/');
$result = $sb->getCRMScope()->type()->list(
order: [],
filter: ['title' => 'Закупка оборудования']
);
from b24pysdk import BitrixWebhook, Client
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
response = client.crm.type.list(
filter={
"title": "Закупка оборудования",
}
).response
res, err := core.Call(ctx, "crm.type.list", b24.Params{
"filter": b24.Params{"title": spaTitle},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.type.list: %w", err)
}
// Метод заворачивает ответ в объект с ключом types. Двум смарт-процессам
// никто не запрещает называться одинаково, поэтому ответ — список даже при
// точном фильтре.
var types struct {
Types []struct {
ID int `json:"id"`
EntityTypeID int `json:"entityTypeId"`
Title string `json:"title"`
} `json:"types"`
}
if err := json.Unmarshal(res.Result, &types); err != nil {
return fmt.Errorf("разбор смарт-процессов: %w", err)
}
if len(types.Types) == 0 {
return fmt.Errorf("смарт-процесс %q не найден", spaTitle)
}
// id — порядковый номер смарт-процесса, entityTypeId — идентификатор его
// ТИПА. Дальше нужен именно entityTypeId, это разные числа.
entityTypeID := types.Types[0].EntityTypeID
В результате получили два значения ID:
id:7— порядковый номер смарт-процесса в БитриксentityTypeId:177— идентификатор типа смарт-процесса. Параметр, необходимый для следующего запроса
{
"result": {
"types": [
{
"id": 7,
"title": "Закупка оборудования",
"code": "",
"createdBy": 1,
"entityTypeId": 177,
"customSectionId": null,
"isCategoriesEnabled": "Y",
"isStagesEnabled": "Y",
"isBeginCloseDatesEnabled": "Y",
"isClientEnabled": "Y",
"isUseInUserfieldEnabled": "Y",
"isLinkWithProductsEnabled": "Y",
"isMycompanyEnabled": "Y",
"isDocumentsEnabled": "Y",
"isSourceEnabled": "Y",
"isObserversEnabled": "Y",
"isRecyclebinEnabled": "Y",
"isAutomationEnabled": "Y",
"isBizProcEnabled": "Y",
"isSetOpenPermissions": "Y",
"isPaymentsEnabled": "N",
"isCountersEnabled": "N",
"createdTime": "2021-11-26T10:52:17+03:00",
"updatedTime": "2024-11-12T15:32:39+03:00",
"updatedBy": 1
}
]
}
}
2. Добавляем комментарий к элементу смарт-процесса
Для добавления комментария используем метод crm.timeline.comment.add с параметрами:
ENTITY_ID— ID элемента. Для получения значения ID используйте метод crm.item.list, гдеentityTypeIdфильтра равно значениюentityTypeIdиз crm.type.listENTITY_TYPE— укажемDYNAMIC_177. Значение состоит изentityTypeIdиз результата предыдущего метода и префикса динамического объектаDYNAMIC_COMMENT— текстовое значение комментария
const response = await $b24.actions.v2.call.make({
method: 'crm.timeline.comment.add',
params: {
fields:
{
"ENTITY_ID": 19,
"ENTITY_TYPE": "DYNAMIC_177",
"COMMENT": "Подтвердить закупку по почте!",
}
},
requestId: 'comment-add'
});
$result = $sb->getCRMScope()->timelineComment()->add(
[
'ENTITY_ID' => 19,
'ENTITY_TYPE' => 'DYNAMIC_177',
'COMMENT' => 'Подтвердить закупку по почте!',
]
);
response = client.crm.timeline.comment.add(
fields={
"ENTITY_ID": 19,
"ENTITY_TYPE": "DYNAMIC_177",
"COMMENT": "Подтвердить закупку по почте!",
}
).response
// ENTITY_TYPE для смарт-процесса — это строка "DYNAMIC_" + entityTypeId.
// Поля таймлайна пишутся В ВЕРХНЕМ РЕГИСТРЕ, тогда как crm.item.* принимает
// camelCase: одна сущность, два соглашения в одном сценарии.
res, err = core.Call(ctx, "crm.timeline.comment.add", b24.Params{
"fields": b24.Params{
"ENTITY_ID": itemID,
"ENTITY_TYPE": "DYNAMIC_" + strconv.Itoa(entityTypeID),
"COMMENT": "Подтвердить закупку по почте!",
},
})
if err != nil {
return fmt.Errorf("crm.timeline.comment.add: %w", err)
}
// Обёртки здесь нет вовсе: result — это сразу идентификатор записи
// таймлайна, голым числом.
var commentID b24.ID
if err := json.Unmarshal(res.Result, &commentID); err != nil {
return fmt.Errorf("разбор идентификатора комментария: %w", err)
}
Мы добавили комментарий в таймлайн элемента смарт-процесса и в ответ получили ID записи таймлайна 55771. ID записи можно использовать в методах обновления и удаления комментария.
{
"result": 55771
}
Пример кода
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'
// Функция для поиска идентификатора смарт-процесса
async function findSPA() {
// Название смарт-процесса, для получения entityTypeId
var SPAtitle = 'название_вашего_смарт_процесса';
try {
// Вызываем метод crm.type.list для получения entityTypeId
const result = await $b24.actions.v2.call.make({
method: 'crm.type.list',
params: { filter: { title: SPAtitle } },
requestId: 'type-list'
});
var types = result.getData().result.types;
if (Array.isArray(types) && types.length > 0) {
var SPAId = types[0].entityTypeId; // Предполагаем, что нужный объект первый в массиве
console.log('Смарт-процесс найден', SPAId);
await createComment(SPAId);
} else {
console.error('Смарт-процесс не найден или данные пусты');
}
} catch (error) {
console.error('Ошибка при поиске смарт-процесса:', error);
}
}
// Функция для создания комментария в элементе смарт-процесса
async function createComment(SPAId) {
// ID элемента, в который будет добавлен комментарий
var elementId = 'ваш_ID_элемента';
// Текст комментария
var commentText = 'ваш_комментарий';
try {
// Вызываем метод crm.timeline.comment.add для добавления комментария
const result = await $b24.actions.v2.call.make({
method: 'crm.timeline.comment.add',
params: {
fields: {
ENTITY_ID: elementId,
ENTITY_TYPE: 'DYNAMIC_' + SPAId,
COMMENT: commentText
}
},
requestId: 'comment-add'
});
console.log('Комментарий добавлен', result.getData().result);
} catch (error) {
console.error('Ошибка при создании комментария:', error);
}
}
// Вызов функции для поиска смарт-процесса и добавления комментария
findSPA();
<?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 Psr\Log\NullLogger;
$sb = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
->initFromWebhook('https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/');
// Функция для поиска идентификатора смарт-процесса
function findSPA(ServiceBuilder $sb) {
// Название смарт-процесса, для получения entityTypeId
$SPAtitle = 'название_вашего_смарт_процесса';
try {
// Вызываем метод crm.type.list для получения entityTypeId
$types = $sb->getCRMScope()->type()->list(
order: [],
filter: ['title' => $SPAtitle]
)->getTypes();
if (is_array($types) && count($types) > 0) {
$SPAId = $types[0]->entityTypeId; // Предполагаем, что нужный объект первый в массиве
echo 'Смарт-процесс найден: ' . $SPAId;
createComment($sb, $SPAId);
} else {
echo 'Смарт-процесс не найден или данные пусты';
}
} catch (\Throwable $e) {
echo 'Ошибка при поиске смарт-процесса: ' . $e->getMessage();
}
}
// Функция для создания комментария в элементе смарт-процесса
function createComment(ServiceBuilder $sb, $SPAId) {
// ID элемента, в который будет добавлен комментарий
$elementId = 'ваш_ID_элемента';
// Текст комментария
$commentText = 'ваш_комментарий';
try {
// Вызываем метод crm.timeline.comment.add для добавления комментария
$sb->getCRMScope()->timelineComment()->add(
[
'ENTITY_ID' => $elementId,
'ENTITY_TYPE' => 'DYNAMIC_' . $SPAId,
'COMMENT' => $commentText
]
);
echo 'Комментарий добавлен';
} catch (\Throwable $e) {
echo 'Ошибка при создании комментария: ' . $e->getMessage();
}
}
// Вызов функции для поиска смарт-процесса и добавления комментария
findSPA($sb);
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError
def find_spa(client):
spa_title = "название_вашего_смарт_процесса"
try:
resp = client.crm.type.list(
filter={"title": spa_title},
).response
except BitrixAPIError as error:
print(f"Ошибка при поиске смарт-процесса: {error}")
return
types = resp.result["types"]
if types:
spa_id = types[0]["entityTypeId"]
print(f"Смарт-процесс найден: {spa_id}")
create_comment(client, spa_id)
else:
print("Смарт-процесс не найден или данные пусты")
def create_comment(client, spa_id):
element_id = "ваш_ID_элемента"
comment_text = "ваш_комментарий"
try:
client.crm.timeline.comment.add(
fields={
"ENTITY_ID": element_id,
"ENTITY_TYPE": f"DYNAMIC_{spa_id}",
"COMMENT": comment_text,
},
).response
except BitrixAPIError as error:
print(f"Ошибка при создании комментария: {error}")
else:
print("Комментарий добавлен")
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
find_spa(client)
// Подготовка в пустом каталоге — 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 .
//
// Пример самодостаточный: он создаёт смарт-процесс и элемент в нём, находит
// смарт-процесс по названию, добавляет комментарий в таймлайн элемента и
// убирает за собой. Запускается на любом портале, ничего править не нужно.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strconv"
b24 "github.com/bitrix24/b24gosdk"
)
// Название смарт-процесса — то же, что ищет шаг 1.
const spaTitle = "Закупка оборудования (пример b24gosdk)"
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()
// --- подготовка: свой смарт-процесс и элемент в нём
typeID, err := addType(ctx, core, spaTitle)
if err != nil {
return err
}
defer del(ctx, core, "crm.type.delete", b24.Params{"id": typeID})
// entityTypeId нужен и для создания элемента, и для комментария, но пока
// известен только id самого типа — за entityTypeId идём на шаге 1.
// --- шаг 1: находим смарт-процесс по названию
res, err := core.Call(ctx, "crm.type.list", b24.Params{
"filter": b24.Params{"title": spaTitle},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.type.list: %w", err)
}
// Метод заворачивает ответ в объект с ключом types. Двум смарт-процессам
// никто не запрещает называться одинаково, поэтому ответ — список даже при
// точном фильтре.
var types struct {
Types []struct {
ID int `json:"id"`
EntityTypeID int `json:"entityTypeId"`
Title string `json:"title"`
} `json:"types"`
}
if err := json.Unmarshal(res.Result, &types); err != nil {
return fmt.Errorf("разбор смарт-процессов: %w", err)
}
if len(types.Types) == 0 {
return fmt.Errorf("смарт-процесс %q не найден", spaTitle)
}
// id — порядковый номер смарт-процесса, entityTypeId — идентификатор его
// ТИПА. Дальше нужен именно entityTypeId, это разные числа.
entityTypeID := types.Types[0].EntityTypeID
fmt.Printf("смарт-процесс %q: id=%d, entityTypeId=%d\n",
types.Types[0].Title, types.Types[0].ID, entityTypeID)
itemID, err := addItem(ctx, core, entityTypeID, "Закупка ноутбуков")
if err != nil {
return err
}
defer del(ctx, core, "crm.item.delete", b24.Params{
"entityTypeId": entityTypeID, "id": itemID,
})
// --- шаг 2: добавляем комментарий в таймлайн элемента
// ENTITY_TYPE для смарт-процесса — это строка "DYNAMIC_" + entityTypeId.
// Поля таймлайна пишутся В ВЕРХНЕМ РЕГИСТРЕ, тогда как crm.item.* принимает
// camelCase: одна сущность, два соглашения в одном сценарии.
res, err = core.Call(ctx, "crm.timeline.comment.add", b24.Params{
"fields": b24.Params{
"ENTITY_ID": itemID,
"ENTITY_TYPE": "DYNAMIC_" + strconv.Itoa(entityTypeID),
"COMMENT": "Подтвердить закупку по почте!",
},
})
if err != nil {
return fmt.Errorf("crm.timeline.comment.add: %w", err)
}
// Обёртки здесь нет вовсе: result — это сразу идентификатор записи
// таймлайна, голым числом.
var commentID b24.ID
if err := json.Unmarshal(res.Result, &commentID); err != nil {
return fmt.Errorf("разбор идентификатора комментария: %w", err)
}
fmt.Printf("комментарий %d добавлен в элемент %d\n", commentID, itemID)
return nil
}
// --- вспомогательное: подготовка данных и уборка
// addType создаёт смарт-процесс. entityTypeId намеренно не передаётся: его
// выдаёт портал, и именно за ним идёт шаг 1.
func addType(ctx context.Context, core *b24.Core, title string) (b24.ID, error) {
// isRecyclebinEnabled выключаем осознанно: элемент в корзине всё ещё
// считается элементом, а crm.type.delete отказывается удалять тип, у
// которого есть элементы.
res, err := core.Call(ctx, "crm.type.add", b24.Params{
"fields": b24.Params{"title": title, "isRecyclebinEnabled": "N"},
})
if err != nil {
// На тарифах без смарт-процессов метод отвечает отдельным кодом.
// Код сравнивается через errors.Is, а не строкой: опечатка в литерале
// скомпилируется и молча уведёт в другую ветку.
if errors.Is(err, b24.Code("CREATE_DYNAMIC_TYPE_RESTRICTED")) {
return 0, fmt.Errorf("на этом портале нельзя создать смарт-процесс: %w", err)
}
return 0, fmt.Errorf("crm.type.add: %w", err)
}
raw, ok := b24.Unwrap(res.Result, "type", "id")
if !ok {
return 0, fmt.Errorf("нет type.id в %s", res.Result)
}
var id b24.ID
return id, json.Unmarshal(raw, &id)
}
func addItem(ctx context.Context, core *b24.Core, entityTypeID int, title string) (b24.ID, error) {
res, err := core.Call(ctx, "crm.item.add", b24.Params{
"entityTypeId": entityTypeID,
"fields": b24.Params{"title": title},
})
if err != nil {
return 0, fmt.Errorf("crm.item.add: %w", err)
}
raw, ok := b24.Unwrap(res.Result, "item", "id")
if !ok {
return 0, fmt.Errorf("нет item.id в %s", res.Result)
}
var id b24.ID
return id, json.Unmarshal(raw, &id)
}
// 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)
}
}