CRM: amoCRM and Bitrix24

There are no ready-made “create a deal”, “find a contact”, “move it down the pipeline” actions here. Each CRM gets a pair of nodes — one that picks the connection and one universal call — and that call is a thin wrapper over the CRM’s REST API: you write which method to invoke and what to put in the body, while the platform takes care of the address, the auth header, the timeout and the JSON parsing. Which means the CRM’s own documentation stays open next to the editor: method names, field names and the shape of the response are theirs, not ours. That is the price of being universal — every method the CRM ships is available the day it ships, instead of waiting for us to write a node with “Deal title” and “Amount” fields.

What a connection stores, and what authenticates the call

Credentials live in the workspace rather than in the graph — the general mechanism is described in the integrations overview. What matters here is that the two CRMs do it differently.

Connection kindFieldsWhat authenticates the call
amoCRMSubdomain + long-lived tokena Bearer header carrying the token
Bitrix24Incoming webhook URL (+ portal domain, optional)the URL itself: the webhook code sits in its path

amoCRM. The Subdomain field takes either a bare account label (mycompanymycompany.amocrm.ru) or a full host in one of three allowed zones: amocrm.ru, amocrm.com, kommo.com. The zone list is closed, and not out of caution: the value is interpolated into the address of a request that carries your token — an arbitrary host in that field would mean a saved connection can walk the token anywhere. The token is long-lived, issued in the integration settings of the amoCRM account itself; the platform runs no OAuth refresh flow, so the token’s lifetime is yours to watch, and on the day it expires every call starts answering 401.

Bitrix24. For an incoming webhook the credential is the entire URLhttps://<portal>.bitrix24.ru/rest/<user id>/<code>/ — there is no separate token. That is why the field is stored as a secret and never handed back to the browser. A practical consequence: the rights of a call are decided not by the platform but by whose account the webhook was created under and with which scopes. An “insufficient permissions” reply is fixed in Bitrix24, not in the workflow. The Portal domain (optional) field does not affect the call at all — it is cross-checked against the webhook’s host when you save, and it catches exactly one mistake: a webhook pasted from a different (or already regenerated) portal.

Two nodes per service

The shape is identical for both CRMs. The config node (amoCRM Config, Bitrix24 Config) does nothing but select a saved connection and hand it over a data wire into the calling node’s port (amoCRM REST Call, Bitrix24 REST Call). Execution order is set by the ordinary execution chain — the configuration plugs in from the side and does not affect it.

Entry
Bitrix24 REST Call
Bitrix24 Config
Exit
  • Execute + Data
  • Data
The connection arrives on its own data wire; the sequence is set by the chain above.

A second node for a single call is not compulsory: the calling node carries its own connection picker. The precedence rule is the same across the whole integration family — a wired port beats the field — and the form says so by dimming the field.

While neither is filled in, the editor shows a wire.required_port_not_wired warning, and deploy and run reject such a graph (the codes are listed on Validation codes). A filled-in field counts as a source just like a wire does, so the warning disappears either way.

The config node’s output is redacted in the run log: only the connection’s name survives, without the token and without the webhook URL. Worth remembering when you stare at a run and cannot find the value — it is not lost, it is deliberately not written to the database.

How a request is composed

NodeFieldsWhat goes in them
amoCRM REST Callmethod / path / bodyan HTTP verb from a list; a path starting with a slash; a JSON body
Bitrix24 REST Callmethod / paramsa dotted method name; a JSON object of parameters

The difference is not cosmetic. amoCRM addresses things “the HTTP way”: GET /api/v4/leads, POST /api/v4/contacts, PATCH /api/v4/leads/12345. The verb is picked from a fixed list (GET, POST, PATCH, PUT, DELETE) — there is no free text there because there is nothing to type. Bitrix24 addresses things “the RPC way”: the method name is dotted — crm.lead.add, crm.deal.list, user.get — and fully determines what happens; the path and the verb are assembled by the platform.

path, body, Bitrix24’s method and params are templated fields: {{ inputs.<port> }}, {{ nodes.<id>.output.<field> }}, {{ variables.<name> }} and {{ secret.<NAME>}} all work there as everywhere else (see Templates and variables). That is exactly how data gathered by earlier nodes gets in:

/api/v4/leads/{{ nodes.find_lead.output.result._embedded.leads[0].id }}

Three things people trip over the first time:

  • the path starts with a slash. It is appended to the host verbatim, with no normalisation: api/v4/leads without the leading slash produces a nonsense address and an unhelpful error;
  • an empty body and {} are not the same thing. The default value of body is literally {}, and that is a non-empty JSON object — it will be sent as the request body. For a plain GET, clear the field completely, and then no body is sent at all. Bitrix24 is the reverse: empty params become {}, because an argument-less method is perfectly normal;
  • invalid JSON fails the node, it does not silently send nothing. The error carries the VALIDATION code and the parser’s line and column. Building JSON by string concatenation in a template is the surest way to meet it; assembling the object in a separate node and passing it whole — {{ nodes.build_params.output }} — is sturdier.

