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 (Python)
import datetime
def main(input_data, hs_client, http_client, secret, console):
raw = input_data[0] if isinstance(input_data, list) and input_data else (input_data or {})
deal_id = raw.get('deal_id') or raw.get('dealId') or raw.get('objectId') or raw.get('hs_object_id')
contact_id = raw.get('contact_id') or raw.get('contactId')
marketing_event_id = raw.get('marketing_event_id') or raw.get('marketingEventId')
lookback_months = int(input_data.get('lookback_months', 3))
remove_outside_window = bool(input_data.get('remove_outside_window', False))
lookback_delta = datetime.timedelta(days=lookback_months * 30)
now = datetime.datetime.utcnow()
# Scenario A: Triggered when Deal is created
if deal_id:
deal = hs_client.crm.deals.basic_api.get_by_id(deal_id, properties=['dealname', 'createdate'])
create_str = deal.properties.get('createdate')
deal_created = datetime.datetime.fromisoformat(create_str.replace('Z', '+00:00')) if create_str else now
contact_assocs = hs_client.crm.associations.v4.basic_api.get_page(
from_object_type='deals',
object_id=deal_id,
to_object_type='contacts',
limit=50
)
if not contact_assocs.results:
return {"associatedDealsCount": 0, "associatedDealIds": [], "message": f"No associated contacts on Deal {deal_id}"}
linked_events = []
for c_assoc in contact_assocs.results:
cid = c_assoc.to_object_id
event_assocs = hs_client.crm.associations.v4.basic_api.get_page(
from_object_type='contacts',
object_id=cid,
to_object_type='marketing_events',
limit=50
)
for e_assoc in event_assocs.results:
eid = e_assoc.to_object_id
hs_client.crm.associations.v4.basic_api.create(
from_object_type='deals',
object_id=deal_id,
to_object_type='marketing_events',
to_object_id=eid,
types=[{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 15}]
)
linked_events.append(eid)
return {
"associatedDealsCount": 1 if linked_events else 0,
"associatedDealIds": [deal_id] if linked_events else [],
"message": f"Linked Deal {deal_id} to {len(linked_events)} Marketing Event(s)"
}
# Scenario B: Triggered when Contact attends event
if contact_id and marketing_event_id:
deal_assocs = hs_client.crm.associations.v4.basic_api.get_page(
from_object_type='contacts',
object_id=contact_id,
to_object_type='deals',
limit=50
)
associated_deals = []
for d_assoc in deal_assocs.results:
did = d_assoc.to_object_id
d_record = hs_client.crm.deals.basic_api.get_by_id(did, properties=['createdate'])
create_str = d_record.properties.get('createdate')
if create_str:
d_created = datetime.datetime.fromisoformat(create_str.replace('Z', '+00:00'))
if (now - d_created) <= lookback_delta:
hs_client.crm.associations.v4.basic_api.create(
from_object_type='deals',
object_id=did,
to_object_type='marketing_events',
to_object_id=marketing_event_id,
types=[{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 15}]
)
associated_deals.append(did)
return {
"associatedDealsCount": len(associated_deals),
"associatedDealIds": associated_deals,
"marketingEventId": marketing_event_id,
"message": f"Associated {len(associated_deals)} Deal(s) created within {lookback_months} months to Marketing Event {marketing_event_id}"
}
# Scenario C: Trigger-Agnostic / Scheduled / Manual Batch Auto-Discovery
recent_deals = hs_client.crm.deals.basic_api.get_page(
limit=25,
properties=['dealname', 'createdate', 'amount']
)
all_associated = []
for d in (recent_deals.results or []):
did = d.id
create_str = d.properties.get('createdate')
if create_str:
d_created = datetime.datetime.fromisoformat(create_str.replace('Z', '+00:00'))
if (now - d_created) > lookback_delta:
continue
c_assocs = hs_client.crm.associations.v4.basic_api.get_page(
from_object_type='deals',
object_id=did,
to_object_type='contacts',
limit=20
)
for c in (c_assocs.results or []):
cid = c.to_object_id
e_assocs = hs_client.crm.associations.v4.basic_api.get_page(
from_object_type='contacts',
object_id=cid,
to_object_type='marketing_events',
limit=20
)
for e in (e_assocs.results or []):
eid = e.to_object_id
hs_client.crm.associations.v4.basic_api.create(
from_object_type='deals',
object_id=did,
to_object_type='marketing_events',
to_object_id=eid,
types=[{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 15}]
)
if did not in all_associated:
all_associated.append(did)
return {
"associatedDealsCount": len(all_associated),
"associatedDealIds": all_associated,
"message": f"Trigger-agnostic scan: evaluated {len(recent_deals.results or [])} recent deals, linked {len(all_associated)} deal(s) within {lookback_months}-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.
