This workflow follows the Google Sheets → HTTP Request 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 →
{
"id": "qualify-scraped-leads-ai-sheets",
"name": "Qualify scraped leads with OpenAI and log them to Google Sheets",
"active": false,
"nodes": [
{
"parameters": {
"content": "## Qualify scraped leads with OpenAI and log them to Google Sheets\n\n### Who's it for\nSales teams, agencies and freelancers who already pull leads from somewhere and want them\nscored before a human looks at them. If you spend the first hour of the day deciding which\nnew leads are worth contacting, this does that part for you.\n\n### What it does\nEvery morning it pulls a fresh batch of leads from an Apify actor you configure, normalises the fields,\nasks an AI to score each lead from 1-10 against your own ideal-customer profile with a short\nreason, and appends every result to a Google Sheet. Low scores are logged too, so you can\ntune the profile instead of guessing.\n\n### How to set up\n1. Create two Header Auth credentials and select them on the two HTTP nodes.\n2. Open \"Your settings (EDIT ME)\" and describe your ideal customer in plain language.\n Be specific; vague profiles produce vague scores.\n3. In \"Fetch fresh leads\", replace YOUR-USERNAME~YOUR-ACTOR with your actor ID and\n replace the {} body with that actor's own input parameters.\n4. Connect Google Sheets and point it at a sheet with the header row listed below.\n\n### Requirements\nAn n8n instance, an Apify account for the lead source, an OpenAI-compatible API key, and a\nGoogle account. A run costs roughly a cent or two at the default settings.\n\n### How to customize\nSwap the actor and its input parameters together to change lead source. Edit the system prompt in the AI node to change how\nstrictly leads are judged. Raise or lower maxLeadsPerRun to control cost per run. The AI\nnode is a plain HTTP request, so it works with any OpenAI-compatible endpoint.\n\nSheet header row (exact):\nprocessedAt leadType state businessName ownerName address city county zip eventDate\nsourceUrl score reason scoreThreshold",
"height": 720,
"width": 520
},
"id": "overview-sticky",
"name": "Sticky Note \u2014 Read me first",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-760,
-40
]
},
{
"parameters": {
"content": "## 1. Credentials (2 required)\n\n**Apify token (Header Auth):** Credentials -> New -> Header Auth.\nName: `Authorization` \u2014 Value: `Bearer apify_api_YOUR_TOKEN`\n\n**OpenAI (Header Auth):** Credentials -> New -> Header Auth.\nName: `Authorization` \u2014 Value: `Bearer sk-YOUR_KEY`\n\nThen open the two HTTP nodes and select these credentials.\nAlso connect **Google Sheets** (OAuth) on its node.",
"width": 340,
"height": 280,
"color": 7
},
"id": "sticky-credentials",
"name": "Sticky Note \u2014 Credentials",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1180,
-420
]
},
{
"parameters": {
"content": "## 2. Your settings\n\nOpen **\"Your settings (EDIT ME)\"** and describe:\n- your ideal customer profile, in plain words\n- max leads per run (this is your cost control)\n\nBe specific about the ICP. \"Independent restaurants opening in Texas metros, not chains\"\nproduces far better scores than \"restaurants\".\n\nThe AI scores every lead 1-10 against YOUR words here and writes a one-line reason.",
"width": 320,
"height": 240,
"color": 7
},
"id": "sticky-settings",
"name": "Sticky Note \u2014 Settings",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-820,
-420
]
},
{
"parameters": {
"content": "## \u2462 Lead source (configure before running)\n\nThe URL and `{}` body are placeholders. Replace `YOUR-USERNAME~YOUR-ACTOR` with your actor ID, then replace `{}` with that actor's own input parameters.\n\n**Optional worked example \u2014 Rook liquor-license leads:**\nURL actor ID: `rook-data-tools~new-liquor-license-leads`\nBody: `{\"states\":[\"TX\",\"CA\"],\"leadTypes\":[\"pending_application\"],\"sinceDays\":2,\"onlyNewSinceLastRun\":true,\"maxResults\":100}`\n\nThis example input belongs to that actor only; using another actor requires its own input schema.",
"width": 340,
"height": 260,
"color": 7
},
"id": "sticky-source",
"name": "Sticky Note \u2014 Lead source",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-460,
-420
]
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "days",
"triggerAtHour": 8
}
]
}
},
"id": "schedule-trigger-001",
"name": "Every morning at 8",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-1120,
-80
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "set-icp",
"name": "icpDescription",
"type": "string",
"value": "EDIT ME: e.g. Independent restaurants and bars opening soon in Texas metro areas \u2014 decision-maker reachable, not a chain, not a temporary event permit."
},
{
"id": "set-offer",
"name": "offerDescription",
"type": "string",
"value": "EDIT ME: e.g. We sell point-of-sale systems for new restaurants \u2014 free setup before opening day."
},
{
"id": "set-threshold",
"name": "scoreThreshold",
"type": "number",
"value": 7
},
{
"id": "set-maxleads",
"name": "maxLeadsPerRun",
"type": "number",
"value": 25
}
]
}
},
"id": "settings-node-001",
"name": "Your settings (EDIT ME)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-900,
-80
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.apify.com/v2/acts/YOUR-USERNAME~YOUR-ACTOR/run-sync-get-dataset-items",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{}",
"options": {
"timeout": 300000
}
},
"id": "http-apify-001",
"name": "Fetch fresh leads (Apify)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-660,
-80
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "// Normalize any actor's output into one lead shape, attach settings, dedupe, cap.\nconst settings = $('Your settings (EDIT ME)').first().json;\nconst max = Number(settings.maxLeadsPerRun) || 25;\nconst seen = new Set();\nconst out = [];\nfor (const item of $input.all()) {\n const j = item.json ?? {};\n if (j.error) continue; // upstream failure item \u2014 skip gracefully\n const lead = {\n leadType: j.leadType ?? j.type ?? 'lead',\n state: j.state ?? j.region ?? '',\n businessName: j.businessName ?? j.name ?? j.title ?? j.company ?? '',\n ownerName: j.ownerName ?? j.owner ?? j.contactName ?? '',\n address: j.address ?? j.street ?? '',\n city: j.city ?? '',\n county: j.county ?? '',\n zip: j.zip ?? j.postalCode ?? '',\n eventDate: j.eventDate ?? j.date ?? '',\n sourceUrl: j.sourceUrl ?? j.url ?? '',\n };\n const key = [lead.businessName, lead.ownerName, lead.address].join('|').toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n // compact text the AI reads \u2014 includes any extra fields the actor provides\n lead.leadText = JSON.stringify({ ...lead, extra: undefined, licenseType: j.licenseTypeDescription ?? j.licenseType ?? undefined, status: j.status ?? undefined });\n lead.icpDescription = settings.icpDescription;\n lead.offerDescription = settings.offerDescription;\n lead.scoreThreshold = Number(settings.scoreThreshold) || 7;\n out.push({ json: lead });\n if (out.length >= max) break;\n}\nif (out.length === 0) {\n return [{ json: { _empty: true, note: 'No new leads this run.' } }];\n}\nreturn out;"
},
"id": "code-normalize-001",
"name": "Normalize & prepare leads",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-420,
-80
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "cond-notempty",
"leftValue": "={{ $json._empty }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "notEquals"
}
}
],
"combinator": "and"
}
},
"id": "if-notempty-001",
"name": "Any leads?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-180,
-80
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({\n model: 'gpt-4o-mini',\n response_format: { type: 'json_object' },\n messages: [\n { role: 'system', content: 'You are a B2B lead-qualification analyst. Score the LEAD against the IDEAL CUSTOMER PROFILE from 1 (useless) to 10 (perfect fit). Be conservative: missing information lowers the score. Respond with strict JSON: {\\\"score\\\": <int 1-10>, \\\"reason\\\": \\\"<one sentence>\\\"}' },\n { role: 'user', content: 'IDEAL CUSTOMER PROFILE: ' + $json.icpDescription + '\\n\\nOFFER: ' + $json.offerDescription + '\\n\\nLEAD: ' + $json.leadText }\n ]\n}) }}",
"options": {
"timeout": 60000
}
},
"id": "http-openai-001",
"name": "AI: score this lead",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
60,
-180
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineByPosition",
"options": {}
},
"id": "merge-001",
"name": "Re-attach lead data",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
300,
-80
]
},
{
"parameters": {
"jsCode": "// Parse the model's JSON verdict and produce the final row shape.\nconst rows = [];\nfor (const item of $input.all()) {\n const j = item.json;\n let verdict = { score: 0, reason: 'AI call failed \u2014 inspect execution log' };\n try {\n const content = j.choices?.[0]?.message?.content;\n if (content) verdict = JSON.parse(content);\n } catch (e) { verdict.reason = 'AI returned unparseable output'; }\n rows.push({ json: {\n processedAt: new Date().toISOString(),\n leadType: j.leadType ?? '',\n state: j.state ?? '',\n businessName: j.businessName ?? '',\n ownerName: j.ownerName ?? '',\n address: j.address ?? '',\n city: j.city ?? '',\n county: j.county ?? '',\n zip: j.zip ?? '',\n eventDate: j.eventDate ?? '',\n sourceUrl: j.sourceUrl ?? '',\n score: Number(verdict.score) || 0,\n reason: String(verdict.reason ?? ''),\n scoreThreshold: j.scoreThreshold ?? 7,\n }});\n}\nreturn rows;"
},
"id": "code-parse-001",
"name": "Parse AI verdict",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
540,
-80
]
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"mode": "url",
"value": "PASTE_YOUR_GOOGLE_SHEET_URL_HERE"
},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "Leads"
},
"columns": {
"mappingMode": "autoMapInputData",
"options": {}
},
"options": {}
},
"id": "gsheets-001",
"name": "Log every lead (Google Sheets)",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
780,
-80
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
}
],
"connections": {
"Every morning at 8": {
"main": [
[
{
"node": "Your settings (EDIT ME)",
"type": "main",
"index": 0
}
]
]
},
"Your settings (EDIT ME)": {
"main": [
[
{
"node": "Fetch fresh leads (Apify)",
"type": "main",
"index": 0
}
]
]
},
"Fetch fresh leads (Apify)": {
"main": [
[
{
"node": "Normalize & prepare leads",
"type": "main",
"index": 0
}
]
]
},
"Normalize & prepare leads": {
"main": [
[
{
"node": "Any leads?",
"type": "main",
"index": 0
}
]
]
},
"Any leads?": {
"main": [
[
{
"node": "AI: score this lead",
"type": "main",
"index": 0
},
{
"node": "Re-attach lead data",
"type": "main",
"index": 1
}
],
[]
]
},
"AI: score this lead": {
"main": [
[
{
"node": "Re-attach lead data",
"type": "main",
"index": 0
}
]
]
},
"Re-attach lead data": {
"main": [
[
{
"node": "Parse AI verdict",
"type": "main",
"index": 0
}
]
]
},
"Parse AI verdict": {
"main": [
[
{
"node": "Log every lead (Google Sheets)",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"meta": {
"templateCredsSetupCompleted": false
}
}
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.
googleSheetsOAuth2ApihttpHeaderAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Qualify scraped leads with OpenAI and log them to Google Sheets. Uses httpRequest, googleSheets. Scheduled trigger; 13 nodes.
Source: https://github.com/willowridge1234/n8n-ai-lead-scoring/blob/main/workflow/ai-lead-machine-free.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
AI-Powered Short-Form Video Generator with OpenAI, Flux, Kling, and ElevenLabs and upload to all .... Uses httpRequest, googleDrive, discord, googleSheets. Scheduled trigger; 51 nodes.
Free Support: Setting up and getting the workflow tailord to your needs. One small free adjustment included.
This workflow aims to help you and your team track your expenses with OpenAI It automatically collects your OpenAI organization’s API usage and cost data every few days and saves it to a ready-to-use
Automatically fetch, rank, and summarize the top financial stories from curated RSS feeds each day, then deliver a concise AI-written digest to Telegram and log the run to Google Sheets. Runs on a dai
This workflow runs twice daily to fetch recent Instagram post comments via the Instagram Graph API, uses OpenAI to classify sentiment, saves new positive comments to Google Sheets, and emails an HTML