How to Add a Comment to the Timeline of a Smart Process
Scope:
crmWho can execute the methods: to complete the entire scenario, the strictest of the listed rights is required — administrative access to the CRM section
- crm.type.list — a user with administrative access to the CRM section
- crm.timeline.comment.add — any user
- crm.item.list — any user with permission to read CRM object items
Choose a tool for developing with an AI agent:
- use Alaio Vibecode to build an app for Bitrix24 from a task description without knowing any programming language. The agent writes the code and deploys the app to a server, with no manual hosting setup
- use the MCP server to develop a REST API integration in your own project. The agent refers to the official REST documentation
The key parameter for adding a comment to a CRM object is the object type identifier. This identifier indicates which type of object the comment will be added to: a deal, a lead, or a specific smart process.
The identifier is used in the parameters OWNER_TYPE, OWNER_TYPE_ID, ENTITY_TYPE, and ENTITY_TYPE_ID of the method groups crm.item.*, crm.timeline.*, and crm.activity.*.
In CRM, there are two types of object identifiers:
-
Predefined — these are identifiers for leads, deals, companies, contacts, invoices, and estimates. The identifiers for predefined objects can be found in the documentation.
-
Dynamic — these are identifiers for smart processes. The identifier for a smart process is generated at the time of creation and does not depend on the name of the smart process.
You can obtain the identifier for a smart process using two methods:
-
crm.enum.ownertype — a method without parameters that returns an enumeration of CRM object types, both predefined and dynamic.
-
crm.type.list — a method with a filter that returns only dynamic CRM objects.
As a result of the scenario, a comment appears in the timeline of the smart process item, and the method returns the ID of the timeline entry.
The scenario consists of two steps.
- Retrieve the
entityTypeIdof the smart process using the crm.type.list method. - Create the comment using the crm.timeline.comment.add method, building the value of the
ENTITY_TYPEparameter fromentityTypeId.
Before You Start
-
The smart process is already created in Bitrix24, and you know its name. Smart processes are not available on every plan: if they cannot be created, the crm.type.add method returns the
CREATE_DYNAMIC_TYPE_RESTRICTEDerror. -
The smart process contains an item whose timeline you want to add a comment to. The item ID is returned by the crm.item.list method with the
entityTypeIdparameter from step 1. -
The webhook is created on behalf of a user with administrative access to the CRM section — this is a requirement of the crm.type.list method.
1. Retrieve the Smart Process Type Identifier
To obtain the type identifier, we use the crm.type.list method with a filter:
title— specify the name of the smart process. ReplaceEquipment procurementwith the name of your own smart process.
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/'
const response = await $b24.actions.v2.call.make({
method: 'crm.type.list',
params: {
filter: {
"title": "Equipment procurement"
}
},
requestId: 'type-list'
});
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": "Equipment procurement",
}
).response
// 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.com/rest/USER_ID/TOKEN/');
$result = $sb->getCRMScope()->type()->list(
order: [],
filter: ['title' => 'Equipment procurement']
);
// core, ctx, and spaTitle are declared in the complete example below
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)
}
// The method wraps the response in an object with the types key. Two smart processes
// may have identical titles, so the response is a list even with
// an exact filter.
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("parse smart processes: %w", err)
}
if len(types.Types) == 0 {
return fmt.Errorf("smart process %q not found", spaTitle)
}
// id is the sequential number of the smart process, entityTypeId is the ID of its
// TYPE. Further on you need exactly entityTypeId, these are different numbers.
entityTypeID := types.Types[0].EntityTypeID
As a result, we obtained two ID values:
-
id:7— the sequential number of the smart process in Bitrix24 -
entityTypeId:177— the smart process type identifier. This parameter is required for the next request
{
"result": {
"types": [
{
"id": 7,
"title": "Equipment procurement",
"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
}
]
}
}
Retain the entityTypeId — the ENTITY_TYPE value is built from it in the next step. The id value is not needed for this scenario.
2. Add a Comment to the Smart Process Entity
To add a comment, use the crm.timeline.comment.add method with the following parameters:
-
ENTITY_ID— the item ID. To retrieve the ID value, use the crm.item.list method, where theentityTypeIdfilter equals theentityTypeIdvalue from crm.type.list. In the example, we specify19 -
ENTITY_TYPE— specifyDYNAMIC_177. The value consists of the dynamic object prefixDYNAMIC_andentityTypeIdfrom the previous method's result. Substitute exactlyentityTypeId: theidof the smart process does not work here -
COMMENT— the text value of the comment. The method does not accept an empty string
const response = await $b24.actions.v2.call.make({
method: 'crm.timeline.comment.add',
params: {
fields:
{
"ENTITY_ID": 19,
"ENTITY_TYPE": "DYNAMIC_177",
"COMMENT": "Confirm the purchase via email!",
}
},
requestId: 'comment-add'
});
response = client.crm.timeline.comment.add(
fields={
"ENTITY_ID": 19,
"ENTITY_TYPE": "DYNAMIC_177",
"COMMENT": "Confirm the purchase via email!",
}
).response
$result = $sb->getCRMScope()->timelineComment()->add(
[
'ENTITY_ID' => 19,
'ENTITY_TYPE' => 'DYNAMIC_177',
'COMMENT' => 'Confirm the purchase via email!',
]
);
// core, ctx, entityTypeID, and itemID are declared in the complete example below.
// ENTITY_TYPE for a smart process is the string "DYNAMIC_" + entityTypeId.
// Timeline fields are written in UPPERCASE, whereas crm.item.* accepts
// camelCase: one entity, two conventions in a single scenario.
res, err = core.Call(ctx, "crm.timeline.comment.add", b24.Params{
"fields": b24.Params{
"ENTITY_ID": itemID,
"ENTITY_TYPE": "DYNAMIC_" + strconv.Itoa(entityTypeID),
"COMMENT": "Confirm the purchase via email!",
},
})
if err != nil {
return fmt.Errorf("crm.timeline.comment.add: %w", err)
}
// There is no wrapper here at all: result is the ID of the timeline
// record itself, as a bare number.
var commentID b24.ID
if err := json.Unmarshal(res.Result, &commentID); err != nil {
return fmt.Errorf("parse comment ID: %w", err)
}
We added a comment to the SPA item timeline and received the timeline entry ID 55771 in the response. The entry ID can be used in the update and delete methods for the comment.
{
"result": 55771
}
Verify the Result
Open the smart process item in Bitrix24. The comment is displayed in the item timeline, in the feed below the card.
Through REST, the item comments are returned by the crm.timeline.comment.list method with the same ENTITY_ID and ENTITY_TYPE values as in step 2.
const checkResponse = await $b24.actions.v2.call.make({
method: 'crm.timeline.comment.list',
params: {
filter: {
"ENTITY_ID": 19,
"ENTITY_TYPE": "DYNAMIC_177"
},
order: { ID: 'DESC' }
},
requestId: 'comment-list'
});
console.dir(checkResponse.getData().result);
comments = client.crm.timeline.comment.list(
filter={
"ENTITY_ID": 19,
"ENTITY_TYPE": "DYNAMIC_177",
},
order={"ID": "DESC"},
).response.result
// crm.timeline.comment.list has no wrapper in the SDK — call the method directly
$comments = $sb->core->call(
'crm.timeline.comment.list',
[
'filter' => [
'ENTITY_ID' => 19,
'ENTITY_TYPE' => 'DYNAMIC_177',
],
'order' => ['ID' => 'DESC'],
]
)->getResponseData()->getResult();
The scenario is complete if the response contains an object with the ID from step 2, and its COMMENT field matches the text you sent.
{
"result": [
{
"ID": "55771",
"ENTITY_ID": 19,
"ENTITY_TYPE": "dynamic_177",
"CREATED": "2024-11-12T15:32:39+03:00",
"COMMENT": "Confirm the purchase via email!",
"AUTHOR_ID": "1"
}
],
"total": 1
}
In the request, ENTITY_TYPE can be passed in any case, and the method returns it in lowercase — dynamic_177. This is not a sign of an error.
Errors and Diagnostics
If the method returns an error, check the request data.
|
Code |
Reason and action |
|
|
The user does not have administrative access to the CRM section required by crm.type.list. Check which user the webhook was created on behalf of |
|
|
The crm.type.list method is allowed only for intranet users. Extranet users and external users cannot complete the scenario |
|
|
A nonexistent filter field was passed to crm.type.list. Filter by the fields of the type object, and to search by name — by the |
|
|
A type that does not exist in Bitrix24 was passed in |
|
|
An empty string was passed in |
|
|
Required fields were not passed. The |
The crm.timeline.comment.add method may return an entry ID while the comment does not appear in the timeline. The method does not check whether an item with the passed ENTITY_ID exists: if there is no such item, the comment is created but has nothing to attach to.
-
Check
ENTITY_IDusing the crm.item.list method withentityTypeIdfrom step 1. If there is no item with that identifier, take an existing one -
Make sure that
ENTITY_IDis the item identifier, not theidorentityTypeIdof the smart process
Repeat the scenario from the step that returned the error. Step 1 does not create anything, so it can be executed any number of times. If step 2 returned the error, the comment was not created: fix the fields and repeat only that step.
Key Considerations
-
Timeline fields are written in uppercase —
ENTITY_ID,ENTITY_TYPE,COMMENT. The crm.item.* methods for the same object accept camelCase, for exampleentityTypeId. One entity, two conventions in a single scenario -
The
titlefilter in crm.type.list does not guarantee a single result: two smart processes are allowed to have the same name. The method always returns a list, so check that it contains exactly one element instead of blindly taking the first one -
Running the example again adds one more comment to the timeline, duplicates are not filtered out
Code Example
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
// Function to search for the smart process ID
async function findSPA() {
// Smart process name to obtain entityTypeId
var SPAtitle = 'your_smart_process_name';
try {
// Calling the crm.type.list method to obtain 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; // Assuming the required object is the first in the array
console.log('Smart process found', SPAId);
await createComment(SPAId);
} else {
console.error('Smart process not found or data is empty');
}
} catch (error) {
console.error('Error searching for the smart process:', error);
}
}
// Function to create a comment in a smart process element
async function createComment(SPAId) {
// Element ID where the comment will be added
var elementId = 'your_element_ID';
// Comment text
var commentText = 'your_comment';
try {
// Calling the crm.timeline.comment.add method to add a comment
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('Comment added', result.getData().result);
} catch (error) {
console.error('Error creating the comment:', error);
}
}
// Calling the function to search for the smart process and add a comment
findSPA();
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError
def find_spa(client):
spa_title = "your_smart_process_name"
try:
resp = client.crm.type.list(
filter={"title": spa_title},
).response
except BitrixAPIError as error:
print(f"Error searching for the smart process: {error}")
return
types = resp.result["types"]
if types:
spa_id = types[0]["entityTypeId"]
print(f"Smart process found: {spa_id}")
create_comment(client, spa_id)
else:
print("Smart process not found or data is empty")
def create_comment(client, spa_id):
element_id = "your_element_ID"
comment_text = "your_comment"
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 creating the comment: {error}")
else:
print("Comment added")
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
find_spa(client)
<?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.com/rest/USER_ID/TOKEN/');
// Function to search for the smart process ID
function findSPA(ServiceBuilder $sb) {
// Smart process name to obtain entityTypeId
$SPAtitle = 'your_smart_process_name';
try {
// Calling the crm.type.list method to obtain entityTypeId
$types = $sb->getCRMScope()->type()->list(
order: [],
filter: ['title' => $SPAtitle]
)->getTypes();
if (is_array($types) && count($types) > 0) {
$SPAId = $types[0]->entityTypeId; // Assuming the required object is the first in the array
echo 'Smart process found: ' . $SPAId;
createComment($sb, $SPAId);
} else {
echo 'Smart process not found or data is empty';
}
} catch (\Throwable $e) {
echo 'Error searching for the smart process: ' . $e->getMessage();
}
}
// Function to create a comment in a smart process element
function createComment(ServiceBuilder $sb, $SPAId) {
// Element ID where the comment will be added
$elementId = 'your_element_ID';
// Comment text
$commentText = 'your_comment';
try {
// Calling the crm.timeline.comment.add method to add a comment
$sb->getCRMScope()->timelineComment()->add(
[
'ENTITY_ID' => $elementId,
'ENTITY_TYPE' => 'DYNAMIC_' . $SPAId,
'COMMENT' => $commentText
]
);
echo 'Comment added';
} catch (\Throwable $e) {
echo 'Error creating the comment: ' . $e->getMessage();
}
}
// Calling the function to search for the smart process and add a comment
findSPA($sb);
// Setup in an empty directory — go get will not work without go mod init:
//
// go mod init example && go get github.com/bitrix24/b24gosdk
//
// Run:
//
// export B24_WEBHOOK_URL='https://your-portal.bitrix24.com/rest/1/token/' && go run .
//
// The example is self-contained: it creates a smart process and an item in it, finds
// the smart process by title, adds a comment to the item timeline, and
// cleans up after itself. It runs on any portal, nothing needs to be edited.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strconv"
b24 "github.com/bitrix24/b24gosdk"
)
// The smart process title is the same one that step 1 looks for.
const spaTitle = "Equipment procurement (b24gosdk example)"
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
// The webhook path is a secret, so it comes from the environment, not from the code.
core := b24.NewClient(os.Getenv("B24_WEBHOOK_URL")).Core()
// --- setup: our own smart process and an item in it
typeID, err := addType(ctx, core, spaTitle)
if err != nil {
return err
}
defer del(ctx, core, "crm.type.delete", b24.Params{"id": typeID})
// entityTypeId is needed both to create the item and for the comment, but so far
// only the id of the type itself is known — step 1 goes for entityTypeId.
// --- step 1: find the smart process by its title
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)
}
// The method wraps the response in an object with the types key. Two smart processes
// may have identical titles, so the response is a list even with
// an exact filter.
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("parse smart processes: %w", err)
}
if len(types.Types) == 0 {
return fmt.Errorf("smart process %q not found", spaTitle)
}
// id is the sequential number of the smart process, entityTypeId is the ID of its
// TYPE. Further on you need exactly entityTypeId, these are different numbers.
entityTypeID := types.Types[0].EntityTypeID
fmt.Printf("smart process %q: id=%d, entityTypeId=%d\n",
types.Types[0].Title, types.Types[0].ID, entityTypeID)
itemID, err := addItem(ctx, core, entityTypeID, "Laptop procurement")
if err != nil {
return err
}
defer del(ctx, core, "crm.item.delete", b24.Params{
"entityTypeId": entityTypeID, "id": itemID,
})
// --- step 2: add a comment to the item timeline
// ENTITY_TYPE for a smart process is the string "DYNAMIC_" + entityTypeId.
// Timeline fields are written in UPPERCASE, whereas crm.item.* accepts
// camelCase: one entity, two conventions in a single scenario.
res, err = core.Call(ctx, "crm.timeline.comment.add", b24.Params{
"fields": b24.Params{
"ENTITY_ID": itemID,
"ENTITY_TYPE": "DYNAMIC_" + strconv.Itoa(entityTypeID),
"COMMENT": "Confirm the purchase via email!",
},
})
if err != nil {
return fmt.Errorf("crm.timeline.comment.add: %w", err)
}
// There is no wrapper here at all: result is the ID of the timeline
// record itself, as a bare number.
var commentID b24.ID
if err := json.Unmarshal(res.Result, &commentID); err != nil {
return fmt.Errorf("parse comment ID: %w", err)
}
fmt.Printf("comment %d added to item %d\n", commentID, itemID)
return nil
}
// --- helpers: data setup and cleanup
// addType creates a smart process. entityTypeId is deliberately not passed: it is
// issued by the portal, and that is exactly what step 1 goes for.
func addType(ctx context.Context, core *b24.Core, title string) (b24.ID, error) {
// isRecyclebinEnabled is disabled deliberately: an item in the recycle bin still
// counts as an item, and crm.type.delete refuses to delete a type
// that has items.
res, err := core.Call(ctx, "crm.type.add", b24.Params{
"fields": b24.Params{"title": title, "isRecyclebinEnabled": "N"},
})
if err != nil {
// On plans without smart processes, the method responds with a dedicated code.
// The code is compared with errors.Is rather than as a string: a typo in the literal
// would compile and silently take a different branch.
if errors.Is(err, b24.Code("CREATE_DYNAMIC_TYPE_RESTRICTED")) {
return 0, fmt.Errorf("a smart process cannot be created on this portal: %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("no type.id in %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("no item.id in %s", res.Result)
}
var id b24.ID
return id, json.Unmarshal(raw, &id)
}
// del removes what was created. A cleanup error is printed but not returned: it must not
// mask the real error of the scenario.
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, "cleanup, %s: %v
", method, err)
}
}
```
Continue Learning
- Add Comment crm.timeline.comment.add
- Get a List of Comments From crm.timeline.comment.list
- Update Comment crm.timeline.comment.update
- Delete Comment crm.timeline.comment.delete
- Get a list of custom types crm.type.list
- Get a List of Items crm.item.list
- Data Types and Object Structure in the CRM REST API