gtmjosh

How to ground HubSpot AI in your custom properties

· 5 min read· HubSpot

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.

scripts/list-hubspot-properties.ts
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:

bad-context.yaml
property: custom_fit_score
meaning: "The fit score."

That tells the model nothing.

Better:

context/hubspot/company/fit_score.yaml
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-01

That 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.

context/hubspot/company/tier.yaml
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-01

This 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:

context/hubspot/company/recent_escalations.yaml
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-01

Now 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.

lib/context-layer.ts
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:

PropertyValueWhat the model might assume
custom_fit_score7272 sounds pretty good
hs_lead_statusIN_PROGRESSThis is probably the current lead state
customer_tierstrategicImportant, but unclear why
recent_escalation_count4Four tickets happened

After context:

PropertyValueWhat the model knows
custom_fit_score72Partial fit — nurture unless intent is high
hs_lead_statusIN_PROGRESSDeprecated; ignore for routing
customer_tierstrategicNamed account; executive coverage expected
recent_escalation_count4Risk 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:

  1. raw HubSpot properties only;
  2. 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.

Get the next guide

New builds and guides, sent when they're ready. No cadence promises.