Marketing Ops
TypeScript
Webhook Trigger

Associate Deals to Marketing Event on Post-Attendance Creation

Automatically link deals created within 3 months of contact marketing event attendance to the attended event for multi-touch revenue attribution.

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)

associate-deal-marketing-event.ts
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

ParameterTypeDefaultDescription
lookbackMonthsnumber3Number of months prior to deal creation that contact event attendance is eligible for attribution.
removeOutsideWindowbooleanfalseIf enabled, un-associates any existing deals created outside the attribution window.

How to deploy in Workflood

1

Connect your HubSpot Portal

Add your HubSpot Private App token once. Workflood securely encrypts it via libsodium XChaCha20 and injects pre-authenticated SDK clients.

2

Create Action from Template

Select this recipe in Workflood. Edit variables, customize logic in Monaco editor, or execute dry runs with sample JSON payloads.

3

Trigger via Webhook or Cron

Trigger from standard HubSpot workflows via Webhook action, or schedule recurring batch syncs with cron expressions and timezone handling.