Why this automation is needed
Native HubSpot workflows often struggle with complex multi-object traversals, conditional lookback attribution windows, custom phone format standardizations, and custom scoring calculations.
With Workflood, this recipe runs in a secure gVisor container sandbox with pre-authenticated HubSpot SDK injection, avoiding HubSpot Data Hub execution limits, memory bottlenecks, and per-seat fees.
Action Source Code (TypeScript)
export default async function main(input: {
contactId?: string
email?: string
marketingEventId?: string
attendanceState?: 'ATTENDED' | 'REGISTERED' | 'NO_SHOW' | 'CANCELLED'
[key: string]: unknown
}): Promise<{
associated: boolean
companyId?: string
contactId?: string
marketingEventId?: string
processedCount?: number
message: string
}> {
const rawInput = (Array.isArray(input) ? input[0] : input) || {}
let contactId = (rawInput.contactId || rawInput.contact_id || rawInput.objectId || (rawInput.object && (rawInput.object as Record<string, unknown>).objectId) || rawInput.hs_object_id || (rawInput.fields && (rawInput.fields as Record<string, unknown>).contactId)) as string | undefined
let email = (rawInput.email || (rawInput.fields && (rawInput.fields as Record<string, unknown>).email) || (rawInput.properties && (rawInput.properties as Record<string, unknown>).email)) as string | undefined
let marketingEventId = (rawInput.marketingEventId || rawInput.marketing_event_id || rawInput.eventId || rawInput.event_id || (rawInput.fields && (rawInput.fields as Record<string, unknown>).marketingEventId)) as string | undefined
const attendanceState = (rawInput.attendanceState || rawInput.attendance_state) as string | undefined
// Only link company if attendance state is not a non-attendance
if (attendanceState && attendanceState !== 'ATTENDED') {
return {
associated: false,
marketingEventId,
message: `Skipped: contact attendanceState is "${attendanceState}" (not ATTENDED)`,
}
}
// Trigger-Agnostic / Batch Fallback: if no specific contact/event is given, scan recent contacts
if (!contactId && !email && !marketingEventId) {
const recentContacts = await hubspot.client.crm.contacts.basicApi.getPage(
25,
0,
['email', 'firstname', 'lastname']
)
let linkedCount = 0
for (const c of recentContacts.results) {
const cAssoc = await hubspot.client.crm.associations.v4.basicApi.getPage('contacts', c.id, 'companies', 1)
const compId = cAssoc.results?.[0]?.toObjectId
if (!compId) continue
const eAssoc = await hubspot.client.crm.associations.v4.basicApi.getPage('contacts', c.id, 'marketing_events', 5)
for (const e of (eAssoc.results || [])) {
await hubspot.client.crm.associations.v4.basicApi.create(
'companies',
compId,
'marketing_events',
e.toObjectId,
[{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 14 }]
)
linkedCount++
}
}
return {
associated: linkedCount > 0,
processedCount: recentContacts.results.length,
message: `Trigger-agnostic scan: evaluated ${recentContacts.results.length} contacts, created ${linkedCount} company-to-event association(s)`,
}
}
// 1. Resolve contact ID if only email is passed
if (!contactId && email) {
const contactRes = await hubspot.client.crm.contacts.basicApi.getPage(
1,
0,
['email', 'associatedcompanyid'],
undefined,
undefined,
[{ value: email, propertyName: 'email', operator: 'EQ' }]
)
if (contactRes.results.length > 0) {
contactId = contactRes.results[0].id
}
}
if (!contactId) {
throw new Error('Could not determine contactId (provide contactId or valid email)')
}
// 2. Query contact associations to find the primary associated company
const assocRes = await hubspot.client.crm.associations.v4.basicApi.getPage(
'contacts',
contactId,
'companies',
5
)
const companyId = assocRes.results[0]?.toObjectId
if (!companyId) {
return {
associated: false,
contactId,
marketingEventId,
message: `No associated company found for contact ID ${contactId}`,
}
}
// If marketingEventId not specified, look up contact's attended marketing events
if (!marketingEventId) {
const eventAssocs = await hubspot.client.crm.associations.v4.basicApi.getPage(
'contacts',
contactId,
'marketing_events',
5
)
if (!eventAssocs.results || eventAssocs.results.length === 0) {
return {
associated: false,
companyId,
contactId,
message: `No marketing events associated with contact ${contactId}`,
}
}
marketingEventId = eventAssocs.results[0].toObjectId
}
// 3. Associate the company to the marketing event for ABM attribution
await hubspot.client.crm.associations.v4.basicApi.create(
'companies',
companyId,
'marketing_events',
marketingEventId,
[
{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 14,
},
]
)
return {
associated: true,
companyId,
contactId,
marketingEventId,
message: `Successfully linked company ${companyId} to marketing event ${marketingEventId}`,
}
}
How to deploy in Workflood
Connect your HubSpot Portal
Add your HubSpot Private App token once. Workflood securely encrypts it via libsodium XChaCha20 and injects pre-authenticated SDK clients.
Create Action from Template
Select this recipe in Workflood. Edit variables, customize logic in Monaco editor, or execute dry runs with sample JSON payloads.
Trigger via Webhook or Cron
Trigger from standard HubSpot workflows via Webhook action, or schedule recurring batch syncs with cron expressions and timezone handling.
Deploy in Workflood
Single Portal is $79/mo flat with unlimited team seats, 50,000 runs/mo, and no per-seat costs.
