Custom objects, one year in
A year after launching custom objects, our customers have created more than 31,000 of them. What we got right in the data model, what we had to rebuild, and what it taught us.

Jun Seo
Staff Engineer
Share

Photo:
When we launched custom objects last July, we expected teams to add one or two: a Partners object here, a Projects object there. A year later, customers have created just over 31,000 custom objects, with a median of five in each workspace that uses them. One investment firm has 23. We have learned a lot from watching what people build, and we have rebuilt parts of the data model twice.
The starting point
Every Modal workspace begins with three standard objects: people, companies and deals. Custom objects sit alongside them and behave identically. They have fields, views, permissions, relationships, reports and API endpoints, and there is no second-class version of anything.
That was the most important decision we made, and the one we would make again. Early prototypes treated custom objects as a lighter tier, stored as JSON on a generic table. They were quick to build and immediately limiting. You could not filter on their fields efficiently, sort by them, or point a relationship at them. Customers noticed within a week of the private beta.
What we store, and how
Each object has a schema made of typed attributes. Records are stored in a column-per-attribute layout per workspace, with the schema held separately and versioned. Adding a field is a metadata change plus a background backfill, never a table lock.
From the outside, the API reads the same for every object:
The object and id pair is how every relationship is written, whether it points at a standard object or one a customer created yesterday.
What we had to rebuild
Relationships
Version one supported a relationship from one object to another, in one direction. Customers immediately wanted the other side. If a deal has a “sourced by” partner, the partner record should list its deals without anyone setting that up twice.
We moved to relationships stored once and read from both ends. Creating a relationship now creates a pair of attributes, one on each object, kept consistent in the same transaction. It took four months and touched almost every query path, and it is the single change that made custom objects feel native.
Field types
We shipped with nine field types and now have seventeen. The ones that mattered most, in order of how often they were requested:
Multi-select, because almost every real-world category turns out to allow more than one answer.
Currency with a code, since a fund tracking investments in four currencies cannot use a plain number.
Record reference lists, for “this project involves these five people”.
Formula fields, computed at read time from other fields on the same record.
Imports
Moving from another CRM means mapping someone else’s data model onto yours. Our importer originally mapped fields one to one. It now proposes new objects as well: if a Salesforce org has a custom object with a lookup to accounts, Modal suggests the same structure and shows a preview of the first 50 records before anything is written.
What the numbers say
A few patterns from a year of real schemas:
The most common custom objects are Partners, Projects, Investors, Candidates and Subscriptions, in that order.
71% of custom objects have at least one relationship to a standard object, usually companies.
Workspaces with at least one custom object keep their data noticeably fresher. They update records 1.6 times as often as those without, probably because the CRM matches the work they actually do.
What we would tell another team
Build the flexible thing properly from the start, or not at all. Every shortcut we took to ship custom objects faster became a migration later. The cost of making them first-class was high, but it was paid once. The cost of a two-tier model would have been paid on every feature we have shipped since.
Next on the list is history for schema changes, so that renaming or retyping a field can be undone with one click instead of a support ticket.
#data-model
#api

Written by
Jun Seo
Staff Engineer


