How to ground HubSpot AI in your custom properties
Build a HubSpot context layer that tells AI what your custom properties mean, which fields to trust, and how to load the right context at runtime.
This is the HubSpot setup for the CRM AI context layer.
The idea is simple: write down what your important HubSpot properties mean, then load those notes when an AI workflow reads a record.
This is not a replacement for HubSpot's Breeze context or knowledge vaults. Use those where they help. This guide is for the narrower problem: your custom properties have business meaning that HubSpot's schema does not explain.
Start by pulling the real properties
Do not build the list from memory. HubSpot portals collect old fields the way garages collect half-empty paint cans. Pull the real schema first.
import { Client } from "@hubspot/api-client";
const hubspot = new Client({ accessToken: process.env.HUBSPOT_TOKEN });
async function listProperties(objectType: "contacts" | "companies" | "deals" | "tickets") {
const res = await hubspot.crm.properties.coreApi.getAll(objectType);
return res.results
.filter((property) => !property.hidden)
.map((property) => ({
name: property.name,
label: property.label,
type: property.type,
fieldType: property.fieldType,
options: property.options?.map((option) => option.value) ?? [],
}));
}
console.table(await listProperties("companies"));This gives you the starting list. It does not mean every property needs a context entry.
Most fields are not worth the work. Focus on fields that change a decision: routing, scoring, qualification, lifecycle stage, owner assignment, account health, renewal risk, or AI-generated outputs.
Write the note a new hire would need
A useful context entry should say something the property label does not.
Bad:
property: custom_fit_score
meaning: "The fit score."That tells the model nothing.
Better:
property: custom_fit_score
object: company
label: Fit score
hubspotType: number
meaning: >
0–100 estimate of ICP fit. Written nightly by the scoring job. Reps do not
enter this manually.
interpretation:
- "80–100: strong fit — route to AE"
- "50–79: partial fit — nurture unless intent is high"
- "0–49: poor fit — do not route"
authoritative: true
updated: 2026-08-01That entry answers the questions the field name cannot:
- who writes the value;
- whether a human entered it;
- whether high is good or bad;
- what each range means;
- whether the model should trust it.
Be careful with dropdowns
HubSpot dropdowns are easy to misread. The value stored by the API is not always the label your team sees in the UI.
So for dropdown-style properties, write out the options.
property: customer_tier
object: company
label: Customer tier
hubspotType: enumeration
options:
strategic: "Strategic — named account, exec coverage expected"
commercial: "Commercial — standard lifecycle"
self_serve: "Self-serve — no assigned CSM"
meaning: >
Customer operating tier. Use this to decide which engagement rules apply.
authoritative: true
updated: 2026-08-01This is boring work. Good. Boring is what keeps the model from inventing meaning.
Associations need rules too
HubSpot data rarely lives on one object.
An account-health workflow might need the company, associated contacts, open deals, recent tickets, and list membership. The mistake is dumping all of that into the prompt and hoping the model sorts it out.
Write the retrieval rule instead:
property: recent_escalation_count
object: company
source: associated_tickets
meaning: >
Count of associated tickets marked escalated in the last 90 days. Use as a risk
signal only when the company is a customer and renewal is within 180 days.
retrieval:
association: company_to_ticket
filter: "hs_pipeline_stage = escalated AND createdate >= now - 90d"
authoritative: true
updated: 2026-08-01Now the model does not just see 4. It sees where the number came from and when it
matters.
Load only what the workflow needs
If a workflow reads five properties, load five context entries. Do not turn your whole HubSpot portal into a prompt.
import { readFileSync, readdirSync } from "node:fs";
import { parse } from "yaml";
type FieldContext = {
property: string;
object: string;
meaning: string;
interpretation?: string[];
authoritative: boolean;
};
export function loadHubSpotContext(object: string, properties: string[]): string {
const entries = readdirSync(`context/hubspot/${object}`)
.map((file) => parse(readFileSync(`context/hubspot/${object}/${file}`, "utf8")) as FieldContext)
.filter((entry) => properties.includes(entry.property));
return entries
.map((entry) => [
`## ${entry.property}`,
`Authoritative: ${entry.authoritative}`,
entry.meaning,
...(entry.interpretation ?? []),
].join("\n"))
.join("\n\n");
}This keeps the prompt small and makes the behavior easier to test.
Do not hit the Properties API at runtime
Use HubSpot's Properties API to sync schema into your review process. Do not fetch the full schema every time an AI workflow runs.
In practice:
- sync property metadata on a schedule;
- review new custom properties before they become authoritative;
- cache approved context entries near the workflow;
- stop the workflow when an important field has no context.
That last one will feel annoying. It is still better than letting the model guess.
If the qualifier needs a fit score and the context layer does not know how to read that score, send it to review.
What changes once this works
Before context:
| Property | Value | What the model might assume |
|---|---|---|
custom_fit_score | 72 | 72 sounds pretty good |
hs_lead_status | IN_PROGRESS | This is probably the current lead state |
customer_tier | strategic | Important, but unclear why |
recent_escalation_count | 4 | Four tickets happened |
After context:
| Property | Value | What the model knows |
|---|---|---|
custom_fit_score | 72 | Partial fit — nurture unless intent is high |
hs_lead_status | IN_PROGRESS | Deprecated; ignore for routing |
customer_tier | strategic | Named account; executive coverage expected |
recent_escalation_count | 4 | Risk signal only for customers near renewal |
That is the whole win. The model stops treating field names like instructions.
Test it before you trust it
Pick one company record where the model usually gets the answer wrong.
Run the same prompt twice:
- raw HubSpot properties only;
- raw properties plus the relevant context entries.
The grounded answer should ignore stale fields, cite the fields it trusted, and apply your thresholds correctly.
If it only sounds more confident, keep editing. The goal is not nicer prose. The goal is better reasoning.
Related implementations
- The pillar guide explains the concept.
- The Salesforce version will use custom metadata and Apex retrieval.
- The n8n version will keep the layer outside any one CRM.
Get the next guide
New builds and guides, sent when they're ready. No cadence promises.