import * as z from "zod" import { defineAction } from "../../automation/actions" import { integrationScope as scope } from "../../automation/integrations" import { getHubSpotApi, HUBSPOT_ACCOUNT, hubSpotAccountRequirement, hubSpotPathSegment, } from "./lib" const OBJECT_TYPE_SCHEMA = z.string().trim().min(1) const READ_CORE_SCOPE = scope.any( "crm.objects.companies.read", "crm.objects.contacts.read", "crm.objects.deals.read", "tickets", ) const ASSOCIATION_OBJECTS = { companies: { label: "HubSpot company", scope: "crm.objects.companies.write", }, contacts: { label: "HubSpot contact", scope: "crm.objects.contacts.write", }, deals: { label: "HubSpot deal", scope: "crm.objects.deals.write" }, tickets: { label: "HubSpot ticket", scope: "tickets" }, } as const const OWNER_SCHEMA = z.object({ archived: z.boolean(), createdAt: z.iso .datetime({ offset: true }) .transform((value) => new Date(value)), email: z.email().optional(), firstName: z.string().optional(), id: z.string(), lastName: z.string().optional(), teams: z .object({ id: z.string(), name: z.string(), primary: z.boolean() }) .array(), updatedAt: z.iso .datetime({ offset: true }) .transform((value) => new Date(value)), userId: z.number().int().optional(), }) const PROVIDER_OWNER_SCHEMA = z.object({ archived: z.boolean(), createdAt: z.iso.datetime({ offset: true }), email: z.email().optional(), firstName: z.string().optional(), id: z.string(), lastName: z.string().optional(), teams: z .object({ id: z.string(), name: z.string(), primary: z.boolean() }) .array() .default([]), updatedAt: z.iso.datetime({ offset: true }), userId: z.number().int().optional(), }) const PROPERTY_SCHEMA = z.object({ calculated: z.boolean(), description: z.string(), fieldType: z.string(), groupName: z.string(), hidden: z.boolean(), label: z.string(), name: z.string(), options: z .object({ description: z.string().optional(), displayOrder: z.number(), hidden: z.boolean(), label: z.string(), value: z.string(), }) .array(), type: z.string(), }) const PIPELINE_STAGE_SCHEMA = z.object({ archived: z.boolean(), displayOrder: z.number().int(), id: z.string(), label: z.string(), metadata: z.record(z.string(), z.string()), }) const PIPELINE_SCHEMA = z.object({ archived: z.boolean(), displayOrder: z.number().int(), id: z.string(), label: z.string(), stages: PIPELINE_STAGE_SCHEMA.array(), }) const ASSOCIATION_LABEL_SCHEMA = z.object({ category: z.enum(["HUBSPOT_DEFINED", "INTEGRATOR_DEFINED", "USER_DEFINED"]), label: z.string().nullable(), typeId: z.number().int().positive(), }) /** Lists owners who can own CRM records. */ export const listHubSpotOwners = defineAction("List HubSpot owners") .describe("Lists active or archived owners in the connected HubSpot account.") .account( HUBSPOT_ACCOUNT, hubSpotAccountRequirement("crm.objects.owners.read"), ) .input( z.object({ after: z.string().optional(), archived: z.boolean().optional(), limit: z.number().int().min(1).max(500).prefault(100), }), ) .output( z.object({ after: z.string().optional(), owners: OWNER_SCHEMA.array() }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const page = await getHubSpotApi(account.secret).request( "crm/owners/2026-03", { query: input, responseSchema: z.object({ paging: z .object({ next: z.object({ after: z.string() }).optional() }) .optional(), results: PROVIDER_OWNER_SCHEMA.array(), }), }, ) return { after: page.paging?.next?.after, owners: page.results } }) /** Retrieves one owner by owner or user ID. */ export const getHubSpotOwner = defineAction("Get HubSpot owner") .describe("Retrieves one HubSpot owner by owner ID or user ID.") .account( HUBSPOT_ACCOUNT, hubSpotAccountRequirement("crm.objects.owners.read"), ) .input( z.object({ archived: z.boolean().optional(), id: z.string().trim().min(1), idProperty: z.enum(["id", "userId"]).optional(), }), ) .output(OWNER_SCHEMA) .retry({ replaySafety: "safe" }) .handler(({ account, input }) => getHubSpotApi(account.secret).request( `crm/owners/2026-03/${hubSpotPathSegment(input.id)}`, { query: { archived: input.archived, idProperty: input.idProperty }, responseSchema: PROVIDER_OWNER_SCHEMA, }, ), ) /** Lists property definitions for any accessible CRM object type. */ export const listHubSpotProperties = defineAction("List HubSpot properties") .describe("Lists property definitions for a HubSpot CRM object type.") .account(HUBSPOT_ACCOUNT, hubSpotAccountRequirement(READ_CORE_SCOPE)) .input( z.object({ archived: z.boolean().optional(), objectType: OBJECT_TYPE_SCHEMA, }), ) .output(z.object({ properties: PROPERTY_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ({ properties: ( await getHubSpotApi(account.secret).request( `crm/properties/2026-03/${hubSpotPathSegment(input.objectType)}`, { query: { archived: input.archived }, responseSchema: z.object({ results: PROPERTY_SCHEMA.array() }), }, ) ).results, })) /** Lists deal or ticket pipelines and their ordered stages. */ export const listHubSpotPipelines = defineAction("List HubSpot pipelines") .describe("Lists pipelines and stages for deals or tickets.") .account( HUBSPOT_ACCOUNT, hubSpotAccountRequirement(scope.any("crm.objects.deals.read", "tickets")), ) .input( z.object({ archived: z.boolean().optional(), objectType: z.enum(["deals", "tickets"]), }), ) .output(z.object({ pipelines: PIPELINE_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ({ pipelines: ( await getHubSpotApi(account.secret).request( `crm/pipelines/2026-03/${input.objectType}`, { query: { archived: input.archived }, responseSchema: z.object({ results: PIPELINE_SCHEMA.array() }), }, ) ).results, })) /** Lists association labels from one CRM object type to another. */ export const listHubSpotAssociationLabels = defineAction( "List HubSpot association labels", ) .describe( "Lists the default and custom labels available between two CRM object types.", ) .account(HUBSPOT_ACCOUNT, hubSpotAccountRequirement(READ_CORE_SCOPE)) .input( z.object({ fromObjectType: OBJECT_TYPE_SCHEMA, toObjectType: OBJECT_TYPE_SCHEMA, }), ) .output(z.object({ labels: ASSOCIATION_LABEL_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ({ labels: ( await getHubSpotApi(account.secret).request( `crm/associations/2026-03/${hubSpotPathSegment(input.fromObjectType)}/${hubSpotPathSegment(input.toObjectType)}/labels`, { responseSchema: z.object({ results: ASSOCIATION_LABEL_SCHEMA.array(), }), }, ) ).results, })) /** Scope-specific HubSpot company association mutations. */ const companyAssociationActions = createAssociationActions("companies") /** Scope-specific HubSpot contact association mutations. */ const contactAssociationActions = createAssociationActions("contacts") /** Scope-specific HubSpot deal association mutations. */ const dealAssociationActions = createAssociationActions("deals") /** Scope-specific HubSpot ticket association mutations. */ const ticketAssociationActions = createAssociationActions("tickets") /** Associates a company with another HubSpot CRM record. */ export const associateHubSpotCompany = companyAssociationActions.associate /** Removes one association label from a HubSpot company. */ export const removeHubSpotCompanyAssociation = companyAssociationActions.remove /** Associates a contact with another HubSpot CRM record. */ export const associateHubSpotContact = contactAssociationActions.associate /** Removes one association label from a HubSpot contact. */ export const removeHubSpotContactAssociation = contactAssociationActions.remove /** Associates a deal with another HubSpot CRM record. */ export const associateHubSpotDeal = dealAssociationActions.associate /** Removes one association label from a HubSpot deal. */ export const removeHubSpotDealAssociation = dealAssociationActions.remove /** Associates a ticket with another HubSpot CRM record. */ export const associateHubSpotTicket = ticketAssociationActions.associate /** Removes one association label from a HubSpot ticket. */ export const removeHubSpotTicketAssociation = ticketAssociationActions.remove /** * Builds scope-safe association mutations for one source object type. * * @param fromObjectType - Static HubSpot source object collection. */ function createAssociationActions( fromObjectType: keyof typeof ASSOCIATION_OBJECTS, ) { const config = ASSOCIATION_OBJECTS[fromObjectType] const accountRequirement = hubSpotAccountRequirement(config.scope) const commonInput = { fromRecordId: z.string().trim().min(1), toObjectType: OBJECT_TYPE_SCHEMA, toRecordId: z.string().trim().min(1), } const path = ( fromRecordId: string, toObjectType: string, toRecordId: string, ) => `crm/objects/2026-03/${fromObjectType}/${hubSpotPathSegment(fromRecordId)}/associations/${hubSpotPathSegment(toObjectType)}/${hubSpotPathSegment(toRecordId)}` return { associate: defineAction(`Associate ${config.label}`) .describe(`Creates labeled associations from one ${config.label}.`) .account(HUBSPOT_ACCOUNT, accountRequirement) .input( z.object({ ...commonInput, associationTypes: z .object({ associationCategory: z.enum([ "HUBSPOT_DEFINED", "INTEGRATOR_DEFINED", "USER_DEFINED", ]), associationTypeId: z.number().int().positive(), }) .array() .min(1), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getHubSpotApi(account.secret).request( path(input.fromRecordId, input.toObjectType, input.toRecordId), { body: input.associationTypes, method: "PUT", responseSchema: z.looseObject({ fromObjectId: z.union([z.number(), z.string()]), fromObjectTypeId: z.string(), labels: z.string().array(), toObjectId: z.union([z.number(), z.string()]), toObjectTypeId: z.string(), }), }, ) }), remove: defineAction(`Remove ${config.label} association`) .describe(`Removes one association type from a ${config.label}.`) .account(HUBSPOT_ACCOUNT, accountRequirement) .input( z.object({ ...commonInput, associationCategory: z.enum([ "HUBSPOT_DEFINED", "INTEGRATOR_DEFINED", "USER_DEFINED", ]), associationTypeId: z.number().int().positive(), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getHubSpotApi(account.secret).request( `crm/associations/2026-03/${fromObjectType}/${hubSpotPathSegment(input.toObjectType)}/batch/labels/archive`, { body: { inputs: [ { from: { id: input.fromRecordId }, to: { id: input.toRecordId }, types: [ { associationCategory: input.associationCategory, associationTypeId: input.associationTypeId, }, ], }, ], }, method: "POST", responseSchema: z.undefined(), }, ) }), } }