This workflow corresponds to n8n.io template #9855 — we link there as the canonical source.
This workflow follows the Google Drive → 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": "RWghkZDBJi9mm0Bq",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "[Template] Building Relationships (param)",
"tags": [],
"nodes": [
{
"id": "55136a9d-4d1e-455d-8b26-f449c28da2bb",
"name": "trigger: form submission",
"type": "n8n-nodes-base.googleSheetsTrigger",
"position": [
-336,
0
],
"parameters": {
"event": "rowAdded",
"options": {},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"sheetName": "={{ $env.SHEETS_TAB || 'crm' }}",
"documentId": "={{ $env.SHEETS_ID }}"
},
"typeVersion": 1
},
{
"id": "9e262787-c907-4bf7-b8e4-2867ffe2946f",
"name": "get screenshot",
"type": "n8n-nodes-base.googleDrive",
"position": [
-128,
0
],
"parameters": {
"fileId": "={{ (function () { const col = $env.COL_DRIVE_URL || 'Upload a screenshot or image of the person'; const s = $json[col] || ''; const m = s.match(/(?:\\/file\\/d\\/|\\/d\\/|open\\?id=|uc\\?id=|thumbnail\\?id=|id=)([-\\w]{10,})/); return m ? m[1] : ''; })() }}",
"options": {
"binaryProperty": "data"
},
"operation": "download"
},
"typeVersion": 3
},
{
"id": "6219058a-3a24-48d7-b2a9-cf861a6ae74b",
"name": "Analyze image",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
112,
0
],
"parameters": {
"text": "You extract structured data from images of profiles, email signatures, business cards, event badges, resumes, or screenshots.\n\nRules:\n- OUTPUT STRICT JSON ONLY. No markdown, no prose, no comments.\n- Return exactly these 5 keys; if a field isn\u2019t clearly present, use null.\n- Prefer the primary/most prominent person if multiple appear.\n- Normalize whitespace and title-case personal names; keep original casing for company/university.\n- Keep description to 1\u20132 sentences summarizing what\u2019s visible (role, focus, notable lines). Do not invent info.\n\nReturn exactly:\n{\n \"name\": string|null,\n \"job_title\": string|null,\n \"company\": string|null,\n \"university\": string|null,\n \"description\": string|null\n}",
"modelId": "={{ $env.OPENAI_VISION_MODEL || 'chatgpt-4o-latest' }}",
"options": {},
"resource": "image",
"inputType": "binary",
"operation": "analyze",
"binaryPropertyName": "data"
},
"typeVersion": 1.8
},
{
"id": "1fc377bc-18a7-4fe1-897f-4a0a8fcb9bf1",
"name": "structure info",
"type": "n8n-nodes-base.code",
"position": [
352,
0
],
"parameters": {
"jsCode": "// n8n Code node (JavaScript). Mode: \"Run Once for Each Item\"\nfunction titleCaseName(s) {\n if (!s || typeof s !== 'string') return s;\n return s\n .trim()\n .split(/\\s+/)\n .map(w => w[0] ? (w[0].toUpperCase() + w.slice(1).toLowerCase()) : w)\n .join(' ');\n}\n\nfunction clean(v) {\n if (v === undefined || v === null) return null;\n const s = String(v).trim();\n return s === '' ? null : s;\n}\n\nconst out = [];\n\nfor (const item of items) {\n // 1) Get raw model output (handles various OpenAI node shapes)\n let raw =\n item.json?.text ?? // common \"Simplify Output\" field\n item.json?.content ?? // some OpenAI nodes\n item.json?.response ?? // alt\n item.json?.choices?.[0]?.message?.content ?? // raw OpenAI\n item.json; // last resort\n\n // 2) Parse to object (strip code fences, smart quotes, double-parse fallback)\n let obj = raw;\n if (typeof raw === 'string') {\n let s = raw.trim().replace(/^```(?:json)?\\s*|\\s*```$/g, '');\n s = s.replace(/[\\u201C\\u201D]/g, '\"').replace(/[\\u2018\\u2019]/g, \"'\");\n try { obj = JSON.parse(s); }\n catch {\n obj = JSON.parse(JSON.parse(s)); // handles quoted-JSON-in-a-string\n }\n }\n\n // 3) Accept common alias keys from the model, then normalize\n const aliases = {\n name: ['name', 'full_name', 'person', 'candidate'],\n job_title: ['job_title', 'title', 'role', 'position', 'headline'],\n company: ['company', 'org', 'employer', 'organization'],\n location: ['location', 'city', 'based_in', 'place'],\n university: ['university', 'school', 'alma_mater', 'education'],\n description: ['description', 'summary', 'about', 'bio']\n };\n\n const pick = (o, keys) => {\n for (const k of keys) if (o && o[k] != null) return o[k];\n return null;\n };\n\n const result = {\n name: titleCaseName(clean(pick(obj, aliases.name))),\n job_title: clean(pick(obj, aliases.job_title)),\n company: clean(pick(obj, aliases.company)),\n location: clean(pick(obj, aliases.location)),\n university: clean(pick(obj, aliases.university)),\n description: clean(pick(obj, aliases.description))\n };\n\n // 4) Optional: trim description length\n if (result.description && result.description.length > 300) {\n result.description = result.description.slice(0, 297) + '...';\n }\n\n out.push({ json: result });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "5346ea2f-b3e0-4056-9026-30dc3ad5320f",
"name": "update crm",
"type": "n8n-nodes-base.googleSheets",
"position": [
560,
0
],
"parameters": {
"columns": {
"value": {
"company": "={{ $json.company }}",
"location": "={{ $json.location }}",
"full name": "={{ $json.name }}",
"job title": "={{ $json.job_title }}",
"university": "={{ $json.university }}",
"description": "={{ $json.description }}",
"Upload a screenshot or image of the person": "={{ $('get screenshot').item.json[$env.COL_DRIVE_URL || 'Upload a screenshot or image of the person'] }}"
},
"schema": [
{
"id": "Timestamp",
"type": "string",
"display": true,
"removed": true,
"displayName": "Timestamp",
"canBeUsedToMatch": true
},
{
"id": "Upload a screenshot or image of the person",
"type": "string",
"display": true,
"displayName": "Upload a screenshot or image of the person",
"canBeUsedToMatch": true
},
{
"id": "Quick notes about the person",
"type": "string",
"display": true,
"removed": true,
"displayName": "Quick notes about the person",
"canBeUsedToMatch": true
},
{
"id": "full name",
"type": "string",
"display": true,
"displayName": "full name",
"canBeUsedToMatch": true
},
{
"id": "company",
"type": "string",
"display": true,
"displayName": "company",
"canBeUsedToMatch": true
},
{
"id": "job title",
"type": "string",
"display": true,
"displayName": "job title",
"canBeUsedToMatch": true
},
{
"id": "location",
"type": "string",
"display": true,
"displayName": "location",
"canBeUsedToMatch": true
},
{
"id": "description",
"type": "string",
"display": true,
"displayName": "description",
"canBeUsedToMatch": true
},
{
"id": "university",
"type": "string",
"display": true,
"displayName": "university",
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Upload a screenshot or image of the person"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": "={{ $env.SHEETS_TAB || 'crm' }}",
"documentId": "={{ $env.SHEETS_ID }}"
},
"typeVersion": 4.7
},
{
"id": "b410e112-ca36-4d17-96c8-777fbea7d3c0",
"name": "Message a model",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
768,
0
],
"parameters": {
"modelId": "={{ $env.OPENAI_TEXT_MODEL || 'gpt-4o-mini' }}",
"options": {
"temperature": 0.2
},
"messages": {
"values": [
{
"content": "=NOTES (must use): {{ $('trigger: form submission').item.json[$env.COL_NOTES || 'Quick notes about the person'] }}\n\nFIELDS (use only if present; never guess):\nname: {{ $json.name || $json[\"full name\"] }}\ntitle: {{ $json.job_title || $json[\"job title\"] }}\ncompany: {{ $json.company }}\nlocation: {{ $json.location }}\nuniversity: {{ $json.university }}\ndescription: {{ $json.description }}\n\nTASK\nWrite ONE LinkedIn DM that:\n- Cites exactly ONE concrete detail from NOTES.\n- Optionally weaves in title/company/location if present.\n- Makes ONE light ask.\n- Ends with a question.\nPlain text only, \u2264450 chars, no added facts beyond NOTES/FIELDS."
},
{
"role": "system",
"content": "You write short LinkedIn follow-ups grounded in structured fields.\n\nGROUNDING & SAFETY\n- ALWAYS anchor on NOTES. If a detail is not in NOTES or the provided fields, OMIT it.\n- NEVER invent, infer, or generalize.\n- If a field is empty or unclear, ignore it\u2014don\u2019t guess.\n- Keep claims strictly to what\u2019s provided.\n\nSTYLE\n- Plain text only (no emojis, bullets, or brackets). \u2264 450 characters.\n- Friendly, crisp, professional. Reference ONE concrete detail from NOTES.\n- Suggest ONE light next step (share resource / quick chat / intro).\n- End with ONE question.\n\nCONTENT RULES\n- If name is available, greet by first name only.\n- Use title/company/location only if present and useful.\n- Do not mention university unless present and relevant."
}
]
}
},
"typeVersion": 1.8
},
{
"id": "2c060fb9-5a0d-45a4-8b72-623c2d64be88",
"name": "update crm with message",
"type": "n8n-nodes-base.googleSheets",
"position": [
1120,
0
],
"parameters": {
"columns": {
"value": {
"follow-up message": "={{ $json.message?.content || $json.text || $json.content }}",
"Upload a screenshot or image of the person": "={{ $('get screenshot').item.json[$env.COL_DRIVE_URL || 'Upload a screenshot or image of the person'] }}"
},
"schema": [
{
"id": "Timestamp",
"type": "string",
"display": true,
"removed": true,
"displayName": "Timestamp",
"canBeUsedToMatch": true
},
{
"id": "Upload a screenshot or image of the person",
"type": "string",
"display": true,
"displayName": "Upload a screenshot or image of the person",
"canBeUsedToMatch": true
},
{
"id": "Quick notes about the person",
"type": "string",
"display": true,
"removed": true,
"displayName": "Quick notes about the person",
"canBeUsedToMatch": true
},
{
"id": "full name",
"type": "string",
"display": true,
"removed": true,
"displayName": "full name",
"canBeUsedToMatch": true
},
{
"id": "company",
"type": "string",
"display": true,
"removed": true,
"displayName": "company",
"canBeUsedToMatch": true
},
{
"id": "job title",
"type": "string",
"display": true,
"removed": true,
"displayName": "job title",
"displayNameSrc": "job title",
"canBeUsedToMatch": true
},
{
"id": "location",
"type": "string",
"display": true,
"removed": true,
"displayName": "location",
"canBeUsedToMatch": true
},
{
"id": "description",
"type": "string",
"display": true,
"removed": true,
"displayName": "description",
"canBeUsedToMatch": true
},
{
"id": "university",
"type": "string",
"display": true,
"removed": true,
"displayName": "university",
"canBeUsedToMatch": true
},
{
"id": "follow-up message",
"type": "string",
"display": true,
"displayName": "follow-up message",
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Upload a screenshot or image of the person"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": "={{ $env.SHEETS_TAB || 'crm' }}",
"documentId": "={{ $env.SHEETS_ID }}"
},
"typeVersion": 4.7
},
{
"id": "15d58a4e-6c00-4e4a-afda-09f82722eb30",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-368,
-208
],
"parameters": {
"width": 192,
"content": "Trigger:\nWhen a 2-field only Google Form is submitted with a LinkedIn header screenshot + quick personal notes. "
},
"typeVersion": 1
},
{
"id": "9f91086d-210b-4fc8-89e4-415375dafb37",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-128,
-208
],
"parameters": {
"width": 150,
"content": "This step gets the new screenshot to parse information"
},
"typeVersion": 1
},
{
"id": "c24d446a-3d2d-4639-99f8-05ac599900ed",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
80,
-208
],
"parameters": {
"width": 150,
"content": "Open AI's vision model analyses the screenshot to extract info"
},
"typeVersion": 1
},
{
"id": "5102f336-add5-4992-a9fa-2542021f7558",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
304,
-208
],
"parameters": {
"width": 150,
"content": "Extracted information is structured "
},
"typeVersion": 1
},
{
"id": "d54eb503-b086-431c-b483-fa9c67701c88",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
528,
-208
],
"parameters": {
"width": 150,
"content": "Google sheet with form submission info is updated with added fields"
},
"typeVersion": 1
},
{
"id": "6a945f3a-6926-4f6d-9d3d-d9415af4355e",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
784,
-208
],
"parameters": {
"width": 150,
"content": "With all this info AND your original notes, Open AI generates a message"
},
"typeVersion": 1
},
{
"id": "dbd68a25-b501-4f06-a1ea-dc746fced6f3",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1056,
-208
],
"parameters": {
"width": 150,
"content": "crm updated with message"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"executionOrder": "v1"
},
"versionId": "5bc78610-e614-4340-8703-a7e7bcf2d274",
"connections": {
"update crm": {
"main": [
[
{
"node": "Message a model",
"type": "main",
"index": 0
}
]
]
},
"Analyze image": {
"main": [
[
{
"node": "structure info",
"type": "main",
"index": 0
}
]
]
},
"get screenshot": {
"main": [
[
{
"node": "Analyze image",
"type": "main",
"index": 0
}
]
]
},
"structure info": {
"main": [
[
{
"node": "update crm",
"type": "main",
"index": 0
}
]
]
},
"Message a model": {
"main": [
[
{
"node": "update crm with message",
"type": "main",
"index": 0
}
]
]
},
"trigger: form submission": {
"main": [
[
{
"node": "get screenshot",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
The problem Ever attend a networking event and find yourself taking screenshots of people's LinkedIn? Sounds counter-intuitive because you are connecting on LinkedIn. But you find it hard to keep track of everyone you've met. You also don't want to miss diligently updating your…
Source: https://n8n.io/workflows/9855/ — 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.
This workflow is perfect for eCommerce teams, market researchers, and product analysts who want to track or extract product information from websites that restrict scraping tools. It’s also useful for
Use n8n to extract medical test data from diagnostic reports uploaded to Google Drive, automatically detect abnormal values, and generate personalized health advice. Upload a medical report (PDF or im
Transform your receipt management with this comprehensive n8n workflow that automatically processes receipts through Telegram, extracts transaction data using AI, and stores it across multiple platfor
This advanced n8n workflow automates the full lead enrichment, qualification, and personalized outreach process tailored specifically for the B2B real estate sector. Integrating top platforms like Api
This automation is designed to help you generate AI-powered music tracks, cover art, and fully rendered music videos — all triggered from a simple Telegram chat and managed via Google Sheets.