All articles

Designing your Attio data model: objects, attributes, relationships

·14 min read

The data model is the only decision in Attio that is expensive to change later.

Views can be rebuilt in an afternoon. Automations can be rewired in a day. Lists can be deleted and recreated without anyone noticing. The schema is the part that, once a team has been working in it for three months, you cannot reshape without breaking everyone's muscle memory and every report that depends on it.

That is the whole reason to take it seriously on day one. This is the post on how to design one well: when to add a custom object, when to use a list instead, how to think about attributes and relationships, five real schemas we have shipped, and the anti-patterns we rip out of half the workspaces we audit.

The mental model

Three layers stack on top of each other in Attio. Most teams blur them, which is where most of the trouble starts.

Objects are the entities the business deals with. Companies, People, Deals are the defaults. Custom objects extend the set: Patients, Commitments, Workshops, Staffing Assignments. Each object has its own attributes, its own records, its own permissions, its own URL pattern.

Attributes are the fields on records. Names, statuses, dates, currencies, links to other records, AI-generated values. The choice of which attributes belong on which object is the bulk of the schema work.

Lists are views. They filter and sort existing records, optionally with a few list-only attributes layered on top (a list-specific status, a list-entry timestamp). Lists do not own data. They are how the team sees the data the objects already hold.

The most common DIY mistake is treating lists as databases. Every attribute that matters belongs on a record on an object. Lists are just lenses on top.

When to add a custom object

The question to ask is "does this thing have its own identity and lifecycle?".

If the answer is yes, it is an object. If the answer is "it is just a stage of an existing thing", it is a list or a status field, not an object.

Five tests. Custom object if:

  1. It has its own attributes that no other object needs. A Patient has next-examination-date, lifetime cost, examination count. None of those make sense on Person. Different object.
  2. It has its own permissions. Investors should not see the LP records. Custom object lets you scope read access. Mixing it into Companies makes the permission story messy.
  3. You need to count or report on it separately. "How many active patients" needs Patient to be its own object. "How many active prospects" can stay as a filtered view of Companies.
  4. It has many-to-many relationships with other things. A Commitment links to one Company and many LPs. Modeling that on Companies is doable but ugly. A first-class Commitment record is clean.
  5. It has its own lifecycle separate from the parent. A Staffing Assignment starts when a Deal goes active and runs through completion. The Deal is closed; the Assignment is still running. Different object.

If only one of those is true, you might be able to live with a list view or a status field. If two or more are true, you want a custom object.

The bias most DIYers have is to under-use custom objects. Attio makes them cheap to create. Use them when the test says yes.

How to think about attributes

The rule we apply on every schema: an attribute exists to answer a question someone needs to answer from this record.

Concretely, before any attribute gets added to an object, we write down the question it answers. "What is the lifetime value of this patient?" Add the lifetime value attribute. "When was the last activity?" Add the last-activity attribute. "What is the ICP segment?" Add the ICP segment classify attribute.

If we cannot name the question, the attribute does not get added. This is how Companies object stays at 12 attributes instead of 47.

Three more attribute rules:

Atomic, not concatenated. A single "Address" field as a long string is worse than five fields: street, city, state, postal code, country. The five are filterable. The string is not.

Source-of-truth fields separated from generated fields. A human edits the name. The AI writes the ICP segment. If both can write to the same field, eventually they will step on each other and you will not know which value was last. Name the AI fields with a prefix or suffix so it is obvious which is which.

Empty fields are a debt. A field that is empty on 80% of records is signal that the field belongs on a different object, or only on a subset of records that should be their own object. Empty fields are not free. They clutter the record view, the API responses, and the human reading.

How to think about relationships

Attio has three kinds of relationships that matter.

One-to-many. A Company has many People. A Deal has one primary Person. Attio's default Companies, People, Deals are wired this way already. Custom objects can extend the pattern: a Patient has many Examinations.

Many-to-many. A Commitment has one Company and many LPs. An LP backs many Commitments. The relationship goes both ways. Attio supports this through reference attributes that can hold multiple records on each side.

Hierarchy. A Company has a parent Company. A Workshop has an owner Company. The relationship is to the same object. Modeled with a self-reference attribute.

