CRM & Pipeline Management
Olyron CRM's CRM tracks a prospect through three record types — a lead you are still qualifying, a contact you have converted, and one or more deals moving through a pipeline. Every record is scoped to your tenant, ranked with an AI Next Best Action, and searchable from a single global bar. This page explains the data model, how records flow between stages, and how to work with tasks, notes, and activities against them.
#The data model: leads, contacts, and deals
Three tables form the backbone of the CRM. A crm_leads row is an unqualified prospect. Once qualified, you convert it into a crm_contacts row — a person you actively work with. Revenue opportunities against a contact are tracked as crm_deals, each sitting in one stage of a pipeline. Every one of these tables carries a tenant_id, so records are isolated per organization and never leak across tenants.
| Record | Table | Represents | Key fields |
|---|---|---|---|
| Lead | crm_leads | An unqualified prospect you are still working | first_name, last_name, email, phone, status, source, assigned_to |
| Contact | crm_contacts | A qualified person, converted from a lead | first_name, last_name, email, phone, household_id, custom_fields |
| Deal | crm_deals | A revenue opportunity in a pipeline stage | title, amount, stage_id, probability, expected_close_date, contact_id, assigned_to |
status = 'converted'. That keeps your qualified book (contacts) clean and separate from raw top-of-funnel volume (leads).All three tables include a custom_fields JSONB column, so any tenant-defined field (configured under Custom Fields) rides along with the record without a schema change. Leads and contacts can also be grouped into a crm_households record via household_id for family or business units.
#Pipelines and stages
A pipeline (crm_pipelines) is an ordered set of stages (crm_pipeline_stages) that a deal travels through. Each stage has a name, an order_num that sets its position, and a probability (0–100) representing the odds of closing once a deal reaches it. Every deal references exactly one stage via stage_id, and pipelines are per-tenant with one marked is_default.
nametext- Display name of the stage, e.g. "Discovery" or "Proposal Sent".
order_numint- Position in the pipeline. Stage ordering (and the first stage used on conversion) is driven by ascending order_num.
probabilityint (0–100)- Close likelihood associated with the stage. A deal created in the first stage during conversion starts at probability 10.
pipeline_iduuid- The pipeline this stage belongs to.
#Creating and moving deals
Deals are managed through /api/crm/deals (list and create) and /api/crm/deals/[id] (fetch, update, delete). Moving a deal between stages is just a PUT that changes its stage_id — there is no separate "move" endpoint. You can update the amount, probability, expected close date, owner, or any custom field the same way.
- 1Create the dealPOST /api/crm/deals with a title (required) and optionally amount, stage_id, probability, expected_close_date, and contact_id. If you omit assigned_to, the deal is assigned to you; if you omit probability, it defaults to 50.
- 2Advance it through the pipelinePUT /api/crm/deals/[id] with a new stage_id to move the deal to the next stage. Update probability alongside it to reflect the new stage's odds.
- 3Close or remove itSet the final stage and update the record, or DELETE /api/crm/deals/[id] to remove it. Deletion is restricted to org_owner and agency_admin roles; other roles receive a 403.
curl -X POST https://<your-tenant>.olyron.app/api/crm/deals \
-H "Content-Type: application/json" \
--cookie "<session>" \
-d '{
"title": "Term life — J. Rivera",
"amount": 4200,
"stage_id": "8f1c...uuid",
"probability": 25,
"expected_close_date": "2026-08-15",
"contact_id": "b23d...uuid"
}'curl -X PUT https://<your-tenant>.olyron.app/api/crm/deals/<deal-id> \
-H "Content-Type: application/json" \
--cookie "<session>" \
-d '{ "stage_id": "c90a...uuid", "probability": 60 }'#Converting a lead to a contact and deal
When a lead qualifies, convert it with POST /api/crm/leads/[id]/convert. The route runs a small workflow per tenant: it copies the lead's name, email, phone, and custom_fields into a new contact; optionally spins up a first deal; reassigns the lead's notes to the new contact; marks the lead converted; and logs a system activity.
- 1Call convertPOST /api/crm/leads/[id]/convert. Pass createDeal: true (plus optional dealTitle and dealAmount) to open a deal at the same time.
- 2A contact is createdA new crm_contacts row is inserted with the lead's details. The lead itself is not deleted — its status becomes 'converted'.
- 3The deal (if requested) lands in stage oneIf createDeal is true, a deal is created against the new contact in the pipeline stage with the lowest order_num, at probability 10, titled dealTitle or "Deal for <first> <last>".
- 4Notes and history follow the personAny crm_notes attached to the lead are re-pointed to the new contact, and a "Lead Converted" activity is written so the timeline stays intact.
{
"success": true,
"contact": { "id": "…", "first_name": "…", "…": "…" },
"deal": { "id": "…", "title": "Deal for J. Rivera", "…": "…" },
"notesTransferred": 2,
"message": "Lead converted to contact with deal. 2 note(s) transferred."
}#Tasks, notes, and activities
Every record can carry follow-up work and a history trail. Tasks (crm_tasks) are the to-dos, notes (crm_notes) are free-form context, and activities (crm_activities) are the timeline of calls, emails, and system events. Tasks and notes attach to any record through the polymorphic pair related_to_type (e.g. "lead", "contact", "deal") and related_to_id.
Tasks
titletext- What needs doing. Required.
priorityenum- One of low, medium (default), or high.
statusenum- One of pending (default), in_progress, completed, or cancelled.
due_datetimestamptz- When the task is due; drives overdue sorting.
assigned_to / related_to_iduuid- Owner of the task, and the record it hangs off of (paired with related_to_type).
The dashboard exposes quick-create screens for these: /leads/new, /tasks/new, and /activities/new all redirect into the dashboard's creation flows, so you can capture a lead, a follow-up, or a logged interaction from anywhere.
Notes and activities
A note is a body of text with an optional title, attributed to its author via created_by. Activities carry a type (such as "system", a call, or an email), an optional subject and body, and an activity_at timestamp, and they link directly to a contact_id, lead_id, or deal_id. When you convert a lead, the platform writes a system activity automatically — you rarely create those by hand.
#Global search and AI scoring
GET /api/crm/search?q=<query> searches contacts, leads, deals, and landing pages in one call. It runs a full-text query against crm_search_index and, in parallel, ILIKE fallbacks on the core tables, then merges and de-duplicates the results grouped by type. Queries must be at least two characters; limit defaults to 15 and is capped at 50.
{
"results": {
"contacts": [
{ "id": "…", "type": "contact", "title": "Jordan Rivera",
"subtitle": "jordan@example.com", "url": "/crm/contacts/…" }
],
"leads": [],
"deals": [
{ "id": "…", "type": "deal", "title": "Term life — J. Rivera",
"subtitle": "$4,200", "url": "/crm/deals/…" }
],
"landing_pages": [],
"other": [],
"total": 2
},
"query": "rivera"
}POST /api/crm/scoring computes an AI lead/deal/contact score. Send record_type plus a single record_id or a batch of record_ids (up to 200). The route weights data completeness (20%), engagement from activity count (20%), recency (15%), source quality (15%), email/phone presence (10% each), and field richness (10%) into a 0–100 score, then labels it hot (≥80), warm (≥60), neutral (≥40), or cold. The result is written to crm_predicted_scores and stamped onto the record's ai_score column.
For per-lead guidance, GET /api/crm/leads/[id]/nba returns a rule-engine Next Best Action; add ?ai=1 to layer on Claude coaching commentary (a no-op if ANTHROPIC_API_KEY is not configured). Advisors can pull their whole triage list at once from GET /api/crm/advisor/worklist, which returns assigned leads each already annotated with their primary next action.