{
  "id": "hWGJmcovPdHqeVTW",
  "name": "AI Outbound Calling from HubSpot via Vapi",
  "tags": [],
  "nodes": [
    {
      "id": "cd98436c-b974-48f4-bfcf-81077485a6e4",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2400,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 584,
        "content": "## \ud83e\udd16 AI Outbound Calling \u2014 HubSpot + Vapi\n\n**What this workflow does:**\nWhen a new contact is created ) in HubSpot, this workflow:\n1. Fetches full contact details\n2. Standardizes & validates the phone number (US formats)\n3. Triggers an AI voice call via Vapi with personalized context\n4. Polls until the call ends\n5. Logs the call summary + recording back to HubSpot\n\n**Requirements before activating:**\n- HubSpot OAuth2 credentials connected\n- HubSpot Developer API key (for the trigger)\n- Vapi account with an Assistant and Phone Number configured\n- Bearer token for Vapi API\n\n**Tip:** Update the `assistantId` and `phoneNumberId` in the **Make a Call** node with your own Vapi values."
      },
      "typeVersion": 1
    },
    {
      "id": "ae25928b-0783-47bd-8deb-e38ee7d1ae00",
      "name": "Trigger Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1952,
        48
      ],
      "parameters": {
        "color": 4,
        "width": 296,
        "height": 192,
        "content": "### Step 1 \u2014 Trigger\nFires whenever a contact event occurs in HubSpot (e.g. contact created).\n\n\u26a0\ufe0f Make sure to select the correct event type in the trigger settings \u2014 by default it listens for all contact events."
      },
      "typeVersion": 1
    },
    {
      "id": "a4220cc8-a016-430a-966a-5a76a237739a",
      "name": "Parse Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1632,
        -48
      ],
      "parameters": {
        "color": 4,
        "width": 320,
        "height": 280,
        "content": "### Step 2 \u2014 Fetch & Parse Contact\nFetches full contact properties from HubSpot, then normalizes key fields:\n- `firstName`, `email`, `company`, `message`\n- Phone standardization for **US** (10-digit) formats\n- Detects country code automatically\n\nIf the phone number can't be parsed, it's flagged as `\"incorrect format\"` and the **Check Phone Format** node stops the flow."
      },
      "typeVersion": 1
    },
    {
      "id": "4fce0e9f-4980-41b2-960b-58342d17881c",
      "name": "Phone Check Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1296,
        -48
      ],
      "parameters": {
        "color": 6,
        "width": 280,
        "height": 272,
        "content": "### Step 3 \u2014 Phone Validation\nRoutes the flow:\n- \u2705 **True** (phone = \"incorrect format\") \u2192 Flow stops, no call made\n- \u2705 **False** (valid phone) \u2192 Proceeds to Vapi call\n\nThis prevents API errors from malformed numbers."
      },
      "typeVersion": 1
    },
    {
      "id": "758583f4-9a82-476f-8773-302669a80007",
      "name": "Make Call Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -976,
        -96
      ],
      "parameters": {
        "color": 4,
        "width": 316,
        "height": 364,
        "content": "### Step 4 \u2014 Make Vapi AI Call\nCalls the Vapi `/call` endpoint with:\n- Your assistant ID and phone number ID\n- Lead's raw phone number as the dialing target\n- Personalized variables injected into the assistant:\n  - `lead_name`, `lead_company_name`, `lead_request`, `lead_email`\n\n\ud83d\udd27 **Update these values:**\n- `assistantId` \u2014 your Vapi assistant UUID\n- `phoneNumberId` \u2014 your Vapi phone number UUID"
      },
      "typeVersion": 1
    },
    {
      "id": "2ca3c510-d827-4a44-a452-c0ef2fd5bb25",
      "name": "Polling Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -640,
        -80
      ],
      "parameters": {
        "color": 5,
        "width": 340,
        "height": 348,
        "content": "### Step 5 \u2014 Polling Loop\nWaits for the call to finish using a polling pattern:\n1. **Wait 2 min** \u2014 initial buffer for the call to start and run\n2. **Get Call Details** \u2014 fetches call status from Vapi\n3. **Pick One** \u2014 ensures only one item continues (deduplication)\n4. **Check Status** \u2014 if `status == \"ended\"`, exits the loop\n5. **Polling (10s wait)** \u2014 if still in progress, waits and re-polls\n\n\u23f1\ufe0f Adjust the wait times based on your average call duration."
      },
      "typeVersion": 1
    },
    {
      "id": "0985131a-7be2-4812-b1fa-11a06ea3f377",
      "name": "Log Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        -144
      ],
      "parameters": {
        "color": 4,
        "width": 320,
        "height": 312,
        "content": "### Step 6 \u2014 Log Call to HubSpot\nCreates a **Call activity** in HubSpot and associates it with the original contact:\n- Call title, direction (OUTBOUND), status (COMPLETED)\n- Duration calculated from `startedAt` / `endedAt`\n- Body contains the AI-generated summary + recording URL\n\n\ud83d\udccc The contact association uses `associationTypeId: 194` (call \u2192 contact). This is a standard HubSpot association type."
      },
      "typeVersion": 1
    },
    {
      "id": "9dc1db22-f408-4b08-9abd-27777f3231e5",
      "name": "HubSpot Trigger",
      "type": "n8n-nodes-base.hubspotTrigger",
      "position": [
        -1856,
        256
      ],
      "parameters": {
        "eventsUi": {
          "eventValues": [
            {}
          ]
        },
        "additionalFields": {}
      },
      "credentials": {
        "hubspotDeveloperApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "cad5ca6f-17c6-40b4-bd84-6ef2b31f55db",
      "name": "Get a contact",
      "type": "n8n-nodes-base.hubspot",
      "position": [
        -1648,
        256
      ],
      "parameters": {
        "contactId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.contactId }}"
        },
        "operation": "get",
        "authentication": "oAuth2",
        "additionalFields": {}
      },
      "credentials": {
        "hubspotOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "4a54b2b7-12cb-492b-9502-478fd0047429",
      "name": "Parse Response",
      "type": "n8n-nodes-base.code",
      "position": [
        -1456,
        256
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nreturn items.map(item => {\n  const props = item.json.properties || {};\n\n  // \u2500\u2500\u2500 Extract HubSpot properties correctly \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const firstName   = props.firstname?.value || '';\n  const email       = props.email?.value || '';\n  const company     = props.company?.value || '';\n  const message     = props.message?.value || '';\n  const rawPhone    = props.phone?.value || props.hs_calculated_phone_number?.value || '';\n  const ipCountry   = props.ip_country?.value || '';\n  const ipCity      = props.ip_city?.value || '';\n  const lifecycle   = props.lifecyclestage?.value || '';\n  const source      = props.hs_latest_source?.value || '';\n\n  // \u2500\u2500\u2500 Standardize phone for PK + US \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  let cleanNumber = String(rawPhone).replace(/\\D/g, ''); // strip everything except digits\n  let standardizedPhone;\n  let countryCode;\n\n  if (cleanNumber.startsWith('92') && cleanNumber.length === 12) {\n    // Pakistani number with country code: 923XXXXXXXXX \u2192 03XXXXXXXXX\n    standardizedPhone = '0' + cleanNumber.slice(2);   // local format\n    countryCode = 'PK';\n\n  } else if (cleanNumber.startsWith('0') && cleanNumber.length === 11) {\n    // Pakistani local format already: 03XXXXXXXXX\n    standardizedPhone = cleanNumber;\n    countryCode = 'PK';\n\n  } else if (cleanNumber.length === 10) {\n    // Could be PK (3XXXXXXXXX) or US (10 digits, no country code)\n    if (cleanNumber.startsWith('3')) {\n      // PK number without leading 0\n      standardizedPhone = '0' + cleanNumber;\n      countryCode = 'PK';\n    } else {\n      // US 10-digit number\n      standardizedPhone = cleanNumber;\n      countryCode = 'US';\n    }\n\n  } else if (cleanNumber.startsWith('1') && cleanNumber.length === 11) {\n    // US number with country code: 1XXXXXXXXXX \u2192 10 digits\n    standardizedPhone = cleanNumber.slice(1);\n    countryCode = 'US';\n\n  } else {\n    standardizedPhone = 'incorrect format';\n    countryCode = 'unknown';\n  }\n\n  return {\n    json: {\n      firstName,\n      email,\n      company,\n      message,\n      phone: standardizedPhone,\n      countryCode,\n      rawPhone,\n      ipCountry,\n      ipCity,\n      lifecycle,\n      source,\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "a505b834-e348-4165-9f99-427593afb11d",
      "name": "Check Phone Format",
      "type": "n8n-nodes-base.if",
      "position": [
        -1232,
        256
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e68a68dc-720b-423e-84f5-1b7aa9238daa",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json[\"phone\"] }}",
              "rightValue": "incorrect format"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "2405c540-4081-4d2f-8ee9-434123da7d76",
      "name": "Make a Call",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -976,
        352
      ],
      "parameters": {
        "url": "https://api.vapi.ai/call",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"assistantId\": \"YOUR_ASSISTANT_ID\",\n  \"phoneNumberId\": \"YOUR_PHONE_NUMBER_ID\",\n  \"customers\": [\n    {\n      \"number\": \"{{ $json['rawPhone'] }}\"\n    }\n  ],\n  \"assistantOverrides\": {\n    \"variableValues\": {\n      \"lead_name\": \"{{ $json.firstName }}\",\n      \"lead_company_name\": \"{{ $json['company'] }}\",\n      \"lead_request\": \"{{ $json.message }}\",\n      \"lead_email\": \"{{ $json.email }}\"\n    }\n  }\n}\n",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "01178462-0401-4d90-a596-fff12dce4ebb",
      "name": "Wait for 2 minutes",
      "type": "n8n-nodes-base.wait",
      "position": [
        -768,
        352
      ],
      "parameters": {
        "unit": "minutes",
        "amount": 2
      },
      "typeVersion": 1.1
    },
    {
      "id": "50565f73-9bfa-4b4a-906b-383bd45df42b",
      "name": "Get Vapi Call Details",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -544,
        352
      ],
      "parameters": {
        "url": "=https://api.vapi.ai/call/{{ $json.results[0].id }}",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "executeOnce": false,
      "typeVersion": 4.3
    },
    {
      "id": "3bd1cd0a-3b20-4f43-98dc-65f9d79de310",
      "name": "Pick One",
      "type": "n8n-nodes-base.limit",
      "position": [
        -336,
        352
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "55c900d7-587c-41fe-8712-556851d64e13",
      "name": "Check Status",
      "type": "n8n-nodes-base.if",
      "position": [
        -160,
        352
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "30c7ebf4-6de9-49f3-9a27-b7f3a7e2b558",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "ended"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "67839f8d-c0af-4645-9480-490639689002",
      "name": "Log Call",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        80,
        192
      ],
      "parameters": {
        "url": "https://api.hubapi.com/crm/v3/objects/calls",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"properties\": {\n    \"hs_call_title\": \"AI Outbound Call - {{ $('Parse Response').item.json.firstName }}\",\n    \"hs_call_direction\": \"OUTBOUND\",\n    \"hs_call_status\": \"COMPLETED\",\n    \"hs_call_duration\": \"{{ Math.round((new Date($json.endedAt) - new Date($json.startedAt))) }}\",\n    \"hs_timestamp\": \"{{ $json.startedAt }}\",\n    \"hs_call_body\": \"\ud83d\udccb SUMMARY:\\n{{ $json.analysis.summary }}\\n\\n\ud83d\udcde TRANSCRIPT:\\n{{ $json.recordingUrl }}\"\n  },\n  \"associations\": [\n    {\n      \"to\": { \"id\": \"{{ $('HubSpot Trigger').item.json.contactId }}\" },\n      \"types\": [\n        {\n          \"associationCategory\": \"HUBSPOT_DEFINED\",\n          \"associationTypeId\": 194\n        }\n      ]\n    }\n  ]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": []
        },
        "nodeCredentialType": "hubspotOAuth2Api"
      },
      "credentials": {
        "hubspotOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "42550cb2-0da3-4100-881c-e3e5c3165213",
      "name": "Polling",
      "type": "n8n-nodes-base.wait",
      "position": [
        64,
        496
      ],
      "parameters": {
        "amount": 10
      },
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "039b9307-c12c-4452-ae4c-a6ca04ea5e9a",
  "connections": {
    "Polling": {
      "main": [
        [
          {
            "node": "Get Vapi Call Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick One": {
      "main": [
        [
          {
            "node": "Check Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Make a Call": {
      "main": [
        [
          {
            "node": "Wait for 2 minutes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Status": {
      "main": [
        [
          {
            "node": "Log Call",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Polling",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get a contact": {
      "main": [
        [
          {
            "node": "Parse Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Response": {
      "main": [
        [
          {
            "node": "Check Phone Format",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HubSpot Trigger": {
      "main": [
        [
          {
            "node": "Get a contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Phone Format": {
      "main": [
        [],
        [
          {
            "node": "Make a Call",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for 2 minutes": {
      "main": [
        [
          {
            "node": "Get Vapi Call Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Vapi Call Details": {
      "main": [
        [
          {
            "node": "Pick One",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}