The rule: every relationship should be addressable from both sides of the join. If you have an Examination record, you should be able to navigate to its Patient. If you have a Patient record, you should be able to see its Examinations. Attio's record view does this automatically when the relationship is a reference attribute, not a freeform text field.

The most common DIY mistake here: storing the other side of the relationship as a text field instead of a reference. "LP names" as a comma-separated string instead of a multi-reference to the LPs object. The text version looks fine on day one and is useless by month three. The reference version is queryable, filterable, and links the records on both ends.

Objects vs lists

The clean separation:

Object: a Company is a company whether or not anyone is selling it. A Person exists independent of any deal. A Patient is a patient whether or not their record is on a list.

List: "Companies in active deals", "People to follow up with this week", "Patients due for retention". Filters and sorts on top of records that already exist.

If you would still want this thing to exist when no list includes it, it is an object. If it only exists as a filtered view, it is a list.

The clearest test: can you describe what changes about the record when it enters or leaves the list? If the answer is "nothing about the record changes; it just shows up in a different view", it is a list. If the answer is "the record's status changes, fields update, workflows fire", it is probably a state on the object, not a list entry.

Five real schemas

A few from our shipped builds, with the choices we made and why.

VC fund: Affinity to Attio

Two custom objects on top of the standard Companies and People.

  • Commitments. One record per check the fund writes. Linked to a Company and to the LPs backing the deal. Holds check size, vintage, vehicle.
  • LPs. One record per limited partner. Holds capital commitment, update cadence, last update date.

Why custom objects: a Commitment has a lifecycle (committed, wired, marked) that is separate from the Deal. An LP has a relationship pattern (one LP backs many Commitments, one Commitment is backed by many LPs) that does not fit on Companies.

Why not lists: "Fund Commitments" sounds like a list. But the team needs to ask "how much have we committed in this sector this year" and "which LPs are backing this deal". Both queries need Commitment to be a first-class object with its own attributes. Lists would force the team to denormalize the data, which always ends in tears.

Health-tech: Elfcare

Three objects.

  • People. Identity and email threads. Status (Lead / Ongoing patient / Retention).
  • Patient. Created only when a person is actively under care. Linked Person, next examination date, total lifetime cost (rolled up from examinations), count of past examinations.
  • Examination. One atomic record per visit. Date, type, value, products, notes.

Why three objects, not one: a Person is not always a patient. Some are leads, some are partners, some are former patients in retention. Mixing patient-only fields onto People would mean 80% empty fields on the People object. Patient-only fields live on Patient.

Why Examination is its own object: lifetime value rolls up cleanly when each visit is an atomic record. Email history stays anchored to People, where it belongs. A patient's clinical history is just a list of Examinations linked to the Patient.

Insurance TPA: Skelmore

Five objects. Companies sits at the top with a mother-daughter hierarchy. Insurers and Workshops are custom objects linked to Companies. Deals connects to People through Champion and Economic Buyer relationships.

  • Companies. Parent entity. Organisation type tells you whether to expect a linked Insurer or Workshop.
  • Insurers. Custom object. Authority limits (Repair / Total Loss / Injury), GWP, market share, SLA tier.
  • Workshops. Custom object. Capabilities, labor rates, cycle times, coverage zone.
  • Deals. MEDDPICC scoring, forecast categories, ACV, champion and economic buyer relationships.
  • Workshop Assessment. A list, not an object. 60+ evaluation criteria layered on Workshop records.

Why Workshop Assessment is a list, not an object: the assessment is a view of a Workshop. The criteria are attributes on the Workshop record itself, surfaced through a structured list. The team did not need to ask "how many assessments" as a separate count from "how many workshops". One assessment per workshop. List wins.

Consulting firm: Keenan Reid Strategies

Four custom objects on top of the standard Companies and People.

  • Deals. Sales opportunities, full stage history.
  • Staffing Assignments. Each placement once a deal goes active. Separates operational placement from the sales record of the deal that won it.
  • Leads. Inbound leads before they get qualified into Deals.
  • Milestones. Revenue milestones tied to deals, used for weighted and booked forecasts.

