Turn Matrix Into Your CRM: Leads, Custom Fields, and Zoho Sync
Because everything is an entity, Matrix doubles as a lightweight AI CRM your agents write to directly — per-org custom fields and optional Zoho sync included.
Most teams running voice or chat agents end up with the same gap: the agent has a great conversation, learns something useful — the lead's budget, their timeline, the reason they're shopping — and then that knowledge evaporates. It lives in a transcript nobody reads, or it gets re-keyed into a separate CRM by hand the next morning.
That gap exists because the agent and the CRM are two different systems. In Matrix they don't have to be. Everything in the platform is already an entity in the graph — agents, sessions, contacts, and yes, leads — so the same agent that's on the call can write to your pipeline while the call is happening. No export, no nightly job, no re-keying.
This post is for the ops and RevOps people who own the pipeline. It shows how to use Matrix as a lightweight AI CRM: the built-in Lead and Contact types, how to add your fields without forking anything, the tools that let an agent fill those fields mid-conversation, the workspace where you work the list, and the optional sync that mirrors everything into Zoho.
Everything is an entity — including your pipeline
Matrix has no hand-rolled CRM module. It doesn't need one. The whole platform is built on a generic entity model: EntityType defines a shape, EntityNode rows hold the data, and PropertyDefinitions describe the fields. A Lead is just an EntityType like any other. (For the full mechanics, see Everything Is a Node: The Generic Entity Model.)
Two CRM types ship globally, available to every org out of the box:
Lead— a person in your pipeline, with a disposition framework: a status, a disposition code, a sub-disposition, outcome notes.Contact— the channel-bound person your agents actually talk to (this is theUserof typeCONTACT, joined to every chat and call byuserId, carrying the per-contact memory pool).
Because these are real entities, they get everything the platform already gives entities for free: multi-tenant isolation on every row, the shared admin table and drawer, server-side filtering, and central access control. You're not bolting a CRM onto the side — the pipeline is a first-class citizen of the same graph your agents reason over.
Add your fields without forking the platform
Here's the constraint that makes this actually usable: your pipeline is not our pipeline. A staffing firm tracks roleSought and noticePeriod; an admissions team tracks program and intakeTerm; a lender tracks loanAmount and creditBand. A generic Lead with five fixed fields would be useless to all of them.
Matrix solves this with a same-named, org-owned EntityType overlay. You create an EntityType named Lead owned by your org, listing exactly the fields you want. The platform's entity resolution is org-aware: when your org reads or writes a Lead, the EntityManager resolves your overlay; everyone else still gets the global default. You add fields without touching src/main/java, without a fork, and without affecting any other tenant.
A custom field is just a PropertyDefinition. Here's an overlay that adds a couple of staffing-specific fields on top of the standard Lead disposition shape:
{
"name": "Lead",
"schemaJson": {
"properties": [
{ "name": "roleSought", "type": "STRING", "description": "Title the candidate is targeting" },
{ "name": "noticePeriod", "type": "STRING", "description": "How soon they can start" },
{ "name": "expectedCtc", "type": "NUMBER", "unit": "INR/yr" },
{ "name": "dispositionCode","type": "STRING", "options": ["INTERESTED", "CALLBACK", "NOT_FIT", "DNC"] }
]
}
}
A few things worth calling out:
descriptionandunitaren't decoration — they get surfaced to the agent so it knows what each field means and what to put in it.optionsondispositionCodeis the disposition enum. You edit those values from the admin enum editor in the UI — adding a new disposition is a UI action, not a deploy.- Field types are the same primitives every entity uses:
STRING,NUMBER,BOOLEAN,DATE,ENTITY.
One discipline to respect: the platform seeder that maintains the global Lead type stays global-only by design. It uses a name-based
MERGE, and a name-basedMERGEagainst your same-named overlay would clobber it. The rule the core enforces is that domain specifics live in data — your org-owned overlay — never in the shared seeder. Your custom fields are safe precisely because nothing in the platform's boot path reaches for an org-named type.
The agent fills the pipeline — mid-conversation
A CRM is only as good as the data in it, and the whole point here is that the agent puts it there. Matrix gives every agent a dynamic write tool that adapts to your overlay: update_lead (and its sibling update_contact).
"Dynamic" is the key word. The tool's parameters are generated from your Lead overlay. Add noticePeriod to the overlay and update_lead immediately accepts a noticePeriod argument — no code change, no redeploy. The agent sees a tool whose schema matches your pipeline.
During a conversation, the agent calls it as it learns things:
{
"name": "update_lead",
"args": {
"roleSought": "Backend Engineer",
"noticePeriod": "30 days",
"expectedCtc": 2800000,
"dispositionCode": "INTERESTED",
"outcomeNotes": "Open to remote; currently interviewing elsewhere, wants to move by Q3."
}
}
That write lands on the lead's row in the graph the moment the tool fires — not after the call, not in a batch.
The one operational reality to internalize: the agent captures fields by deciding to call update_lead. It is a tool the model invokes, so the model has to choose to invoke it, and it can only fill fields your overlay actually defines. Two practical consequences:
- Capture early and often. Coach the agent — in its persona or the per-call objective — to write each fact down as soon as it's confirmed, rather than saving everything for one big call at the end of the conversation. A model that waits often runs out of turn before it commits the write.
- If a field never gets filled, check your overlay first. A lead value the agent clearly heard but didn't save is usually a field that simply isn't in your overlay — the tool silently has nowhere to put it. Add the
PropertyDefinition, and it starts landing.
This is generic-platform behaviour: the tool is domain-agnostic in the core, and it becomes your CRM tool purely because it reads your schema.
Working the list: the global Leads workspace
Captured leads need somewhere to live and be worked. That's /admin/leads — a dedicated workspace built on the same shared entity table and drawer that powers every admin surface.
It's a real list view, not a toy:
- Server-side filtering and faceting. It drives the generic
GET /api/entitiesendpoint withfilter,q,sort, andrefparams, plus a/facetscall that returns the value distribution for a field — so you can slice bydispositionCode, search across names and numbers, and sort, all evaluated on the backend rather than in the browser. - Inline disposition. Update a lead's disposition straight from the row.
- The shared drawer. Open any lead into a tabbed drawer — its details, its disposition, the call/chat activity (transcripts included), and related records.
Because filtering is the generic entity query, the same machinery works for any type you define, not just leads. Leads just get a first-class nav item and a tuned column set.
Campaigns feed the pipeline automatically
If you run outbound calling campaigns, the loop closes itself. A campaign snapshots its audience into CampaignContact rows, dials each one, and tracks the outcome from the telephony status callback. Those campaign contacts roll into your Lead pipeline — the disposition the agent writes on the call, and the COMPLETED / FAILED / NO_ANSWER outcome the callback records, both land on the lead.
So the motion is: a campaign dials a list, the agent has a real conversation, it writes update_lead with what it learned and how the call went, and you open /admin/leads to a freshly dispositioned pipeline. No handoff between "the calling tool" and "the CRM" — there isn't one.
Optional: mirror writes into Zoho
Sometimes Matrix is the CRM. Sometimes it's the front line and Salesforce-or-Zoho is the system of record the rest of the company already lives in. Matrix supports the second case with a hook-based integration, not a rewrite of where your data lives.
Every entity write in the platform publishes an EntityWrittenEvent. A LeadCrmSyncListener subscribes to those events and mirrors lead writes into Zoho CRM. When an agent calls update_lead and the row changes, the listener picks up the event and pushes the same data to Zoho — so a lead the agent captured on a call shows up in your existing CRM without anyone copying it across.
It's worth being precise about what this is: it's an event-driven sync hook, an integration that mirrors writes outward. Matrix remains where your agents read and write in real time; Zoho stays your system of record if that's how your org is set up. Two systems, one source of truth, kept in step by an event listener — instead of a nightly export and a prayer.
Takeaway
You don't stand up a separate CRM to capture what your agents learn. Because the pipeline is just another entity in the same graph:
LeadandContactship globally, with a real disposition framework.- Your fields are an org-owned overlay — a same-named
EntityTypethe org-awareEntityManagerresolves for you, so you customize without forking. update_lead/update_contactadapt to your schema, and the agent writes to them during the conversation — so capture early and often, and treat a missing field as a missingPropertyDefinition./admin/leadsis your worked list, with server-side filtering and faceting.- Campaign outcomes roll into the pipeline, and an
EntityWrittenEventhook can mirror leads into Zoho when Matrix is the front line and not the system of record.
The thing that makes all of this hang together is the discipline underneath it: the core stays generic, your domain lives in data. That's why you get a CRM that's yours without maintaining a fork of ours.
Get started
Open /admin/entities, create an org-owned EntityType named Lead with the fields your pipeline actually tracks, point an agent at a few calls, and watch /admin/leads fill in real time. Want the calling motion that feeds it? Walk through From Zero to Outbound Campaign: A Recruiter Agent Walkthrough. Curious how the overlay trick works under the hood? Read Everything Is a Node: The Generic Entity Model. Then spin up a workspace and turn your agents into a pipeline that fills itself.
Build your first agent on Matrix
Spin up a workspace, wire up tools and knowledge, give your agent a voice, and talk to it in real time — no agent code required.