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: {
email?: string
contactId?: string
company?: string
[key: string]: unknown
}): Promise<{ ownerId: string; assigned: boolean; assignedCount?: number; details?: string }> {
const rawInput = (Array.isArray(input) ? input[0] : input) || {}
const contactId = (rawInput.contactId || rawInput.contact_id || rawInput.objectId || (rawInput.object && (rawInput.object as Record<string, unknown>).objectId) || rawInput.hs_object_id) 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
const owners = await hubspot.client.crm.owners.ownersApi.getPage()
const availableOwners = owners.results.filter((o) => o.archived !== true)
if (availableOwners.length === 0) {
return { ownerId: 'unassigned', assigned: false, details: 'No active HubSpot owners found' }
}
if (contactId && !email) {
const contact = await hubspot.client.crm.contacts.basicApi.getById(contactId, ['email', 'hubspot_owner_id'])
email = contact.properties.email
if (contact.properties.hubspot_owner_id) {
return { ownerId: contact.properties.hubspot_owner_id, assigned: true, details: `Contact ${contactId} already assigned` }
}
}
if (contactId) {
const targetOwner = availableOwners[0]
await hubspot.client.crm.contacts.basicApi.update(contactId, {
properties: { hubspot_owner_id: targetOwner.id },
})
return { ownerId: targetOwner.id, assigned: true, details: `Assigned Contact ${contactId} to owner ${targetOwner.email || targetOwner.id}` }
}
if (email) {
const contacts = await hubspot.client.crm.contacts.basicApi.getPage(
1, 0, ['email', 'hubspot_owner_id'], undefined, undefined, [{ value: email, propertyName: 'email', operator: 'EQ' }]
)
if (contacts.results.length > 0) {
const contact = contacts.results[0]
if (contact.properties.hubspot_owner_id) {
return { ownerId: contact.properties.hubspot_owner_id, assigned: true, details: `Contact ${email} already assigned` }
}
const targetOwner = availableOwners[0]
await hubspot.client.crm.contacts.basicApi.update(contact.id, {
properties: { hubspot_owner_id: targetOwner.id },
})
return { ownerId: targetOwner.id, assigned: true, details: `Assigned Contact ${contact.id} to owner ${targetOwner.email || targetOwner.id}` }
}
}
// Trigger-Agnostic Fallback: Batch assign unassigned leads
const unassigned = await hubspot.client.crm.contacts.basicApi.getPage(
25, 0, ['email', 'firstname', 'lastname', 'hubspot_owner_id']
)
let count = 0
let idx = 0
for (const c of unassigned.results) {
if (!c.properties.hubspot_owner_id) {
const targetOwner = availableOwners[idx % availableOwners.length]
await hubspot.client.crm.contacts.basicApi.update(c.id, {
properties: { hubspot_owner_id: targetOwner.id },
})
count++
idx++
}
}
return {
ownerId: availableOwners[0]?.id || 'unassigned',
assigned: count > 0,
assignedCount: count,
details: `Trigger-agnostic scan: routed ${count} unassigned contact(s) across ${availableOwners.length} owner(s)`,
}
}
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.
