AutomationFlowsWeb Scraping › Trigger AI Outbound Calls From Hubspot and Log Call Results with Vapi

Trigger AI Outbound Calls From Hubspot and Log Call Results with Vapi

BySPCTEK AI @spctek-ai on n8n.io

This workflow triggers on HubSpot contact events, fetches full contact details, validates and normalizes the phone number, starts an AI outbound call through Vapi, polls until the call ends, and then logs the call summary and recording URL back to HubSpot as a Call activity.…

Event trigger★★★★☆ complexity18 nodesHubSpot TriggerHubSpotHTTP Request
Web Scraping Trigger: Event Nodes: 18 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #16048 — we link there as the canonical source.

This workflow follows the HTTP Request → HubSpot recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "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
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow triggers on HubSpot contact events, fetches full contact details, validates and normalizes the phone number, starts an AI outbound call through Vapi, polls until the call ends, and then logs the call summary and recording URL back to HubSpot as a Call activity.…

Source: https://n8n.io/workflows/16048/ — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

This workflow automatically enriches HubSpot company records with official data from the Polish CEIDG (Central Register and Information on Economic Activity) database when a NIP (Polish Tax ID) is add

HubSpot Trigger, HubSpot, HTTP Request
Web Scraping

This workflow aims to enrich new contacts in HubSpot. The more relevant the HubSpot profile, the more useful it is. Once active, this n8n workflow will update the social profiles, contact data (phone,

HTTP Request, HubSpot, HubSpot Trigger
Web Scraping

This template syncs prospects from ProspectPro into HubSpot. It checks if a company already exists in HubSpot (by ProspectPro ID or domain), then updates the record or creates a new one. Sync results

Execute Workflow Trigger, @Bedrijfsdatanl/N8N Nodes Prospectpro, HTTP Request +1
Web Scraping

This workflow retrieves contacts from HubSpot that have an email address but haven't yet had their email validated by Hunter. It then iterates through each of these contacts, uses Hunter.io to verify

HubSpot, Hunter, HTTP Request +1
Web Scraping

This workflow allows you to import any workflow from a file or another n8n instance and map the credentials easily. A multi-form setup guides you through the entire process At the beginning you have t

Execute Command, Read Write File, HTTP Request +3