How to Add a Comment to the Timeline of a Smart Process

Scope: crm

Who can execute the method: users with permission to modify the CRM object

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.

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.

To create a comment in a smart process entity, we will sequentially execute two methods:

  1. crm.type.list — retrieve the smart process using a filter.
  2. crm.timeline.comment.add — create the comment.

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.

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'
        });
        
// 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']
        );
        
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
        
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 SPA in Bitrix
  • entityTypeId: 177 — the SPA 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
                    }
                ]
            }
        }
        

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 the entityTypeId filter equals the entityTypeId value from crm.type.list
  • ENTITY_TYPE — specify DYNAMIC_177. The value consists of entityTypeId from the previous method's result and the dynamic object prefix DYNAMIC_
  • COMMENT — the text value of the comment
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'
        });
        
$result = $sb->getCRMScope()->timelineComment()->add(
            [
                'ENTITY_ID' => 19,
                'ENTITY_TYPE' => 'DYNAMIC_177',
                'COMMENT' => 'Confirm the purchase via email!',
            ]
        );
        
response = client.crm.timeline.comment.add(
            fields={
                "ENTITY_ID": 19,
                "ENTITY_TYPE": "DYNAMIC_177",
                "COMMENT": "Confirm the purchase via email!",
            }
        ).response
        
// 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
        }
        

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();
        
<?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);
        
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)
        
// 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