Lead Ops
TypeScript
Cron Schedule

Stale Lead SLA Monitor & Unassigned Alerting

Identify uncontacted leads older than SLA thresholds (e.g. 5 days in NEW/OPEN status), flag SLA breach on the contact, and alert managers.

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)

stale-lead-sla-recirculation.ts
TypeScript
export default async function main(input: {
  slaDays?: number
  slackAlertWebhook?: string
  contactId?: string
  [key: string]: unknown
}): Promise<{
  scanned: number
  breachedCount: number
  breachedContactIds: 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

  const slaDays = input.slaDays ?? 5
  const cutoffTime = Date.now() - (slaDays * 24 * 60 * 60 * 1000)

  let contactsToScan: Array<{ id: string; properties: Record<string, string> }> = []

  if (contactId) {
    const c = await hubspot.client.crm.contacts.basicApi.getById(
      contactId,
      ['email', 'firstname', 'lastname', 'hs_lead_status', 'createdate', 'notes_last_contacted', 'hubspot_owner_id']
    )
    contactsToScan = [c]
  } else {
    // Query contacts in NEW or OPEN lead status
    const contacts = await hubspot.client.crm.contacts.basicApi.getPage(
      100,
      0,
      ['email', 'firstname', 'lastname', 'hs_lead_status', 'createdate', 'notes_last_contacted', 'hubspot_owner_id']
    )
    contactsToScan = contacts.results
  }

  const breachedContactIds: string[] = []

  for (const contact of contactsToScan) {
    const status = (contact.properties.hs_lead_status || '').toUpperCase()
    const isUnworked = !status || status === 'NEW' || status === 'OPEN' || status === 'UNQUALIFIED'
    const createdAt = contact.properties.createdate ? new Date(contact.properties.createdate).getTime() : 0
    const lastContacted = contact.properties.notes_last_contacted ? new Date(contact.properties.notes_last_contacted).getTime() : 0

    if (isUnworked && createdAt > 0 && createdAt < cutoffTime && lastContacted === 0) {
      breachedContactIds.push(contact.id)
      await hubspot.client.crm.contacts.basicApi.update(contact.id, {
        properties: {
          lead_sla_breached: 'true',
          lead_sla_breach_date: new Date().toISOString(),
        },
      })
    }
  }

  if (breachedContactIds.length > 0 && input.slackAlertWebhook) {
    await http.post(input.slackAlertWebhook, {
      text: `⚠️ *SLA Alert:* ${breachedContactIds.length} lead(s) have exceeded the ${slaDays}-day response SLA without contact.`,
    })
  }

  return {
    scanned: contactsToScan.length,
    breachedCount: breachedContactIds.length,
    breachedContactIds,
  }
}

Configurable Parameters

ParameterTypeDefaultDescription
slaDaysnumber5Number of business/calendar days before marking lead SLA as breached.
slackAlertWebhookstringOptional Slack webhook to notify managers when leads breach SLA.

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.