Chapter 4 · Custom Objects & Schema Design
custom objects &
schema design
Define business data in HubSpot that cannot be represented using standard objects as custom objects.
Learn practical concepts of object design, property type selection, association design, and Enterprise-specific functions.
4-1 What is a custom object?
Understand the limitations of standard objects and identify issues that can be solved with custom objects.
📦 Standard objects
Fixed objects built into HubSpot.
Contacts / Companies / Deals / Tickets / Products / Line Items / Quotes etc.
Although suitable for most businesses, industry-specific data structures have limitations.
✨ Custom objects
An object that can be freely defined by the developer.
"Properties", "Vehicles", "Subscriptions", "Events", "Qualifications", etc.
You can add unique data structures to HubSpot CRM that suit your industry and business.
Enterprise Required
💡 Typical examples where custom objects are needed
Real estate industry:“Property” object → Manage by linking with Contacts (owner/tenant)
Car sales:“Vehicle” object → Association with Deals (business negotiations) and Contacts (owner)
SaaS:“Subscription” object → Lifecycle management by linking with Contacts/Deals
education:"Course" "Attendance history" object → Track learning progress of Contacts
Manufacturing:“Equipment” and “Maintenance record” objects → Support management in conjunction with Tickets
Limitations:
custom object is Enterprise planAvailable only in
Maximum per portal 10 piecesYou can create custom objects for (may vary by plan).
Each object has a maximum 1,000 piecesproperties can be defined.
4-2 Concept of schema design
Design object structures, properties, and associations before implementation.
🏗 Design steps
① Identify the entity you want to manage → ② Check if it can be replaced with a standard object →
③ Define the name and properties of the custom object → ④ Design the association with the standard object →
⑤ Implementation with API
Design example: SaaS product schema
Subscriptions (custom) is the central object that bridges contacts, companies, and deals
| Design criteria | Use standard objects | Use custom objects |
| Nature of data |
Close to people, companies, business negotiations, and support |
Industry-specific entities (properties, vehicles, contracts, etc.) |
| number of properties |
Existing properties are sufficient |
Requires 10 or more dedicated properties |
| reporting requirements |
Available with standard reports |
Requires custom object-specific aggregation |
| Workflow collaboration |
Can be handled with standard workflow |
I want to use a custom object as a trigger |
4-3 Create a custom object with Schemas API
Define and create schemas for custom objects using the API.
Node.js — Create custom object "Subscription"
import { Client }
from '@hubspot/api-client';
const client =
new Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN });
const schema =
await client.crm.schemas.coreApi.create({
name:
'subscriptions',
labels: {
singular:
'Subscription',
plural:
'Subscriptions',
},
primaryDisplayProperty:
'plan_name',
secondaryDisplayProperties: [
'status',
'mrr'],
properties: [
{
name:
'plan_name',
label:
'Plan name',
type:
'string',
fieldType:
'text',
groupName:
'subscriptioninformation',
},
{
name:
'status',
label:
'status',
type:
'enumeration',
fieldType:
'select',
groupName:
'subscriptioninformation',
options: [
{ label:
'active', value:
'active', displayOrder:
1, hidden:
false },
{ label:
'Trial', value:
'trial', displayOrder:
2, hidden:
false },
{ label: 'Cancelled', value:
'cancelled', displayOrder:
3, hidden:
false },
{ label:
'pause', value:
'paused', displayOrder:
4, hidden:
false },
],
},
{
name:
'mrr',
label:
'MRR (Monthly Revenue)',
type:
'number',
fieldType:
'number',
groupName:
'subscriptioninformation',
},
{
name:
'start_date',
label:
'Contract start date',
type:
'date',
fieldType:
'date',
groupName:
'subscriptioninformation',
},
{
name:
'renewal_date',
label:
'Updated date',
type:
'date',
fieldType:
'date',
groupName:
'subscriptioninformation',
},
{
name:
'external_id',
label:
'External system ID',
type:
'string',
fieldType:
'text',
groupName:
'subscriptioninformation',
hasUniqueValue:
true,
},
],
associatedObjects: [
'CONTACT',
'COMPANY',
'DEAL'],
});
console.log(
`Schema created: ${schema.objectTypeId}`);
About objectTypeId:
After creating the custom object,objectTypeId(example:2-12345678) is returned.
Use this ID in subsequent API calls.
/crm/v3/objects/2-12345678 Access it by including it in the path like this.
4-4 Property type details and selection criteria
Organize property type/fieldType combinations and use cases for each type.
| type | fieldType | stored value | use case |
string |
text |
text |
Name, memo, URL, code, etc. |
string |
textarea |
long text |
Explanation/Remarks/Memo column |
number |
number |
numerical value |
Amount/Score/Quantity/Coordinates |
enumeration |
select |
one choice |
Status/Rank/Category |
enumeration |
radio |
one choice |
Yes/No/Priority (emphasis on visibility) |
enumeration |
checkbox |
Semicolon separated multiple values |
Tags/Function flags/Multiple selection |
date |
date |
YYYY-MM-DD |
Contract date, deadline, birthday |
datetime |
date |
Unix milliseconds |
Timestamp/Last login |
bool |
booleancheckbox |
true / false |
Flag/Valid/Invalid/Consent |
phone_number |
phonenumber |
phone number string |
Telephone number (compatible with international format) |
⚠ Handling of enumeration (checkbox) values:
The checkbox type separates multiple selected values with a semicolon (;) delimited string.
example:"feature_a;feature_b;feature_c"
If you want to filter by Search API CONTAINS_TOKEN Use operators.
Node.js — Add properties later
await client.crm.properties.coreApi.create(
'2-12345678',
{
name:
'seat_count',
label:
'Number of seats',
type:
'number',
fieldType:
'number',
groupName:
'subscriptioninformation',
description:
'Number of contracted users (number of seats)',
}
);
await client.crm.propertyGroups.coreApi.create(
'2-12345678',
{
name:
'billing_info',
displayOrder:
2,
label:
'Billing information',
}
);
4-5 Association design
Define relationships between custom objects and standard/other custom objects.
🔗 Key points for association design
Associations between custom objects When defining a schema using the Schemas APIor set it to
later Associations Schema API You can add it with .
Both "one-to-many" and "many-to-many" relationships can be expressed.
association labelBy using , you can differentiate the type of relationship between the same objects, such as ``primary person'' and ``secondary person person.''
Node.js — Creating custom association labels
const assocDef =
await client.crm.schemas.associationsApi.create(
'subscriptions',
'contacts',
{
label:
'Main contact',
name:
'primary_contact',
category:
'USER_DEFINED',
}
);
console.log(
`Association type ID: ${assocDef.typeId}`);
await client.crm.schemas.associationsApi.create(
'contacts',
'subscriptions',
{
label:
'Responsible subscription',
name:
'managed_subscription',
category:
'USER_DEFINED',
}
);
Node.js — Association using custom association labels
await client.crm.associations.v4.basicApi.create(
'subscriptions',
subscriptionId,
'contacts',
contactId,
[
{
associationCategory:
'USER_DEFINED',
associationTypeId: assocDef.typeId,
},
]
);
const related =
await client.crm.associations.v4.basicApi.getPage(
'subscriptions',
subscriptionId,
'contacts'
);
4-6 CRUD operations for custom objects
Work with custom object records using the same API patterns as standard objects.
Node.js — Create, retrieve, update, and delete records
const OBJECT_TYPE =
'2-12345678';
const record =
await client.crm.objects.basicApi.create(OBJECT_TYPE, {
properties: {
plan_name:
'Pro plan',
status:
'active',
mrr:
'49800',
start_date:
'2026-04-01',
renewal_date:
'2027-03-31',
external_id:
'sub_001',
},
associations: [
{
to: { id: contactId },
types: [{ associationCategory:
'USER_DEFINED', associationTypeId: primaryTypeId }],
},
],
});
const fetched =
await client.crm.objects.basicApi.getById(
OBJECT_TYPE,
record.id,
[
'plan_name',
'status',
'mrr',
'renewal_date']
);
await client.crm.objects.basicApi.update(OBJECT_TYPE, record.id, {
properties: {
status:
'cancelled',
mrr:
'0',
},
});
const activeSubscriptions =
await client.crm.objects.searchApi.doSearch(OBJECT_TYPE, {
filterGroups: [{
filters: [{
propertyName:
'status',
operator:
'EQ',
value:
'active',
}],
}],
properties: [
'plan_name',
'mrr',
'renewal_date'],
sorts: [{ propertyName:
'renewal_date', direction:
'ASCENDING' }],
limit:
100,
});
Node.js — Process custom objects in bulk with Batch API
async function upsertSubscriptions(externalData) {
const CHUNK =
100;
for (
let i =
0; i < externalData.length; i += CHUNK) {
const chunk = externalData.slice(i, i + CHUNK);
await client.crm.objects.batchApi.upsert(OBJECT_TYPE, {
inputs: chunk.map(item => ({
idProperty:
'external_id',
properties: {
external_id: item.id,
plan_name: item.planName,
status: item.status,
mrr: String(item.mrr),
renewal_date: item.renewalDate,
},
})),
});
await new Promise(r => setTimeout(r,
200));
}
}
Upsert API:
batchApi.upsert() specifiedunique property(idProperty) updates if there is a record that matches the value,
If not, create a new one. Very useful for periodic synchronization with external systems.
hasUniqueValue: true properties only idProperty can be specified.
4-7 Obtain/update/delete schema
Manage the schema of custom objects you have created.
Node.js — Schema management operations
const allSchemas =
await client.crm.schemas.coreApi.getAll();
console.log(allSchemas.results.map(s => ({
name: s.name,
objectTypeId: s.objectTypeId,
properties: s.properties.length,
})));
const schema =
await client.crm.schemas.coreApi.getById(
'subscriptions');
await client.crm.schemas.coreApi.update(
'subscriptions', {
labels: {
singular:
'Subscription',
plural:
'Subscription list',
},
primaryDisplayProperty:
'plan_name',
});
await client.crm.schemas.coreApi.archive(
'subscriptions');
⚠ Note on schema deletion:
To delete a custom object's schema, delete that object's schema.Delete all records firstMust be.
Additionally, deleted schemas cannot be restored.
Always test in a sandbox before deleting in production.
4-8 Practical example: Periodic synchronization architecture with external DB
A design pattern that syncs subscription data from external services like Stripe to HubSpot custom objects.
🏛 Architecture Overview
① Stripe Webhook → Receive subscription change event
② Node.js server(or Serverless Function) → Handle events
③ HubSpot Upsert API → Custom object subscriptions update
④ Associations API → Link with Contacts/Deals
⑤ HubSpot Workflow → Notification/task creation triggered by status change
Node.js — Stripe → HubSpot sync handler
import Stripe
from 'stripe';
import { Client }
from '@hubspot/api-client';
const stripe =
new Stripe(process.env.STRIPE_SECRET_KEY);
const hubspot =
new Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN });
async function handleSubscriptionUpdated(stripeSubscription) {
const {
id: externalId,
status,
items,
current_period_end,
customer: customerId,
} = stripeSubscription;
const mrr = items.data.reduce((sum, item) =>
sum + (item.price.unit_amount * item.quantity /
100),
0
);
const result =
await hubspot.crm.objects.batchApi.upsert(
'subscriptions', {
inputs: [{
idProperty:
'external_id',
properties: {
external_id: externalId,
status: status ===
'active' ?
'active' : status ===
'trialing' ?
'trial' :
'cancelled',
mrr: String(mrr),
renewal_date:
new Date(current_period_end *
1000).toISOString().split(
'T')[
0],
},
}],
});
const contacts =
await hubspot.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName:
'stripe_customer_id', operator:
'EQ', value: customerId }],
}],
properties: [
'email'],
limit:
1,
});
if (contacts.results.length >
0) {
const subscriptionId = result.results[
0].id;
const contactId = contacts.results[
0].id;
await hubspot.crm.associations.v4.basicApi.create(
'subscriptions', subscriptionId,
'contacts', contactId,
[{ associationCategory:
'HUBSPOT_DEFINED', associationTypeId:
1 }]
);
}
}
4-9 Summary of this chapter
Please review before proceeding to the next chapter (CMS Hub Development—Themes & Templates).
✅ Chapter 4 Checklist
- Determine when a custom object is required (where standard objects are insufficient)
- Understood the 5 steps of schema design (identification → confirmation → definition → association → implementation)
- You can create custom objects with the Schemas API
- I understood how to use objectTypeId (
2-XXXXXXXXformat)
- You can select a combination of property type (string/number/enumeration/date/bool) and fieldType
- You can set unique constraints using hasUniqueValue
- Create and apply custom association labels
- CRUD/Search/Batch operations can be performed on custom objects
- You can implement differential synchronization with external systems using the Upsert API.
- Sync HubSpot custom objects with external services like Stripe via webhooks
About the next chapter (Chapter 5):
The development side of CMS Hub—Learn about themes and templates. HubSpot theme structure/HubL template language practice/
We will systematically explain CMS development for front-end developers, including global partial responsive design.