{
  "name": "Batch Enrich Missing Fields — HubSpot + Apollo",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 22,
              "field": "weeks",
              "triggerAtDay": 0
            }
          ]
        }
      },
      "id": "schedule-trigger",
      "name": "Weekly Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, 0]
    },
    {
      "parameters": {
        "jsCode": "const HUBSPOT_ACCESS_TOKEN = $credentials.hubspotApi?.accessToken;\nconst allContacts = [];\nlet after = 0;\n\nwhile (true) {\n  const resp = await fetch(\"https://api.hubapi.com/crm/v3/objects/contacts/search\", {\n    method: \"POST\",\n    headers: {\n      \"Authorization\": `Bearer ${HUBSPOT_ACCESS_TOKEN}`,\n      \"Content-Type\": \"application/json\"\n    },\n    body: JSON.stringify({\n      filterGroups: [{ filters: [{ propertyName: \"jobtitle\", operator: \"NOT_HAS_PROPERTY\" }] }],\n      properties: [\"email\", \"firstname\", \"lastname\", \"jobtitle\", \"company\", \"phone\", \"linkedin_url\"],\n      limit: 100,\n      after\n    })\n  });\n  const data = await resp.json();\n  allContacts.push(...data.results);\n\n  if (data.paging?.next?.after) {\n    after = data.paging.next.after;\n  } else {\n    break;\n  }\n}\n\nreturn allContacts.map(c => ({ json: c }));"
      },
      "id": "fetch-contacts",
      "name": "Fetch Contacts Missing Fields",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [220, 0],
      "credentials": {
        "hubspotApi": {
          "id": "credential-id",
          "name": "HubSpot API"
        }
      }
    },
    {
      "parameters": {
        "batchSize": 10,
        "options": {}
      },
      "id": "split-batches",
      "name": "Split In Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [440, 0]
    },
    {
      "parameters": {
        "jsCode": "const contacts = $input.all().map(item => ({\n  email: item.json.properties.email,\n  first_name: item.json.properties.firstname,\n  last_name: item.json.properties.lastname,\n}));\n\nreturn [{ json: { details: contacts } }];"
      },
      "id": "format-batch",
      "name": "Format Batch",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [660, 0]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.apollo.io/api/v1/people/bulk_match",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $credentials.apolloApi.apiKey }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {}
      },
      "id": "apollo-bulk",
      "name": "Apollo Bulk Match",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [880, 0]
    },
    {
      "parameters": {
        "jsCode": "const batchContacts = $('Split In Batches').all();\nconst apolloMatches = $input.first().json.matches || [];\nconst updates = [];\n\nfor (let i = 0; i < batchContacts.length; i++) {\n  const contact = batchContacts[i].json;\n  const match = apolloMatches[i];\n\n  if (!match) continue;\n\n  const properties = {};\n  const props = contact.properties;\n\n  // Only fill empty fields — never overwrite existing data\n  if (!props.jobtitle && match.title) properties.jobtitle = match.title;\n  if (!props.company && match.organization?.name) properties.company = match.organization.name;\n  if (!props.phone && match.phone_numbers?.[0]?.sanitized_number) {\n    properties.phone = match.phone_numbers[0].sanitized_number;\n  }\n  if (!props.linkedin_url && match.linkedin_url) properties.linkedin_url = match.linkedin_url;\n  if (!props.industry && match.organization?.industry) properties.industry = match.organization.industry;\n\n  if (Object.keys(properties).length > 0) {\n    updates.push({\n      contactId: contact.id,\n      properties,\n    });\n  }\n}\n\nreturn updates.map(u => ({ json: u }));"
      },
      "id": "map-updates",
      "name": "Map Updates",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1100, 0]
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "loop-updates",
      "name": "Loop Updates",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [1320, 0]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.contactId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"properties\": {{ JSON.stringify($json.properties) }} }",
        "options": {}
      },
      "id": "update-contact",
      "name": "Update Contact",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1540, 0],
      "credentials": {
        "hubspotApi": {
          "id": "credential-id",
          "name": "HubSpot API"
        }
      }
    },
    {
      "parameters": {
        "amount": 0.5,
        "unit": "seconds"
      },
      "id": "wait-rate-limit",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [1100, 200]
    }
  ],
  "connections": {
    "Weekly Schedule": {
      "main": [[{ "node": "Fetch Contacts Missing Fields", "type": "main", "index": 0 }]]
    },
    "Fetch Contacts Missing Fields": {
      "main": [[{ "node": "Split In Batches", "type": "main", "index": 0 }]]
    },
    "Split In Batches": {
      "main": [
        [{ "node": "Format Batch", "type": "main", "index": 0 }],
        []
      ]
    },
    "Format Batch": {
      "main": [[{ "node": "Apollo Bulk Match", "type": "main", "index": 0 }]]
    },
    "Apollo Bulk Match": {
      "main": [[{ "node": "Map Updates", "type": "main", "index": 0 }]]
    },
    "Map Updates": {
      "main": [[{ "node": "Loop Updates", "type": "main", "index": 0 }]]
    },
    "Loop Updates": {
      "main": [
        [{ "node": "Update Contact", "type": "main", "index": 0 }],
        [{ "node": "Wait", "type": "main", "index": 0 }]
      ]
    },
    "Update Contact": {
      "main": [[{ "node": "Loop Updates", "type": "main", "index": 0 }]]
    },
    "Wait": {
      "main": [[{ "node": "Split In Batches", "type": "main", "index": 0 }]]
    }
  },
  "pinData": {},
  "settings": { "executionOrder": "v1" },
  "staticData": null,
  "tags": [],
  "triggerCount": 0,
  "active": false,
  "meta": { "instanceId": "", "templateId": "revi-batch-enrich" }
}
