import { HUBSPOT_ASSOCIATION_INPUT_SCHEMA, HUBSPOT_PROVIDER_RECORD_SCHEMA, HUBSPOT_RECORD_SCHEMA, toHubSpotRecord, } from "@automate.ax/integration-contracts/hubspot" import * as z from "zod" import { defineAction } from "../../automation/actions" import { getHubSpotApi, HUBSPOT_ACCOUNT, hubSpotAccountRequirement, } from "./lib" const ACTIVITY_SCOPE = "crm.objects.contacts.write" const COMMON_FIELDS = { associations: HUBSPOT_ASSOCIATION_INPUT_SCHEMA.array().optional(), ownerId: z.string().trim().min(1).optional(), timestamp: z.date(), } const CALL_STATUS_SCHEMA = z.enum([ "BUSY", "CANCELED", "CALLING_CRM_USER", "COMPLETED", "CONNECTING", "FAILED", "IN_PROGRESS", "NO_ANSWER", "QUEUED", "RINGING", ]) const TASK_STATUS_SCHEMA = z.enum(["COMPLETED", "NOT_STARTED"]) const TASK_PRIORITY_SCHEMA = z.enum(["HIGH", "LOW", "MEDIUM"]) /** Logs a call on one or more HubSpot CRM records. */ export const logHubSpotCall = defineAction("Log HubSpot call") .describe("Logs an inbound or outbound call on HubSpot record timelines.") .account(HUBSPOT_ACCOUNT, hubSpotAccountRequirement(ACTIVITY_SCOPE)) .input( z.object({ ...COMMON_FIELDS, body: z.string().optional(), direction: z.enum(["INBOUND", "OUTBOUND"]).optional(), durationMilliseconds: z.number().int().nonnegative().optional(), fromNumber: z.string().optional(), recordingUrl: z.url({ protocol: /^https$/ }).optional(), status: CALL_STATUS_SCHEMA.optional(), title: z.string().optional(), toNumber: z.string().optional(), }), ) .output(HUBSPOT_RECORD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => createActivity(account.secret, "calls", input, { hs_call_body: input.body, hs_call_direction: input.direction, hs_call_duration: numberString(input.durationMilliseconds), hs_call_from_number: input.fromNumber, hs_call_recording_url: input.recordingUrl, hs_call_status: input.status, hs_call_title: input.title, hs_call_to_number: input.toNumber, }), ) /** Logs a sent or received email on HubSpot record timelines. */ export const logHubSpotEmail = defineAction("Log HubSpot email") .describe( "Logs email headers and content on HubSpot record timelines without sending mail.", ) .account(HUBSPOT_ACCOUNT, hubSpotAccountRequirement(ACTIVITY_SCOPE)) .input( z.object({ ...COMMON_FIELDS, direction: z .enum(["EMAIL", "FORWARDED_EMAIL", "INCOMING_EMAIL"]) .optional(), from: z.email().optional(), html: z.string().optional(), status: z .enum(["BOUNCED", "FAILED", "SCHEDULED", "SENDING", "SENT"]) .optional(), subject: z.string().optional(), text: z.string().optional(), to: z.email().optional(), }), ) .output(HUBSPOT_RECORD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => createActivity(account.secret, "emails", input, { hs_email_direction: input.direction, hs_email_headers: input.from || input.to ? JSON.stringify({ ...(input.from && { from: { email: input.from } }), ...(input.to && { to: [{ email: input.to }] }), }) : undefined, hs_email_html: input.html, hs_email_status: input.status, hs_email_subject: input.subject, hs_email_text: input.text, }), ) /** Creates a meeting activity on HubSpot record timelines. */ export const createHubSpotMeeting = defineAction("Create HubSpot meeting") .describe( "Creates a meeting activity with optional timing, location, and attendee details.", ) .account(HUBSPOT_ACCOUNT, hubSpotAccountRequirement(ACTIVITY_SCOPE)) .input( z.object({ ...COMMON_FIELDS, body: z.string().optional(), endTime: z.date().optional(), externalUrl: z.url().optional(), location: z.string().optional(), outcome: z .enum(["CANCELED", "COMPLETED", "NO_SHOW", "RESCHEDULED", "SCHEDULED"]) .optional(), startTime: z.date().optional(), title: z.string().optional(), }), ) .output(HUBSPOT_RECORD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => createActivity(account.secret, "meetings", input, { hs_internal_meeting_notes: input.body, hs_meeting_end_time: dateString(input.endTime), hs_meeting_external_url: input.externalUrl, hs_meeting_location: input.location, hs_meeting_outcome: input.outcome, hs_meeting_start_time: dateString(input.startTime), hs_meeting_title: input.title, }), ) /** Creates a note activity on HubSpot record timelines. */ export const createHubSpotNote = defineAction("Create HubSpot note") .describe( "Creates a text note with optional attachments on HubSpot record timelines.", ) .account(HUBSPOT_ACCOUNT, hubSpotAccountRequirement(ACTIVITY_SCOPE)) .input( z.object({ ...COMMON_FIELDS, attachmentIds: z.string().trim().min(1).array().optional(), body: z.string().max(65_536).optional(), }), ) .output(HUBSPOT_RECORD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => createActivity(account.secret, "notes", input, { hs_attachment_ids: input.attachmentIds?.join(";"), hs_note_body: input.body, }), ) /** Creates a task activity on HubSpot record timelines. */ export const createHubSpotTask = defineAction("Create HubSpot task") .describe( "Creates a HubSpot to-do, email, or call task on CRM record timelines.", ) .account(HUBSPOT_ACCOUNT, hubSpotAccountRequirement(ACTIVITY_SCOPE)) .input( z.object({ ...COMMON_FIELDS, body: z.string().optional(), priority: TASK_PRIORITY_SCHEMA.optional(), status: TASK_STATUS_SCHEMA.prefault("NOT_STARTED"), subject: z.string().optional(), taskType: z.enum(["CALL", "EMAIL", "TODO"]).prefault("TODO"), }), ) .output(HUBSPOT_RECORD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => createActivity(account.secret, "tasks", input, { hs_task_body: input.body, hs_task_priority: input.priority, hs_task_status: input.status, hs_task_subject: input.subject, hs_task_type: input.taskType, }), ) /** * Creates one CRM activity with shared ownership and association fields. * * @param secret - Stored HubSpot account secret. * @param objectType - Activity object collection. * @param common - Shared timestamp, owner, and association input. * @param properties - Activity-specific provider properties. */ async function createActivity( secret: unknown, objectType: "calls" | "emails" | "meetings" | "notes" | "tasks", common: z.output>, properties: Record, ) { return toHubSpotRecord( await getHubSpotApi(secret).request(`crm/objects/2026-03/${objectType}`, { body: { ...(common.associations && { associations: common.associations.map(({ toRecordId, types }) => ({ to: { id: toRecordId }, types, })), }), properties: Object.fromEntries( [ ["hs_timestamp", common.timestamp.toISOString()], ["hubspot_owner_id", common.ownerId], ...Object.entries(properties), ].filter( (entry): entry is [string, string] => entry[1] !== undefined, ), ), }, method: "POST", responseSchema: HUBSPOT_PROVIDER_RECORD_SCHEMA, }), ) } /** * Converts an optional date to HubSpot's ISO representation. * * @param value - Optional activity date. */ function dateString(value: Date | undefined) { return value?.toISOString() } /** * Converts an optional number to HubSpot's property representation. * * @param value - Optional activity number. */ function numberString(value: number | undefined) { return value?.toString() }