Custom Modules turn Freshsales from a CRM into a lightweight database. Subscriptions, properties, vehicles, contracts — anything you would otherwise track in a spreadsheet. Most teams over-model on day one and regret it.
Modeling rules of thumb
- One module per noun your business cares about
- Lookup fields, not duplicated data
- Validate cardinality before you build (1:N vs N:N)
- Use record types within a module, not five near-identical modules
Cardinality first
If a contact can own multiple subscriptions, that is 1:N (Subscription has lookup to Contact). If a subscription has multiple contacts (billing + technical), that is N:N. Freshsales N:N is implemented via a join module — plan it explicitly.
Subscription
├─ lookup: Account (1:N)
└─ join: SubscriptionContact (N:N to Contact)
├─ lookup: Subscription
└─ lookup: Contact
└─ field: role (billing, technical, exec)
Field type pitfalls
- Decimal field has fixed precision; choose 2 vs 4 at create time
- Multi-select dropdown values cannot be deleted once data exists
- Lookup fields cannot change target module; you have to recreate
Use record types for variants
A “Contract” module might have Master, Amendment, Renewal types. Do not build three modules. Use a record type field with a workflow that controls visible fields per type.
API patterns
Custom Module records use the same API surface as core modules:
POST /api/cpq/v1/modules/{module_name}/records
{
"data": {
"name": "ACME Annual 2026",
"subscription_status": "active",
"linked_account_id": 9921
}
}
Reporting limits
Custom Modules show up in Analytics, but cross-module joins beyond two hops can hit query limits. Pre-aggregate if you need three-hop reports — store rolled-up values on the parent.
Permissions
Field-level permissions on Custom Modules require Enterprise tier. On Pro tier, plan around module-level access rather than field-level.
What to do this week
List the nouns you currently track outside Freshsales, draw the cardinality diagram before opening the admin panel, pick decimal precision deliberately, and use record types instead of cloning modules.