CRM Manager
One node reads and writes every object in your CRM. Entity plus operation plus data is the whole mental model — and the write path is a JSON string, not a form.
Most of what an automation does to your CRM is small and repetitive. Mark the contact qualified. Open a deal. Attach a note. File a task against the record someone just touched. None of that is interesting work, and all of it has to happen before anything downstream is worth doing.
The CRM Manager is the one node that does all of it. It is not a contact node with a company node beside it and a deal node beside that — it is a single node where you pick which object you are working on and what you want done to it. Once that is wired, a workflow can move data through your CRM the same way a person would, and considerably faster.
Learn three fields and you have the node: which entity, which operation, and the data. Everything else in the panel is one of those three getting more specific.
Entity, operation, data
| Field | What it holds |
|---|---|
entity_type | Type of CRM entity or relationship to manage One of: contact · company · deal · note · task · product · activity · deal_contact · deal_company · contact_company · deal_product · deal_payment · deal_payment_schedule Defaults to contact. |
operation | Operation to perform. Use batch_* for multiple items at once. One of: create · update · get · search · delete · link · unlink · update_relationship · batch_create · batch_update · batch_delete Defaults to get. |
json_data | JSON string of entity data for create/update (preferred write path). On update: "" blanks a field, null leaves it unchanged, custom_fields merge. |
entity_id | Entity ID or email (for contacts) for operations that require it |
create_if_not_found | For contact updates: if contact not found by email, create it (upsert behavior) Defaults to true. |
entity_type
- What it holds
- Type of CRM entity or relationship to manage One of:
contact · company · deal · note · task · product · activity · deal_contact · deal_company · contact_company · deal_product · deal_payment · deal_payment_scheduleDefaults tocontact.
operation
- What it holds
- Operation to perform. Use batch_* for multiple items at once. One of:
create · update · get · search · delete · link · unlink · update_relationship · batch_create · batch_update · batch_deleteDefaults toget.
json_data
- What it holds
- JSON string of entity data for create/update (preferred write path). On update: "" blanks a field, null leaves it unchanged, custom_fields merge.
entity_id
- What it holds
- Entity ID or email (for contacts) for operations that require it
create_if_not_found
- What it holds
- For contact updates: if contact not found by email, create it (upsert behavior) Defaults to
true.
Two things in that table are worth stopping on, because they are where this node stops behaving like a form.
Writes go in json_data, as a string. There is a data object in the schema and it looks like the obvious place to put a record — it is not the reliable one. Put your fields in json_data as JSON text. Some fields can only be set there: a deal's stage has no native config field at all, so {"stage_name": "Qualification"} is the only way to set it.
Update targets a record by ID — or, for contacts, by email. Put either in entity_id. And because create_if_not_found defaults to on, an update against an email that does not exist creates the contact. That is an upsert, and it is the reason lead-capture workflows do not need a create branch and an update branch. It is also a surprise the first time you meant to update strictly.
A worked example
A contact is created in the CRM. Three CRM Managers follow: one marks the contact qualified, one opens a deal, one links the deal back to the contact.
Three operations, one node type
Update, create, link — the same node, configured differently each time.
Scroll for all 4 steps →
Read the three nodes as one shape repeated. Each one names an entity, names an operation, and carries its payload as JSON text. Nothing else changes.
crmmanager_1updates a contact and targets it by email from the trigger.create_if_not_foundis off here on purpose: the contact certainly exists, because its creation is what started the run.crmmanager_2creates a deal, and sets its stage throughstage_name— the field with no form control.crmmanager_3links the two.deal_contactis a junction entity: it is not a record you look at, it is the relationship between two records. Linking is an operation like any other.
The last node is also where the outputs earn their keep. It reads {{crmmanager_2.entity.id}} and {{crmmanager_1.entity.id}} — the ids of records that did not exist when the workflow was saved. That is the normal way to chain CRM work: create something, then use what came back.
What comes out
| Reference | What you get |
|---|---|
{{alias.entity.id}} | The record's id, after a create, update or get. The field you pass into the next node. |
{{alias.entity_id}} | The same id, at the top level. |
{{alias.results}} | The array a search returns. Feed it to a Loop. |
{{alias.total}} | How many a search matched. |
{{alias.operation}} · {{alias.entity_type}} | What this node just did, echoed back. |
{{alias.entity.id}}
- What you get
- The record's id, after a create, update or get. The field you pass into the next node.
{{alias.entity_id}}
- What you get
- The same id, at the top level.
{{alias.results}}
- What you get
- The array a search returns. Feed it to a Loop.
{{alias.total}}
- What you get
- How many a search matched.
{{alias.operation}} · {{alias.entity_type}}
- What you get
- What this node just did, echoed back.
Older material says {{crmmanager_1.result.id}}. It does not resolve. The single-record output is entity, and the search output is results. Screenshots and scripts from earlier product versions still show result, and a template that references it fails quietly — the downstream node receives an empty string rather than an error. If a value arrives blank and you cannot see why, check this first.
Which operation
| Operation | Reach for it when |
|---|---|
get | You know exactly which record you want, and you want one. |
search | You want every record matching a filter. Returns an array plus a count; pair it with a Loop. |
create | The record definitely does not exist yet. |
update | The record exists — or you want it created if it does not. This is the default choice for anything keyed on an email. |
link / unlink | You are relating two records rather than changing either one. |
batch_create / batch_update / batch_delete | You have many records and would otherwise wrap this node in a Loop. One node, one set of outputs, kinder to rate limits. |
get
- Reach for it when
- You know exactly which record you want, and you want one.
search
- Reach for it when
- You want every record matching a filter. Returns an array plus a count; pair it with a Loop.
create
- Reach for it when
- The record definitely does not exist yet.
update
- Reach for it when
- The record exists — or you want it created if it does not. This is the default choice for anything keyed on an email.
link / unlink
- Reach for it when
- You are relating two records rather than changing either one.
batch_create / batch_update / batch_delete
- Reach for it when
- You have many records and would otherwise wrap this node in a Loop. One node, one set of outputs, kinder to rate limits.
The batch row is the one people find late. A Loop around a CRM Manager works, and for twenty records it is fine. For hundreds it is slow and it makes the execution log unreadable. If the list is already in hand, batch it.
What breaks
Watch out: WRITES go in `json_data` (a JSON STRING), not a `data` object — the object is dropped on write.
A deal's stage has no native field — set it only via json_data: '{"stage_name": "..."}'.
custom_fields update MERGES: "" blanks a value, JSON null leaves it unchanged, omitted = unchanged.
SEARCH cannot filter by a custom field (it's ignored) — use a database_query SELECT instead.
Read results as {{alias.results}} / {{alias.results[0].id}}; a single entity as {{alias.entity.id}}.
The one that costs the most time is the search filter. filters covers standard columns only, and a custom-field filter is not rejected — it is ignored. So the search runs, returns records, and returns the wrong ones: everything that matched the rest of the filter, unfiltered by the condition you actually cared about. Nothing in the run looks failed. When you need to filter on a custom field, use a Database Query instead.
The update semantics are the second. On an update, an empty string blanks the field, JSON null leaves it alone, and a key you leave out is untouched. Building the payload from upstream variables that might be empty is therefore how a workflow quietly erases the phone numbers it was supposed to be enriching. Send null, not "", for anything you might not have.
Try it
- Add a CRM Manager, set the entity to
contactand the operation tosearch, and give it a filter you know matches a couple of records. Run it and read{{alias.total}}. - Change the operation to
update, put a real contact's email inentity_id, and setjson_datato{"lead_status": "qualified"}. Run it and check the record. - Now put an email that does not exist in your CRM in the same node and run it again. A new contact appears — that is
create_if_not_found. Turn it off and run it a third time to see the difference. - Finally, add a second field to
json_datawith the value""and watch it blank that field on the record. This is the failure worth having once, on purpose, in a workflow you do not care about.
Next: Contact Summary — one node that reads a contact's whole history and writes the brief a rep would have had to assemble by hand.
Related lessons
Base rates — what a piece of evidence is actually worth
A face-recognition system that is 99.9% accurate and almost entirely wrong, and a number that sent an innocent woman to prison. Both are the same arithmetic, and it is the arithmetic that decides what any piece of evidence is worth.
ReadConfirmation and survivorship — what you never looked for
Two questions about evidence you did not go looking for. One is a rule you have to discover, and one is a pattern in five famous people — and in both, the thing that would have told you the truth is the thing nobody checks.
ReadLoss aversion, sunk cost and regression — what it costs you
Four questions you answer about yourself rather than about a scenario, and your own answers are the finding. Then the pattern that makes praise look useless and criticism look like it works, whatever you actually do.
Read
