This workflow corresponds to n8n.io template #15308 — we link there as the canonical source.
This workflow follows the Chainllm → 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": "t_Dz6agrG4J6WawbZrnBI",
"name": "Save business card contacts from LINE photos to Google Sheets with Gemini",
"tags": [
{
"id": "48b33c1ea5874239",
"name": "ai",
"createdAt": "2026-04-30T18:38:11.353194Z",
"updatedAt": "2026-04-30T18:38:11.353194Z"
},
{
"id": "6bc9df06f6994144",
"name": "gemini",
"createdAt": "2026-04-30T18:38:11.353194Z",
"updatedAt": "2026-04-30T18:38:11.353194Z"
},
{
"id": "6b0b350ba85141e8",
"name": "line",
"createdAt": "2026-04-30T18:38:11.353194Z",
"updatedAt": "2026-04-30T18:38:11.353194Z"
},
{
"id": "0cf235bfdd344946",
"name": "google-sheets",
"createdAt": "2026-04-30T18:38:11.353194Z",
"updatedAt": "2026-04-30T18:38:11.353194Z"
},
{
"id": "e54a93fcce52480d",
"name": "slack",
"createdAt": "2026-04-30T18:38:11.353194Z",
"updatedAt": "2026-04-30T18:38:11.353194Z"
},
{
"id": "409f9432b52042ac",
"name": "crm",
"createdAt": "2026-04-30T18:38:11.353194Z",
"updatedAt": "2026-04-30T18:38:11.353194Z"
},
{
"id": "90810047f8cf4564",
"name": "image-recognition",
"createdAt": "2026-04-30T18:38:11.353194Z",
"updatedAt": "2026-04-30T18:38:11.353194Z"
},
{
"id": "f97f51589a474b5f",
"name": "automation",
"createdAt": "2026-04-30T18:38:11.353194Z",
"updatedAt": "2026-04-30T18:38:11.353194Z"
}
],
"nodes": [
{
"id": "e098e221-5122-433e-8bea-1d3172c4609e",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2976,
-32
],
"parameters": {
"width": 480,
"height": 896,
"content": "## Save business card contacts from LINE photos to Google Sheets with Gemini\n\n## How it works\n\n1. The workflow starts with setting up configuration fields and receiving events from LINE via webhook.\n2. It checks if the received message is an image and processes it accordingly.\n3. The image is downloaded and converted to a base64 format for further processing.\n4. The image data is extracted using Gemini, parsed, and checked for duplicates in Google Sheets.\n5. If not a duplicate, the contact data is saved to Google Sheets, and notifications are sent to Slack and LINE.\n6. The workflow ensures communication by responding to the webhook with appropriate messages.\n\n## Setup steps\n\n- [ ] Set the LINE_CHANNEL_ACCESS_TOKEN in the Set Configuration Fields node\n- [ ] Set the GOOGLE_SHEET_ID in the Set Configuration Fields node\n- [ ] Set the SLACK_WEBHOOK_URL in the Set Configuration Fields node\n- [ ] Configure the webhook URL for LINE to trigger Receive LINE Event via Webhook\n\n## Customization\n\nThe range or sheet name in Google Sheets can be customized by modifying the relevant fields in the Google Sheets nodes."
},
"typeVersion": 1
},
{
"id": "c9135341-d238-4ffe-a8b0-bbaeadf77ee4",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2416,
160
],
"parameters": {
"color": 7,
"width": 240,
"height": 320,
"content": "## Configuration setup\n\nSets up the configuration fields for LINE, Google Sheets, and Slack."
},
"typeVersion": 1
},
{
"id": "ff1b9615-62be-4d6e-a90b-cb5488176565",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2144,
192
],
"parameters": {
"color": 7,
"width": 432,
"height": 304,
"content": "## Webhook event handling\n\nHandles incoming LINE events and checks if the message is an image."
},
"typeVersion": 1
},
{
"id": "a8e3147f-c998-4422-96fd-8fdec2a2d9a3",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1680,
64
],
"parameters": {
"color": 7,
"width": 432,
"height": 304,
"content": "## Image download and conversion\n\nDownloads image from LINE and converts it to base64."
},
"typeVersion": 1
},
{
"id": "4b3be8a3-7ba6-449b-b688-2aec50826a25",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1200,
64
],
"parameters": {
"color": 7,
"width": 448,
"height": 304,
"content": "## Card data extraction\n\nExtracts data from the card image using Gemini and processes the data."
},
"typeVersion": 1
},
{
"id": "44c16624-ccbf-4185-9b58-ac4475f4e30a",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-672,
-32
],
"parameters": {
"color": 7,
"width": 928,
"height": 384,
"content": "## Duplicate check and contact saving\n\nChecks for duplicate contacts in Google Sheets and saves new contacts, while notifying via Slack."
},
"typeVersion": 1
},
{
"id": "94d9eb4f-3b42-42e5-a8cf-2f8b73200aa2",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1680,
400
],
"parameters": {
"color": 7,
"width": 2416,
"height": 880,
"content": "## Messaging and webhook response\n\nHandles replies and responses to LINE and webhook, confirming outcomes."
},
"typeVersion": 1
},
{
"id": "30a09302-35f0-4fe8-b8eb-20bacfa43273",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1952,
528
],
"parameters": {
"color": 7,
"width": 240,
"height": 304,
"content": "## Gemini configuration\n\nIndependent configuration setup for Gemini."
},
"typeVersion": 1
},
{
"id": "d489dfb5-09a8-4c5b-8637-667e44446d5e",
"name": "Set Configuration Parameters",
"type": "n8n-nodes-base.set",
"position": [
-2368,
320
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "line-token",
"name": "LINE_CHANNEL_ACCESS_TOKEN",
"type": "string",
"value": ""
},
{
"id": "sheet-id",
"name": "GOOGLE_SHEET_ID",
"type": "string",
"value": ""
},
{
"id": "slack-url",
"name": "SLACK_WEBHOOK_URL",
"type": "string",
"value": ""
}
]
}
},
"typeVersion": 3.4
},
{
"id": "5adf8fa8-bbca-42b9-abb6-a45c8e48b5e2",
"name": "When LINE Event Received",
"type": "n8n-nodes-base.webhook",
"position": [
-2096,
320
],
"parameters": {
"path": "business-card-webhook",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "352d0532-8e5b-486c-aae1-32f590bea59f",
"name": "If Image Message",
"type": "n8n-nodes-base.if",
"position": [
-1856,
320
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "condition-image",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.events[0].message.type }}",
"rightValue": "image"
}
]
}
},
"typeVersion": 2
},
{
"id": "fed09136-8467-4e70-ba97-d8612cfb48b3",
"name": "Fetch Image from LINE API",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1632,
192
],
"parameters": {
"url": "=https://api-data.line.me/v2/bot/message/{{ $json.events[0].message.id }}/content",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Set Configuration Parameters').item.json.LINE_CHANNEL_ACCESS_TOKEN }}"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "80e1a3ff-a69a-4550-8776-22488c554ae5",
"name": "Encode Image as Base64",
"type": "n8n-nodes-base.code",
"position": [
-1392,
192
],
"parameters": {
"jsCode": "const items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const binaryData = item.binary?.data;\n if (!binaryData) {\n throw new Error('No binary data found');\n }\n const base64String = Buffer.from(\n await this.helpers.getBinaryDataBuffer(item.index ?? 0, 'data')\n ).toString('base64');\n results.push({\n json: {\n base64Image: base64String,\n mimeType: binaryData.mimeType || 'image/jpeg',\n replyToken: $('When LINE Event Received').item.json.events[0].replyToken,\n lineUserId: $('When LINE Event Received').item.json.events[0].source.userId\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "cc157e22-1ab3-4573-9e44-4bd3e857a941",
"name": "Setup Gemini for Card Data",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
-1904,
656
],
"parameters": {
"options": {},
"modelName": "models/gemini-1.5-flash"
},
"typeVersion": 1
},
{
"id": "651e4ab0-35f2-4c60-8381-d79e06ff6f82",
"name": "Extract Card Info with Gemini",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
-1152,
192
],
"parameters": {
"text": "=Analyze this business card image (base64: {{ $json.base64Image }}) and extract the contact information.\n\nReturn ONLY valid JSON with no explanation, no markdown formatting, no code fences.\n\n{\"name\": \"\", \"company\": \"\", \"title\": \"\", \"phone\": \"\", \"email\": \"\", \"address\": \"\"}\n\nIf a field is not found on the card, use an empty string.",
"promptType": "define"
},
"typeVersion": 1.4
},
{
"id": "7e7470d5-ccf1-4394-b4b5-e46628a2682a",
"name": "Parse Gemini Output Data",
"type": "n8n-nodes-base.code",
"position": [
-896,
192
],
"parameters": {
"jsCode": "const items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n let text = item.json.text || item.json.response || '';\n \n // Remove markdown code fences if present\n text = text.replace(/```json\\n?/g, '').replace(/```\\n?/g, '').trim();\n \n let parsed;\n try {\n parsed = JSON.parse(text);\n } catch (e) {\n // Try to extract JSON from the response\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n parsed = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('Could not parse Gemini response as JSON: ' + text);\n }\n }\n \n results.push({\n json: {\n name: parsed.name || '',\n company: parsed.company || '',\n title: parsed.title || '',\n phone: parsed.phone || '',\n email: parsed.email || '',\n address: parsed.address || '',\n replyToken: $('Encode Image as Base64').item.json.replyToken,\n lineUserId: $('Encode Image as Base64').item.json.lineUserId\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "51137eea-f7c8-4cad-a780-bde5e710110b",
"name": "Read Contacts from Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
-624,
192
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $json.email }}",
"lookupColumn": "Email"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Configuration Parameters').item.json.GOOGLE_SHEET_ID }}"
}
},
"typeVersion": 4.5
},
{
"id": "fa595a4b-65d7-48ad-85e0-525fc753c3fa",
"name": "If Contact Exists",
"type": "n8n-nodes-base.if",
"position": [
-384,
192
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "condition-dup",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $input.all().length }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2
},
{
"id": "605a5bb9-b8f0-4157-b4be-5af6c8424395",
"name": "Append Contact to Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
-128,
80
],
"parameters": {
"columns": {
"value": {
"Name": "={{ $('Parse Gemini Output Data').item.json.name }}",
"Email": "={{ $('Parse Gemini Output Data').item.json.email }}",
"Phone": "={{ $('Parse Gemini Output Data').item.json.phone }}",
"Title": "={{ $('Parse Gemini Output Data').item.json.title }}",
"Address": "={{ $('Parse Gemini Output Data').item.json.address }}",
"Company": "={{ $('Parse Gemini Output Data').item.json.company }}",
"LINE_UID": "={{ $('Parse Gemini Output Data').item.json.lineUserId }}",
"Timestamp": "={{ $now.toISO() }}"
},
"mappingMode": "defineBelow",
"matchingColumns": []
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Configuration Parameters').item.json.GOOGLE_SHEET_ID }}"
}
},
"typeVersion": 4.5
},
{
"id": "69a591ff-5069-4dd5-9436-20ff8d5ce6f0",
"name": "Post New Contact to Slack",
"type": "n8n-nodes-base.httpRequest",
"position": [
112,
80
],
"parameters": {
"url": "={{ $('Set Configuration Parameters').item.json.SLACK_WEBHOOK_URL }}",
"method": "POST",
"options": {},
"jsonBody": "={\n \"text\": \"\ud83d\udcc7 New contact saved: {{ $('Parse Gemini Output Data').item.json.name }} ({{ $('Parse Gemini Output Data').item.json.company }})\\nEmail: {{ $('Parse Gemini Output Data').item.json.email }}\\nPhone: {{ $('Parse Gemini Output Data').item.json.phone }}\"\n}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.2
},
{
"id": "26689c3c-7686-4779-8c93-401cd4ba14d6",
"name": "Send Success Reply via LINE",
"type": "n8n-nodes-base.httpRequest",
"position": [
352,
512
],
"parameters": {
"url": "https://api.line.me/v2/bot/message/reply",
"method": "POST",
"options": {},
"jsonBody": "={\n \"replyToken\": \"{{ $('Parse Gemini Output Data').item.json.replyToken }}\",\n \"messages\": [\n {\n \"type\": \"text\",\n \"text\": \"\u2705 \u767b\u9332\u5b8c\u4e86\uff1a{{ $('Parse Gemini Output Data').item.json.name }}\uff08{{ $('Parse Gemini Output Data').item.json.company }}\uff09\"\n }\n ]\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Set Configuration Parameters').item.json.LINE_CHANNEL_ACCESS_TOKEN }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "3c1868be-405b-4010-b65d-ff85c228b761",
"name": "Send Duplicate Reply via LINE",
"type": "n8n-nodes-base.httpRequest",
"position": [
-128,
816
],
"parameters": {
"url": "https://api.line.me/v2/bot/message/reply",
"method": "POST",
"options": {},
"jsonBody": "={\n \"replyToken\": \"{{ $('Parse Gemini Output Data').item.json.replyToken }}\",\n \"messages\": [\n {\n \"type\": \"text\",\n \"text\": \"\u2139\ufe0f \u3053\u306e\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u306f\u65e2\u306b\u767b\u9332\u6e08\u307f\u3067\u3059\"\n }\n ]\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Set Configuration Parameters').item.json.LINE_CHANNEL_ACCESS_TOKEN }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "1b5c694e-4a0f-444d-b770-0e2ff3a25cdc",
"name": "Respond Not Image to LINE",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-1632,
1120
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={\"replyToken\": \"{{ $json.events[0].replyToken }}\", \"messages\": [{\"type\": \"text\", \"text\": \"\ud83d\udcf7 \u540d\u523a\u306e\u5199\u771f\u3092\u9001\u3063\u3066\u304f\u3060\u3055\u3044\"}]}"
},
"typeVersion": 1.1
},
{
"id": "cdbc3fe3-773b-4889-bb44-6ad33b1dcd10",
"name": "Acknowledge OK Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
592,
512
],
"parameters": {
"options": {},
"respondWith": "noData"
},
"typeVersion": 1.1
},
{
"id": "9da4d88e-6b5f-4650-a004-70ff4a537f0d",
"name": "Acknowledge Duplicate Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
112,
816
],
"parameters": {
"options": {},
"respondWith": "noData"
},
"typeVersion": 1.1
}
],
"active": false,
"settings": {
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "78b721a6-3900-41da-95c0-998910fedcd3",
"connections": {
"If Image Message": {
"main": [
[
{
"node": "Fetch Image from LINE API",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Not Image to LINE",
"type": "main",
"index": 0
}
]
]
},
"If Contact Exists": {
"main": [
[
{
"node": "Send Duplicate Reply via LINE",
"type": "main",
"index": 0
}
],
[
{
"node": "Append Contact to Sheets",
"type": "main",
"index": 0
}
]
]
},
"Encode Image as Base64": {
"main": [
[
{
"node": "Extract Card Info with Gemini",
"type": "main",
"index": 0
}
]
]
},
"Append Contact to Sheets": {
"main": [
[
{
"node": "Post New Contact to Slack",
"type": "main",
"index": 0
}
]
]
},
"Parse Gemini Output Data": {
"main": [
[
{
"node": "Read Contacts from Sheets",
"type": "main",
"index": 0
}
]
]
},
"When LINE Event Received": {
"main": [
[
{
"node": "If Image Message",
"type": "main",
"index": 0
}
]
]
},
"Fetch Image from LINE API": {
"main": [
[
{
"node": "Encode Image as Base64",
"type": "main",
"index": 0
}
]
]
},
"Post New Contact to Slack": {
"main": [
[
{
"node": "Send Success Reply via LINE",
"type": "main",
"index": 0
}
]
]
},
"Read Contacts from Sheets": {
"main": [
[
{
"node": "If Contact Exists",
"type": "main",
"index": 0
}
]
]
},
"Send Success Reply via LINE": {
"main": [
[
{
"node": "Acknowledge OK Webhook",
"type": "main",
"index": 0
}
]
]
},
"Extract Card Info with Gemini": {
"main": [
[
{
"node": "Parse Gemini Output Data",
"type": "main",
"index": 0
}
]
]
},
"Send Duplicate Reply via LINE": {
"main": [
[
{
"node": "Acknowledge Duplicate Webhook",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Sales professionals, account managers, and anyone who exchanges business cards regularly. Designed especially for LINE users (Japan, Taiwan, Thailand, etc.) who want to eliminate manual data entry after networking events or client meetings.
Source: https://n8n.io/workflows/15308/ — 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.
ANIS_HUB 1. Uses gmail, googleDrive, googleSheets, httpRequest. Webhook trigger; 89 nodes.
Resume Screening & Behavioral Interviews with Gemini, Elevenlabs, & Notion ATS copy. Uses outputParserStructured, chainLlm, googleDrive, stickyNote. Webhook trigger; 67 nodes.
Candidate Engagement | Resume Screening | AI Voice Interviews | Applicant Insights
leads. Uses supabase, gmail, formTrigger, httpRequest. Webhook trigger; 62 nodes.
Categories: Accounting Automation • OCR Processing • AI Data Extraction • Business Tools