This workflow corresponds to n8n.io template #17253 — we link there as the canonical source.
This workflow follows the Googlegemini → Google Sheets 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": "dGcVksFiCNnhilvg",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Lead Categorization",
"tags": [],
"nodes": [
{
"id": "02d83b8f-0f24-46c1-8de8-bfc91cebcb71",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-1056,
192
],
"parameters": {},
"typeVersion": 1
},
{
"id": "b50cdb7c-f355-4ef1-a44c-70ae224e0053",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-880,
-48
],
"parameters": {
"color": "#FFFFFF",
"width": 800,
"height": 400,
"content": "Section 1: Get Contacts\nRead contacts with a Job Title and no existing Function or Seniority.\n-Configuration is provided for variable name changes."
},
"typeVersion": 1
},
{
"id": "810bd547-6044-4745-87a3-099e2034a5bb",
"name": "Read Data ",
"type": "n8n-nodes-base.googleSheets",
"position": [
-656,
192
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "id",
"value": "{{ $('Global Settings').first().json[\"Sheet ID\"] }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "{{ $('Global Settings').first().json[\"Google Document ID\"] }}"
}
},
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "777ac03f-36f5-4202-8937-45c69ccf0c97",
"name": "Normalise Company Name and Job Title",
"type": "n8n-nodes-base.code",
"position": [
208,
112
],
"parameters": {
"jsCode": "for (const item of $input.all()) {\n\n let title = item.json[\"Job Title\"] || \"\";\n let company = item.json[\"Company Name\"] || \"\";\n title = title.trim().replace(/\\s+/g, \" \").toLowerCase();\n item.json.normalizedTitle = title;\n company = company.trim().replace(/\\s+/g, \" \").toLowerCase();\n item.json.normalizedCompanyName = company;\n}\nreturn $input.all();"
},
"typeVersion": 2
},
{
"id": "a012eed2-ea53-4040-aaa7-21a93ba9376c",
"name": "Remove Duplicates",
"type": "n8n-nodes-base.code",
"position": [
416,
112
],
"parameters": {
"jsCode": "const seen = new Map(); \n\nfor (const item of $input.all()) {\n const key = `${item.json.normalizedTitle}|${item.json.normalizedCompanyName}`;\n const id = item.json[\"#\"]; \n\n if (!seen.has(key)) {\n seen.set(key, {\n 'Job Title': item.json[\"Job Title\"],\n 'Company Name': item.json[\"Company Name\"],\n normalizedTitle: item.json.normalizedTitle,\n normalizedCompanyName: item.json.normalizedCompanyName,\n rowIds: [id]\n });\n } else {\n \n seen.get(key).rowIds.push(id);\n }\n}\n\nreturn Array.from(seen.values()).map(data => ({ json: data }));"
},
"typeVersion": 2
},
{
"id": "062bb951-faf3-4b2c-8d15-e6a73d0a2042",
"name": "Extract Output",
"type": "n8n-nodes-base.code",
"position": [
1520,
112
],
"parameters": {
"jsCode": "const items = $input.all();\nconst finalItems = [];\n\nfor (const item of items) {\n const text = item.json.content.parts[0].text;\n let contacts = [];\n\n try {\n contacts = JSON.parse(text);\n } catch (e) {\n // Handle JSON parsing errors if needed\n }\n\n // Flatten the array so every contact becomes its own n8n item\n for (const contact of contacts) {\n finalItems.push({\n json: {\n \"#\": contact[\"#\"], // Maps the AI's \"id\" field straight to your row column\n \"Function\": contact.function || \"Unknown\",\n \"Seniority\": contact.seniority || \"Unknown\",\n \"Confidence Score\": contact.confidence || 0.00\n }\n });\n }\n}\n\nreturn finalItems;"
},
"typeVersion": 2
},
{
"id": "f11282ed-3846-4158-9a2d-43036b4c06bc",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-48,
-48
],
"parameters": {
"color": "#FFFFFF",
"width": 1392,
"height": 400,
"content": "Section 2: AI Classification\nNormalize and deduplicate contacts before sending unique Job Title and Company pairs to Gemini in batches, reducing API calls and token consumption.\nLoop over and wait is used to make sure rate limit does not exceeds."
},
"typeVersion": 1
},
{
"id": "ebe3f8d7-4fd9-4bff-a638-c9fae22fbf07",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1392,
-48
],
"parameters": {
"color": "#FFFFFF",
"width": 688,
"height": 400,
"content": "Section 3: Update Google Sheet\nCombine the AI classifications with the original contact data, then update the Google Sheet with the Function, Seniority, and Confidence Score."
},
"typeVersion": 1
},
{
"id": "4af7a26e-72c1-4301-802c-27aaeeaf876e",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2032,
-208
],
"parameters": {
"color": "#FFFF00",
"width": 624,
"height": 1216,
"content": "# Classify lead function and seniority in Google Sheets with Google Gemini\n\nThis workflow reads contacts from Google Sheets, uses Google Gemini to classify each contact\u2019s job function and seniority from their job title and company name, and writes the Function, Seniority, and Confidence Score back to the same sheet.\n\n## Required Columns\n1. \\#\n2. Job Title\n3. Company Name (recommended)\n4. Function\n5. Seniority\n6. Confidence Score\n\n## How It Works\n1. Starts manually when you click Execute workflow.\n2. Reads rows from a specified Google Sheets worksheet and maps your column headers to the workflow\u2019s expected fields.\n3. Keeps only contacts that have a Job Title and no existing Seniority value.\n4. Normalizes job titles and company names, deduplicates identical Job Title + Company pairs, and batches the unique combinations to save LLM tokens.\n5. Waits between batches to respect rate limits, then sends each batch to Google Gemini to return Function, Seniority, and a confidence score for every original row ID.\n6. Parses Gemini\u2019s JSON response and updates the matching rows in Google Sheets with Function, Seniority, and Confidence Score.\n\n## Setup\n1. Add Google Sheets OAuth credentials and set the Google Document ID and Sheet ID in the Global Settings values.\n2. Add a Google Gemini (PaLM) API credential for the Google Gemini node.\n3. Ensure your sheet has columns for #, Job Title, Company Name (recommended), Function, Seniority, and Confidence Score, and update the column name settings if your headers differ.\n4. Adjust Batch Size and Wait Time in Global Settings to control Gemini batching and throttling before activating the workflow.\n\n\n## Customization\n1. Update column names in the **Global Settings** node if your sheet uses different headers.\n2. Change the **Batch Size** in the **Global Settings** node to control how many contacts are sent to Gemini at once.\n3. Change the **Wait Time** in the **Global Settings** node to control how many seconds the workflow will wait before hitting the Gemini API again."
},
"typeVersion": 1
},
{
"id": "ef3f35cd-baf1-4f1e-94b9-b3db0640e80b",
"name": "Update Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
1792,
192
],
"parameters": {
"columns": {
"value": {
"#": "={{ $json[\"#\"] }}",
"Function": "={{ $json.Function }}",
"Seniority": "={{ $json.Seniority }}",
"Confidence Score": "={{ $json[\"Confidence Score\"] }}"
},
"schema": [
{
"id": "#",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "#",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Name",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Job Title",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Job Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Company Name",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Company Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Seniority",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Seniority",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Function",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Function",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Confidence Score",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Confidence Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "number",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"#"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "id",
"value": "={{ $('Global Settings').first().json[\"Sheet ID\"] }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Global Settings').first().json[\"Google Document ID\"] }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "5c4a356c-50d3-405e-a28f-531fdc7fdb94",
"name": "Configuration",
"type": "n8n-nodes-base.code",
"position": [
-480,
192
],
"parameters": {
"jsCode": "const settings = $('Global Settings').first().json;\n\nconst SERIAL_NUMBER_COLUMN = settings[\"Serial Number Column Name\"] || \"#\";\nconst JOB_TITLE_COLUMN = settings[\"Job Title Column Name\"] || \"Job Title\";\nconst COMPANY_NAME_COLUMN = settings[\"Company Name Column Name\"] || \"Company Name\";\nconst FUNCTION_COLUMN = settings[\"Function Column Name\"] || \"Function\";\nconst SENIORITY_COLUMN = settings[\"Seniority Column Name\"] || \"Seniority\";\nconst CONFIDENCE_COLUMN = settings[\"Confidence Score Column Name\"] || \"Confidence Score\"; \n\n\nreturn $input.all().map(item => ({\n json: {\n ...item.json,\n\n // Standard names used by the workflow\n \"#\": item.json[SERIAL_NUMBER_COLUMN],\n \"Job Title\": item.json[JOB_TITLE_COLUMN],\n \"Company Name\": item.json[COMPANY_NAME_COLUMN],\n \"Function\": item.json[FUNCTION_COLUMN],\n \"Seniority\": item.json[SENIORITY_COLUMN],\n \"Confidence Score\": item.json[CONFIDENCE_COLUMN],\n }\n}));"
},
"typeVersion": 2
},
{
"id": "6864f163-9ed1-4b95-bd7e-2490e2840aa4",
"name": "Filter Data",
"type": "n8n-nodes-base.if",
"position": [
-256,
192
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "20d6f37a-5058-4d96-b67d-9b40b400bad0",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json[\"Job Title\"] }}",
"rightValue": ""
},
{
"id": "459+1234567890a32-bd74-33ededc6d8b0",
"operator": {
"type": "string",
"operation": "empty",
"singleValue": true
},
"leftValue": "={{ $json.Seniority }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "8872a5a8-1361-4d37-8805-08d766b895ec",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-16,
112
],
"parameters": {
"options": {},
"batchSize": "={{ $('Global Settings').first().json[\"Batch Size\"] }}"
},
"typeVersion": 3
},
{
"id": "bcaf524e-0ddb-4694-997c-7349d4928c6c",
"name": "Wait",
"type": "n8n-nodes-base.wait",
"position": [
640,
112
],
"parameters": {
"amount": "={{ $('Global Settings').first().json[\"Wait Time\"] }}"
},
"typeVersion": 1.1
},
{
"id": "49be17b2-ad7c-4d22-9104-07a2843d6dc4",
"name": "Gemini Payload",
"type": "n8n-nodes-base.code",
"position": [
864,
112
],
"parameters": {
"jsCode": "const contacts = $input.all().map(item => ({\n \"#\": item.json.rowIds,\n \"Job Title\": item.json[\"Job Title\"],\n \"Company Name\": item.json[\"Company Name\"],\n}));\n\nreturn [\n {\n json: {\n contacts\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "1042d731-2d6a-41e6-b643-ef9d480aa07d",
"name": "Categorization (Google Gemini)",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
1088,
112
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "",
"cachedResultUrl": "",
"cachedResultName": ""
},
"options": {},
"messages": {
"values": [
{
"content": "=You are a lead enrichment assistant.\n\nYour task is to classify multiple contacts based only on the information provided.\n\nRules:\n- Use only the Job Title and Company provided.\n- Do not make assumptions beyond the input.\n- If the information is insufficient, return \"Unknown\".\n- The \"#\" field in the input contains an array of row IDs. In your output, you MUST generate a separate, individual JSON object for every single ID inside that array.\n- Choose exactly one Function and one Seniority for each contact.\n- Return exactly one output object for every input object.\n- Return ONLY valid JSON.\n- Do not use markdown.\n- Do not include explanations or additional text.\n\nAllowed Function values:\n- Engineering\n- Product\n- Marketing\n- Sales\n- Customer Success\n- Operations\n- Finance\n- Human Resources\n- Legal\n- IT\n- Executive\n- Consulting\n- Education\n- Healthcare\n- Research\n- Other\n- Unknown\n\nAllowed Seniority values:\n- Intern\n- Entry Level\n- Individual Contributor\n- Senior Individual Contributor\n- Lead\n- Manager\n- Senior Manager\n- Director\n- Senior Director\n- Vice President\n- C-Level\n- Founder\n- Owner\n- Partner\n- Other\n- Unknown\n\nContacts:\n\n{{ JSON.stringify($json.contacts) }}\n\nFor every contact, you MUST return these three fields:\n- \"#\"\n- function\n- seniority\n- confidence\n\nAll these fields are mandatory.\n\nConfidence must be a decimal number between 0.00 and 1.00.\nUse two decimal places where appropriate.\n\nDo not omit confidence under any circumstance.\n\nReturn exactly this JSON array:\n\n[\n {\n \"#\": \"1\",\n \"function\": \"Engineering\",\n \"seniority\": \"Individual Contributor\",\n \"confidence\": 0.98\n }\n]"
}
]
},
"builtInTools": {}
},
"typeVersion": 1.2
},
{
"id": "54774fc9-3236-4695-a053-1480e64b63ba",
"name": "Global Settings",
"type": "n8n-nodes-base.set",
"position": [
-848,
192
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "ea85e7bc-5884-4c9f-a13a-8ac1a87d13da",
"name": "Google Document ID",
"type": "string",
"value": ""
},
{
"id": "7e2b821e-e2ee-43c4-ab01-94678dd1abea",
"name": "Sheet ID",
"type": "string",
"value": ""
},
{
"id": "6b8d8929-cdd9-49b8-aed9-7660bd6afd19",
"name": "Batch Size",
"type": "number",
"value": 20
},
{
"id": "12dcceab-4d03-4bcb-9a6c-8904ff11dcd6",
"name": "Wait Time",
"type": "number",
"value": 10
},
{
"id": "92844569-67aa-45a4-abf8-aae715b7228c",
"name": "Serial Number Column Name",
"type": "string",
"value": "#"
},
{
"id": "3ff91952-c554-44f2-bc27-265d2e82cef7",
"name": "Job Title Column Name",
"type": "string",
"value": "Job Title"
},
{
"id": "ab840e6f-d561-4d2c-a233-65122c7630aa",
"name": "Company Name Column Name",
"type": "string",
"value": "Company Name"
},
{
"id": "3a3955d4-25e2-45d6-985a-059e0d30adac",
"name": "Function Column Name",
"type": "string",
"value": "Function"
},
{
"id": "5d8c20b5-3bfd-4009-a113-df6497e79747",
"name": "Seniority Column Name",
"type": "string",
"value": "Seniority"
},
{
"id": "8e51d95b-08b9-4e6c-b552-1b507ca82fdb",
"name": "Confidence Score Column Name",
"type": "string",
"value": "Confidence Score"
}
]
}
},
"typeVersion": 3.4
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "ecc9bcdf-9202-4853-95ee-bb74be8b56d4",
"nodeGroups": [],
"connections": {
"Wait": {
"main": [
[
{
"node": "Gemini Payload",
"type": "main",
"index": 0
},
{
"node": "Read Data ",
"type": "main",
"index": 0
}
]
]
},
"Read Data ": {
"main": [
[
{
"node": "Configuration",
"type": "main",
"index": 0
}
]
]
},
"Filter Data": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Update Sheet": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Configuration": {
"main": [
[
{
"node": "Filter Data",
"type": "main",
"index": 0
}
]
]
},
"Extract Output": {
"main": [
[
{
"node": "Update Sheet",
"type": "main",
"index": 0
}
]
]
},
"Gemini Payload": {
"main": [
[
{
"node": "Categorization (Google Gemini)",
"type": "main",
"index": 0
}
]
]
},
"Global Settings": {
"main": [
[
{
"node": "Read Data ",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[],
[
{
"node": "Normalise Company Name and Job Title",
"type": "main",
"index": 0
}
]
]
},
"Remove Duplicates": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"Categorization (Google Gemini)": {
"main": [
[
{
"node": "Extract Output",
"type": "main",
"index": 0
}
]
]
},
"Normalise Company Name and Job Title": {
"main": [
[
{
"node": "Remove Duplicates",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Global Settings",
"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.
googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow reads contacts from Google Sheets, uses Google Gemini to classify each contact’s job function and seniority from their job title and company name, and writes the Function, Seniority, and Confidence Score back to the same sheet. Starts manually when you click…
Source: https://n8n.io/workflows/17253/ — 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.
Upload a job posting (PDF, image, or screenshot) and instantly get a tailored CV section and a matching cover letter – both written in the posting's language and tone, both honest about what you actua
This workflow accepts invoice PDFs or images from an n8n Form or Telegram, uses Google Gemini to OCR and extract structured invoice fields and line items, appends the results to Google Sheets, and sen
This workflow monitors Gmail for new unread emails, uses Google Gemini to classify each email’s intent, then creates Gmail draft replies and Slack alerts for questions/requests, escalates complaints w
This workflow monitors a Google Drive folder for newly uploaded invoice files, extracts text with OCR.Space, uses Google Gemini to convert the OCR text into structured invoice fields, validates requir
This workflow polls Gmail for labeled outreach replies, cleans and classifies each reply with Google Gemini, logs results to Google Sheets, and for actionable messages, creates a Gmail draft response