import { HUBSPOT_ASSOCIATION_INPUT_SCHEMA, HUBSPOT_CORE_OBJECT_TYPE_SCHEMA, HubSpotApiError, HUBSPOT_PROPERTY_VALUES_SCHEMA, HUBSPOT_PROVIDER_RECORD_PAGE_SCHEMA, HUBSPOT_PROVIDER_RECORD_SCHEMA, HUBSPOT_RECORD_PAGE_SCHEMA, HUBSPOT_RECORD_SCHEMA, toHubSpotProperties, toHubSpotRecord, toHubSpotRecordPage, } from "@automate.ax/integration-contracts/hubspot" import * as z from "zod" import { defineAction } from "../../automation/actions" import { getHubSpotApi, HUBSPOT_ACCOUNT, hubSpotAccountRequirement, hubSpotPathSegment, } from "./lib" const HUBSPOT_ID_SCHEMA = z.string().trim().min(1) const HUBSPOT_READ_OPTIONS_SCHEMA = z.object({ /** Associated object types to include. */ associations: z.string().trim().min(1).array().max(10).optional(), /** Include archived records instead of active records. */ archived: z.boolean().optional(), /** Property names whose current values should be returned. */ properties: z.string().trim().min(1).array().max(100).optional(), /** Property names whose current and historical values should be returned. */ propertiesWithHistory: z.string().trim().min(1).array().max(100).optional(), }) const HUBSPOT_LIST_INPUT_SCHEMA = HUBSPOT_READ_OPTIONS_SCHEMA.extend({ /** Opaque cursor returned by the previous page. */ after: z.string().trim().min(1).optional(), /** Maximum records to return. */ limit: z.number().int().min(1).max(100).prefault(100), }) const HUBSPOT_GET_INPUT_SCHEMA = HUBSPOT_READ_OPTIONS_SCHEMA.extend({ /** Property used as the identifier instead of the record ID. */ idProperty: z.string().trim().min(1).optional(), /** HubSpot record ID or unique property value. */ recordId: HUBSPOT_ID_SCHEMA, }) const SCALAR_FILTER_SCHEMA = z.object({ operator: z.enum([ "CONTAINS_TOKEN", "EQ", "GT", "GTE", "LT", "LTE", "NEQ", "NOT_CONTAINS_TOKEN", ]), propertyName: z.string().trim().min(1), value: z.string(), }) const BETWEEN_FILTER_SCHEMA = z.object({ highValue: z.string(), operator: z.literal("BETWEEN"), propertyName: z.string().trim().min(1), value: z.string(), }) const SET_FILTER_SCHEMA = z.object({ operator: z.enum(["IN", "NOT_IN"]), propertyName: z.string().trim().min(1), values: z.string().array().min(1), }) const PRESENCE_FILTER_SCHEMA = z.object({ operator: z.enum(["HAS_PROPERTY", "NOT_HAS_PROPERTY"]), propertyName: z.string().trim().min(1), }) export const HUBSPOT_SEARCH_FILTER_SCHEMA = z.union([ BETWEEN_FILTER_SCHEMA, PRESENCE_FILTER_SCHEMA, SCALAR_FILTER_SCHEMA, SET_FILTER_SCHEMA, ]) export const HUBSPOT_SEARCH_INPUT_SCHEMA = z.object({ /** Opaque cursor returned by the previous search page. */ after: z.string().trim().min(1).optional(), /** OR groups whose filters are combined with AND logic. */ filterGroups: z .object({ filters: HUBSPOT_SEARCH_FILTER_SCHEMA.array().min(1).max(6) }) .array() .max(5) .superRefine((groups, context) => { if ( groups.reduce((total, group) => total + group.filters.length, 0) > 18 ) { context.addIssue({ code: "custom", message: "HubSpot search supports at most 18 filters.", }) } }) .optional(), /** Maximum matching records to return. */ limit: z.number().int().min(1).max(200).prefault(100), /** Property names to include in each result. */ properties: z.string().trim().min(1).array().max(100).optional(), /** Search text matched against the object's default searchable fields. */ query: z.string().max(3000).optional(), /** Single property sort supported by HubSpot search. */ sort: z .object({ direction: z.enum(["ASCENDING", "DESCENDING"]), propertyName: z.string().trim().min(1), }) .optional(), }) const HUBSPOT_CREATE_INPUT_SCHEMA = z.object({ /** Associations created atomically with the record. */ associations: HUBSPOT_ASSOCIATION_INPUT_SCHEMA.array().optional(), /** HubSpot property names and serialized values. */ properties: HUBSPOT_PROPERTY_VALUES_SCHEMA.min(1), }) const HUBSPOT_UPDATE_INPUT_SCHEMA = z.object({ /** Property used as the identifier instead of the record ID. */ idProperty: z.string().trim().min(1).optional(), /** HubSpot property names and serialized values. */ properties: HUBSPOT_PROPERTY_VALUES_SCHEMA.min(1), /** HubSpot record ID or unique property value. */ recordId: HUBSPOT_ID_SCHEMA, }) const HUBSPOT_ARCHIVE_INPUT_SCHEMA = z.object({ /** HubSpot record ID. */ recordId: HUBSPOT_ID_SCHEMA, }) const HUBSPOT_BATCH_UPSERT_RESPONSE_SCHEMA = HUBSPOT_PROVIDER_RECORD_PAGE_SCHEMA.extend({ errors: z .object({ category: z.string().optional(), message: z.string(), subCategory: z.string().optional(), }) .array() .default([]), numErrors: z.number().int().nonnegative().default(0), }) const OBJECT_CONFIG = { companies: { label: "HubSpot companies", singular: "HubSpot company", readScope: "crm.objects.companies.read", writeScope: "crm.objects.companies.write", }, contacts: { label: "HubSpot contacts", singular: "HubSpot contact", readScope: "crm.objects.contacts.read", writeScope: "crm.objects.contacts.write", }, deals: { label: "HubSpot deals", singular: "HubSpot deal", readScope: "crm.objects.deals.read", writeScope: "crm.objects.deals.write", }, tickets: { label: "HubSpot tickets", singular: "HubSpot ticket", readScope: "tickets", writeScope: "tickets", }, } as const const contactActions = createObjectActions("contacts") const companyActions = createObjectActions("companies") const dealActions = createObjectActions("deals") const ticketActions = createObjectActions("tickets") /** Lists HubSpot contacts, including archived records when requested. */ export const listHubSpotContacts = contactActions.list /** Retrieves one HubSpot contact by record ID, email, or custom unique property. */ export const getHubSpotContact = contactActions.get /** Searches HubSpot contacts with typed CRM filters. */ export const searchHubSpotContacts = contactActions.search /** Creates one HubSpot contact. */ export const createHubSpotContact = contactActions.create /** Updates one HubSpot contact. */ export const updateHubSpotContact = contactActions.update /** Archives one HubSpot contact. */ export const archiveHubSpotContact = contactActions.archive /** Lists HubSpot companies, including archived records when requested. */ export const listHubSpotCompanies = companyActions.list /** Retrieves one HubSpot company by record ID or unique property. */ export const getHubSpotCompany = companyActions.get /** Searches HubSpot companies with typed CRM filters. */ export const searchHubSpotCompanies = companyActions.search /** Creates one HubSpot company. */ export const createHubSpotCompany = companyActions.create /** Updates one HubSpot company. */ export const updateHubSpotCompany = companyActions.update /** Archives one HubSpot company. */ export const archiveHubSpotCompany = companyActions.archive /** Lists HubSpot deals, including archived records when requested. */ export const listHubSpotDeals = dealActions.list /** Retrieves one HubSpot deal by record ID or unique property. */ export const getHubSpotDeal = dealActions.get /** Searches HubSpot deals with typed CRM filters. */ export const searchHubSpotDeals = dealActions.search /** Creates one HubSpot deal. */ export const createHubSpotDeal = dealActions.create /** Updates one HubSpot deal. */ export const updateHubSpotDeal = dealActions.update /** Archives one HubSpot deal. */ export const archiveHubSpotDeal = dealActions.archive /** Lists HubSpot tickets, including archived records when requested. */ export const listHubSpotTickets = ticketActions.list /** Retrieves one HubSpot ticket by record ID or unique property. */ export const getHubSpotTicket = ticketActions.get /** Searches HubSpot tickets with typed CRM filters. */ export const searchHubSpotTickets = ticketActions.search /** Creates one HubSpot ticket. */ export const createHubSpotTicket = ticketActions.create /** Updates one HubSpot ticket. */ export const updateHubSpotTicket = ticketActions.update /** Archives one HubSpot ticket. */ export const archiveHubSpotTicket = ticketActions.archive /** Batch creates or updates contacts by email or a custom unique property. */ export const upsertHubSpotContacts = defineAction("Upsert HubSpot contacts") .describe( "Batch creates or updates up to 100 contacts by email or a custom unique property.", ) .account( HUBSPOT_ACCOUNT, hubSpotAccountRequirement("crm.objects.contacts.write"), ) .input( z.object({ contacts: z .object({ id: HUBSPOT_ID_SCHEMA, idProperty: z.string().trim().min(1), properties: HUBSPOT_PROPERTY_VALUES_SCHEMA, }) .array() .min(1) .max(100), }), ) .output(z.object({ contacts: HUBSPOT_RECORD_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const response = await getHubSpotApi(account.secret).request( "crm/objects/2026-03/contacts/batch/upsert", { body: { inputs: input.contacts.map(({ id, idProperty, properties }) => ({ id, idProperty, properties: toHubSpotProperties(properties), })), }, method: "POST", responseSchema: HUBSPOT_BATCH_UPSERT_RESPONSE_SCHEMA, }, ) if (response.numErrors > 0 || response.errors.length > 0) { throw new HubSpotApiError(207, { category: "BATCH_PARTIAL_FAILURE", errors: response.errors, message: `HubSpot could not upsert ${Math.max(response.numErrors, response.errors.length)} contact(s).`, }) } return { contacts: response.results.map(toHubSpotRecord) } }) /** * Builds the shared CRUD and search family for one core CRM object. * * @param objectType - HubSpot core object collection. */ function createObjectActions( objectType: z.output, ) { const config = OBJECT_CONFIG[objectType] const path = `crm/objects/2026-03/${objectType}` const readRequirement = hubSpotAccountRequirement(config.readScope) const writeRequirement = hubSpotAccountRequirement(config.writeScope) return { archive: defineAction(`Archive ${config.singular}`) .describe(`Moves one ${config.singular} to HubSpot's recycling bin.`) .account(HUBSPOT_ACCOUNT, writeRequirement) .input(HUBSPOT_ARCHIVE_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getHubSpotApi(account.secret).request( `${path}/${hubSpotPathSegment(input.recordId)}`, { method: "DELETE", responseSchema: z.undefined() }, ) }), create: defineAction(`Create ${config.singular}`) .describe(`Creates one ${config.singular} with optional associations.`) .account(HUBSPOT_ACCOUNT, writeRequirement) .input(HUBSPOT_CREATE_INPUT_SCHEMA) .output(HUBSPOT_RECORD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toHubSpotRecord( await getHubSpotApi(account.secret).request(path, { body: { ...(input.associations && { associations: input.associations.map( ({ toRecordId, types }) => ({ to: { id: toRecordId }, types, }), ), }), properties: toHubSpotProperties(input.properties), }, method: "POST", responseSchema: HUBSPOT_PROVIDER_RECORD_SCHEMA, }), ), ), get: defineAction(`Get ${config.singular}`) .describe(`Retrieves one ${config.singular} by a stable identifier.`) .account(HUBSPOT_ACCOUNT, readRequirement) .input(HUBSPOT_GET_INPUT_SCHEMA) .output(HUBSPOT_RECORD_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toHubSpotRecord( await getHubSpotApi(account.secret).request( `${path}/${hubSpotPathSegment(input.recordId)}`, { query: { archived: input.archived, associations: input.associations, idProperty: input.idProperty, properties: input.properties, propertiesWithHistory: input.propertiesWithHistory, }, responseSchema: HUBSPOT_PROVIDER_RECORD_SCHEMA, }, ), ), ), list: defineAction(`List ${config.label}`) .describe(`Lists ${config.label} with cursor pagination.`) .account(HUBSPOT_ACCOUNT, readRequirement) .input(HUBSPOT_LIST_INPUT_SCHEMA) .output(HUBSPOT_RECORD_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toHubSpotRecordPage( await getHubSpotApi(account.secret).request(path, { query: input, responseSchema: HUBSPOT_PROVIDER_RECORD_PAGE_SCHEMA, }), ), ), search: defineAction(`Search ${config.label}`) .describe(`Searches ${config.label} by CRM property values.`) .account(HUBSPOT_ACCOUNT, readRequirement) .input(HUBSPOT_SEARCH_INPUT_SCHEMA) .output(HUBSPOT_RECORD_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toHubSpotRecordPage( await getHubSpotApi(account.secret).request(`${path}/search`, { body: { after: input.after, filterGroups: input.filterGroups, limit: input.limit, properties: input.properties, query: input.query, ...(input.sort ? { sorts: [input.sort] } : {}), }, method: "POST", responseSchema: HUBSPOT_PROVIDER_RECORD_PAGE_SCHEMA, }), ), ), update: defineAction(`Update ${config.singular}`) .describe(`Updates writable properties on one ${config.singular}.`) .account(HUBSPOT_ACCOUNT, writeRequirement) .input(HUBSPOT_UPDATE_INPUT_SCHEMA) .output(HUBSPOT_RECORD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toHubSpotRecord( await getHubSpotApi(account.secret).request( `${path}/${hubSpotPathSegment(input.recordId)}`, { body: { properties: toHubSpotProperties(input.properties) }, method: "PATCH", query: { idProperty: input.idProperty }, responseSchema: HUBSPOT_PROVIDER_RECORD_SCHEMA, }, ), ), ), } }