Why Leads is a separate object, not a stage on Deals: keeping Leads on the Deals object pollutes every Deals report with raw inbound, and every "deal" report needs a filter to exclude leads. Separating them means reports on Deals are clean by default. Leads get qualified and promoted to Deals through a workflow.

Why Milestones is a separate object, not attributes on Deals: a deal has multiple revenue milestones over its lifecycle (months of recurring billing, multi-year contracts). Modeling them as attributes on Deals would mean 24 fields for "month 1 revenue, month 2 revenue, ...". Milestones as records means the team can ask "what is our forecast revenue in Q3 2026" with one query.

Consulting firm: Outlander VC

Two custom objects, similar to the earlier VC fund.

  • Commitments. Same pattern.
  • LPs. Same pattern.

The interesting choice: the Commitment object lives separately from the default Deals. The fund could have used Deals for the investment pipeline and put a "committed amount" field on it. Instead, the pipeline runs on Companies (with stage attributes) and Commitments are their own object that exists only after the commitment is real.

Why: the question "what is the fund's commitment to this company" is a yes-no-and-how-much question, not a pipeline question. Pipeline lives on Companies as a stage. Commitments live as their own object because once a commitment exists, it has a life independent of the pipeline (deployed capital, MOIC, follow-on rounds).

The five common mistakes

We see these every week, in roughly this order of frequency.

1. Lists used as databases

Attributes that should live on records get put on list entries. Then the data is invisible everywhere else. Then the reports do not work.

The fix: every attribute lives on a record on an object. Lists are views.

2. Top-of-funnel mixed with active deals on one object

Lead intake and active pipeline share one Deals object. Every report filters out the leads. Every workflow checks "is this actually a deal or just a lead". The schema fights the team.

The fix: separate Leads as their own object. Promote to Deals on qualification.

3. Custom objects when a list view would do

The opposite mistake. A team creates a "Hot Leads" custom object when "Hot Leads" is just a filtered list of Leads with a stage of Hot.

The fix: ask "would this still exist if no one ever filtered for it". If no, it is a list. If yes, it is an object.

4. Many-to-many modeled as concatenated text

"LPs in this deal" as a comma-separated string. The text version looks fine on day one and is useless by month three.

The fix: a multi-reference attribute. Linked records both ways. Queryable from both sides.

5. Attributes added preemptively

The Companies object ends up with 47 attributes. Half are empty on most records. The team uses 8 of them.

The fix: add an attribute only when you can name the question it answers. If the question has not come up yet, do not add the field yet.

How many is too many

Rough rules from working schemas across 30+ builds:

  • Objects: 4-8 is normal for a focused team. More than 12 usually means you have created custom objects where lists would have worked.
  • Attributes per object: 10-25 is normal. 30 is a yellow flag. 40+ is almost always bloat.
  • Lists: 6-15 per team. More than 20 lists is usually a sign that views are doing the work that should be done by attribute filters.
  • Relationships per object: each linked object should have a reason. The default Companies / People / Deals are wired together for free. Custom objects add 1-3 relationships each on average.

These are not hard limits. They are the shape healthy schemas tend to settle into.

The Craftt take

The schema is the part of the workspace that compounds. Get it right and every workflow, every report, every AI agent that gets built on top has a clean foundation. Get it wrong and you spend the next year working around it.

Three principles we apply on every build:

  1. Custom object if it has its own identity, lifecycle, or permission scope. Not because the team wants more boxes on the diagram.
  2. Attribute only if it answers a named question. Empty fields are debt.
  3. Relationship as a reference, not a string. The data is queryable from both sides or it does not count.

If your workspace was built by someone who did not apply these rules, the cleanup is a one-week project, not a rebuild. It is worth doing before any agents go in on top.

Sources

Free audit of your Attio workspace

If you want a second pair of eyes on whether your data model is set up to scale, we run a free 48-hour audit. You add us as an Attio expert, no extra seat and no billing. We send back a one-page written teardown of the schema ranked by impact, the three highest-leverage fixes to ship first, and a 5-minute Loom walking through the top one. No call, no pitch. 5 slots a week.

Get your free Attio audit

Need help with your Attio setup?

We migrate teams, build data models, wire automations, and train Claude agents inside your workspace. Discovery call is free.

Book a free discovery call