Reading the answer

Both nodes put the parsed JSON into the result field of their output, so downstream it reads as:

{{ nodes.crm_call.output.result }}

What sits inside is up to the CRM. Bitrix24’s {"result": …, "time": …} envelope is unwrapped by the platform: result holds the contents of that field, not the envelope. For amoCRM there is nothing to unwrap — the response body is handed over as it is, with all of its structure (_embedded.leads, _links, _page and the rest), so the path to a value can get long. The easiest way to learn the shape is a run in the test panel: it shows every node’s input and output in full (see Testing a workflow).

Where an error goes

The clients of both CRMs are built never to raise: a transport failure, a 401, a 404 and an “error inside the envelope” reply all fold into the same kind of result. The node is what raises — otherwise a call with a dead token would end in a green run and an empty value would travel down the graph.

What happens next depends on one checkbox in the node’s configuration:

Error output
on — a second output appears on the node

Off by default: without it a failure fails the whole run.

  • off (the default) — the node’s failure fails the whole run: it ends up failed, and the nodes downstream never execute;
  • on and wired — control leaves along the error branch and the run stays successful. The branch receives an object with message (the author-facing text), error_code, failed_node_id and failed_node_type. Branch on error_code: it is a stable string, fit for comparison in an if node.

One subtlety only practice reveals: amoCRM distinguishes kinds of failure, Bitrix24 does not. amoCRM answers with an HTTP status, so a 401 becomes AUTH, a 404 becomes NOT_FOUND, a 402 becomes BILLING. Bitrix24 (like VK below) answers 200 with the error inside the body, which could only be told apart by its text, so its failures all arrive as PROVIDER_ERROR with the specifics in message. If your branch must tell “no rights” from “no such object”, for Bitrix24 you will be reading that text.

Who sees what: the workflow’s author gets our sentence plus the CRM’s verbatim words (in the log, in the error branch, in the test panel); a chat visitor gets only a neutral phrase derived from the code, with no provider internals. The full account is on Errors; run statuses are on Statuses.

The typical scenario: a request in chat → fields → a deal

The most common chain looks like this: a person writes free-form text in the chat, a model extracts fields out of it, a CRM call creates the object, and the CRM’s answer turns into a reply for the human.

Entry
Request fields
LLM
crm.lead.add
Bitrix24 Config
Exit
  • Execute + Data
  • LLM
  • Data
Free text → structure → CRM call → an answer in the chat.
  1. Parse the message. The Structured Output LLM node returns JSON matching a schema you define — name, phone, what the person wants — rather than free text. It is the schema, not “ask the model to answer in JSON”, that makes the next step predictable.
  2. Assemble the parameters. params holds the method’s object in the CRM’s own terms: {"fields": {"TITLE": "{{ nodes.fields.output.title }}", "NAME": "{{ nodes.fields.output.name }}"}}. The field names come from Bitrix24’s documentation; a portal with custom fields configured will also have codes shaped like UF_CRM_….
  3. Call the method. crm.lead.add for a lead, crm.deal.add for a deal. The answer is the id of the created object — handy if a comment or a task has to be attached with a second call.
  4. Answer the human. The exit node holds the text with a link to the created object, assembled from {{ nodes.crm_call.output.result }}.
  5. Decide what a failure means. For a customer’s request, failing silently is the worst outcome: turn the error output on and send an honest “we could not file it, but we have your contact” down that branch, plus a notification to yourself.

How such chains are built in general — branching, templates, composing the reply — is covered in Tutorial: logic and branching. There is no ready CRM template in the examples right now: none of the built-in templates touches a CRM, because a template has to work the moment it is installed and a CRM call cannot work without your portal and your token. The nearest starting point is the field-extraction template, with the call added on top.

VK and VK Teams: outbound messages only

The four nodes next to the CRM ones — VK Config and Send VK Message, VK Teams Config and Send VK Teams Message — are built the same way: a connection from the side, templated fields, the same error-output checkbox. But they do exactly one thing: send a text message.

NodeThe connection holdsFieldsWhat it returns
Send VK Messageaccess token + API versionpeer_id, textmessage_id
Send VK Teams Messagebot token (+ API base URL)chat_id, textmessage_id

The practical consequence: both nodes make sense as a notification — “a request came in”, “the nightly sync failed”, “a customer is waiting” — into a staff conversation, not as a channel for talking to customers. For VK the peer_id encodes the recipient’s kind in the number itself: a user is their id (12345), a community is negative (-98765), a group conversation is 2000000000 plus the conversation number (conversation 5 → 2000000005). Getting it wrong sends the message to the wrong recipient rather than failing — VK will faithfully deliver it to whoever’s id you named. For VK Teams the chat identifier is a colleague’s email (user@example.com) or a group chat id (681234567@chat.agent). The API base URL (optional) field is only for those running Teams on their own server; leave it empty for the cloud one.

What next