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: {
dealId?: string
contactId?: string
marketingEventId?: string
eventAttendedDate?: string
lookbackMonths?: number
removeOutsideWindow?: boolean
[key: string]: unknown
}): Promise<{
associatedDealsCount: number
associatedDealIds: string[]
marketingEventId?: string
contactId?: string
message: string
}> {
const rawInput = (Array.isArray(input) ? input[0] : input) || {}
const dealId = (rawInput.dealId || rawInput.deal_id || rawInput.objectId || (rawInput.object && (rawInput.object as Record<string, unknown>).objectId) || rawInput.hs_object_id || (rawInput.fields && (rawInput.fields as Record<string, unknown>).dealId)) as string | undefined
const contactId = (rawInput.contactId || rawInput.contact_id || (rawInput.fields && (rawInput.fields as Record<string, unknown>).contactId)) as string | undefined
const marketingEventId = (rawInput.marketingEventId || rawInput.marketing_event_id || rawInput.eventId || rawInput.event_id) as string | undefined
const eventAttendedDate = (rawInput.eventAttendedDate || rawInput.event_attended_date) as string | undefined
const lookbackMonths = input.lookbackMonths ?? 3
const removeOutsideWindow = input.removeOutsideWindow ?? false
const lookbackMs = lookbackMonths * 30 * 24 * 60 * 60 * 1000
const now = Date.now()
// Scenario A: Triggered on Deal Creation (dealId provided)
if (dealId) {
const deal = await hubspot.client.crm.deals.basicApi.getById(
dealId,
['dealname', 'createdate', 'amount']
)
const dealCreatedTime = deal.properties.createdate
? new Date(deal.properties.createdate).getTime()
: now
const contactAssocs = await hubspot.client.crm.associations.v4.basicApi.getPage(
'deals',
dealId,
'contacts',
50
)
if (!contactAssocs.results || contactAssocs.results.length === 0) {
return {
associatedDealsCount: 0,
associatedDealIds: [],
message: `No associated contacts found on Deal ${dealId}`,
}
}
const linkedEventIds: string[] = []
for (const contactAssoc of contactAssocs.results) {
const cId = contactAssoc.toObjectId
const eventAssocs = await hubspot.client.crm.associations.v4.basicApi.getPage(
'contacts',
cId,
'marketing_events',
50
)
for (const eventAssoc of eventAssocs.results) {
const mEventId = eventAssoc.toObjectId
let isWithinWindow = true
try {
const eventRecord = await hubspot.client.marketing.events.marketingEventsExternalApi.getById(
mEventId,
'HUBSPOT'
)
const eventStart = eventRecord.startDateTime
? new Date(eventRecord.startDateTime).getTime()
: null
if (eventStart) {
const timeDiff = dealCreatedTime - eventStart
isWithinWindow = timeDiff >= 0 && timeDiff <= lookbackMs
}
} catch {
isWithinWindow = true
}
if (isWithinWindow) {
await hubspot.client.crm.associations.v4.basicApi.create(
'deals',
dealId,
'marketing_events',
mEventId,
[
{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 15,
},
]
)
linkedEventIds.push(mEventId)
}
}
}
return {
associatedDealsCount: linkedEventIds.length > 0 ? 1 : 0,
associatedDealIds: linkedEventIds.length > 0 ? [dealId] : [],
marketingEventId: linkedEventIds[0],
message: linkedEventIds.length > 0
? `Successfully linked Deal ${dealId} to ${linkedEventIds.length} attended Marketing Event(s)`
: `No marketing events attended by associated contacts within ${lookbackMonths} months of deal creation`,
}
}
// Scenario B: Triggered on Marketing Event Attendance (contactId + marketingEventId provided)
if (contactId && marketingEventId) {
const attendanceTime = eventAttendedDate
? new Date(eventAttendedDate).getTime()
: now
const dealAssocs = await hubspot.client.crm.associations.v4.basicApi.getPage(
'contacts',
contactId,
'deals',
50
)
if (!dealAssocs.results || dealAssocs.results.length === 0) {
return {
associatedDealsCount: 0,
associatedDealIds: [],
contactId,
marketingEventId,
message: `No associated deals found for Contact ${contactId}`,
}
}
const associatedDealIds: string[] = []
for (const dealAssoc of dealAssocs.results) {
const dId = dealAssoc.toObjectId
const dealRecord = await hubspot.client.crm.deals.basicApi.getById(
dId,
['dealname', 'createdate']
)
const dealCreatedTime = dealRecord.properties.createdate
? new Date(dealRecord.properties.createdate).getTime()
: null
if (dealCreatedTime) {
const timeDiff = dealCreatedTime - attendanceTime
if (timeDiff >= 0 && timeDiff <= lookbackMs) {
await hubspot.client.crm.associations.v4.basicApi.create(
'deals',
dId,
'marketing_events',
marketingEventId,
[
{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 15,
},
]
)
associatedDealIds.push(dId)
}
}
}
return {
associatedDealsCount: associatedDealIds.length,
associatedDealIds,
contactId,
marketingEventId,
message: `Associated ${associatedDealIds.length} deal(s) created within ${lookbackMonths} months of event attendance to Marketing Event ${marketingEventId}`,
}
}
// Scenario C: Trigger-Agnostic / Scheduled / Manual Batch Auto-Discovery
const recentDeals = await hubspot.client.crm.deals.basicApi.getPage(
25,
0,
['dealname', 'createdate', 'amount']
)
const allAssociatedDealIds: string[] = []
for (const d of recentDeals.results) {
const dId = d.id
const dCreatedTime = d.properties.createdate
? new Date(d.properties.createdate).getTime()
: now
if (now - dCreatedTime > lookbackMs) continue
const cAssocs = await hubspot.client.crm.associations.v4.basicApi.getPage(
'deals',
dId,
'contacts',
20
)
for (const cAssoc of (cAssocs.results || [])) {
const cId = cAssoc.toObjectId
const eAssocs = await hubspot.client.crm.associations.v4.basicApi.getPage(
'contacts',
cId,
'marketing_events',
20
)
for (const eAssoc of (eAssocs.results || [])) {
const mId = eAssoc.toObjectId
await hubspot.client.crm.associations.v4.basicApi.create(
'deals',
dId,
'marketing_events',
mId,
[
{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 15,
},
]
)
if (!allAssociatedDealIds.includes(dId)) {
allAssociatedDealIds.push(dId)
}
}
}
}
return {
associatedDealsCount: allAssociatedDealIds.length,
associatedDealIds: allAssociatedDealIds,
message: `Trigger-agnostic scan: evaluated ${recentDeals.results.length} recent deals, linked ${allAssociatedDealIds.length} deal(s) within ${lookbackMonths}-month window to marketing events`,
}
}
Configurable Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| lookbackMonths | number | 3 | Number of months prior to deal creation that contact event attendance is eligible for attribution. |
| removeOutsideWindow | boolean | false | If enabled, un-associates any existing deals created outside the attribution window. |
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.
