{
  "id": "5zAPU5L5WdkEkB4N",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Extract business card info to Google Sheets with LINE and AI",
  "tags": [],
  "nodes": [
    {
      "id": "2fad63fd-4433-407e-b759-6bb893930171",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -272,
        96
      ],
      "parameters": {
        "width": 508,
        "height": 409,
        "content": "## How it works\n1. Receive business card image via LINE webhook\n2. Extract text using Google Gemini AI\n3. Parse structured data (company, name, email, phone, etc.)\n4. Save to Google Sheets and upload image to Drive\n5. Send thank-you email and LINE confirmation\n\n## Setup steps\n1. Create LINE Messaging API channel and get Channel Access Token\n2. Set up Google Sheets with columns: Company Name, Contact Person, Department, Address, Email, Phone Number\n3. Create Google Drive folder for image storage\n4. Add your credentials in the Config node\n5. Set webhook URL in LINE Developer Console\n\n\u26a0\ufe0f Note: Processes one image per execution. Multiple images in one message are not supported."
      },
      "typeVersion": 1
    },
    {
      "id": "afc3a258-2988-4b82-b837-6c1bd6ae9750",
      "name": "Step 1 Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        256,
        96
      ],
      "parameters": {
        "color": 7,
        "width": 588,
        "height": 320,
        "content": "## Trigger & Config\nReceives LINE webhook events and loads configuration settings."
      },
      "typeVersion": 1
    },
    {
      "id": "3800f883-4e1e-40b9-bbff-0124f24d1b27",
      "name": "Step 2 Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        864,
        96
      ],
      "parameters": {
        "color": 7,
        "width": 380,
        "height": 704,
        "content": "## AI Processing\nDownloads image from LINE and extracts business card info using Gemini."
      },
      "typeVersion": 1
    },
    {
      "id": "1ca79a58-e64b-44f4-8dac-41e612048337",
      "name": "Step 3 Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1264,
        96
      ],
      "parameters": {
        "color": 7,
        "width": 1100,
        "height": 704,
        "content": "## Data Storage\nParses extracted text, uploads image to Drive, and saves data to Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "bca6ea4d-6e54-40c3-93cc-5eff08010505",
      "name": "Step 4 Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2384,
        96
      ],
      "parameters": {
        "color": 7,
        "width": 396,
        "height": 704,
        "content": "## Notification\nSends thank-you email and replies to LINE with extracted info."
      },
      "typeVersion": 1
    },
    {
      "id": "12bcda0e-bf0e-4620-b9fa-762fd6bce1e9",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "position": [
        496,
        256
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "line_token",
              "name": "LINE_CHANNEL_ACCESS_TOKEN",
              "type": "string",
              "value": "YOUR_LINE_CHANNEL_ACCESS_TOKEN_HEREK8T2FaPR+xpbJyhxHV0TkWjTijbq6IUcJNmcVfUqZMY3og/eZ1Lw1sb9786bGzeiUA2zj/9GraOfpQdB04t89/1O/w1cDnyilFU="
            },
            {
              "id": "sheets_id",
              "name": "GOOGLE_SHEETS_ID",
              "type": "string",
              "value": "YOUR_GOOGLE_SHEETS_ID_HERE"
            },
            {
              "id": "drive_folder",
              "name": "GOOGLE_DRIVE_FOLDER_ID",
              "type": "string",
              "value": "YOUR_GOOGLE_DRIVE_FOLDER_ID_HERE"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "fef3f682-efa0-4690-977b-3e5ff2554af3",
      "name": "LINE Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        272,
        256
      ],
      "parameters": {
        "path": "/BusinessCard",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "d990bacc-ff90-4885-9397-b5e194277ea1",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        928,
        656
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d21403d1-5ea8-4708-9b30-edac6904f1f0",
      "name": "Check If Image",
      "type": "n8n-nodes-base.switch",
      "position": [
        736,
        256
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "1d9fe3c1-bebe-4a3a-9645-720c91e9ebbf",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $('LINE Webhook').item.json.body.events[0].message.type }}",
                    "rightValue": "image"
                  }
                ]
              }
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.3
    },
    {
      "id": "dc37f2b3-7e59-4517-a6f0-aabf30fa2992",
      "name": "Download Image from LINE",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1008,
        256
      ],
      "parameters": {
        "url": "=https://api-data.line.me/v2/bot/message/{{ $('LINE Webhook').item.json.body.events[0].message.id }}/content",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('Config').item.json.LINE_CHANNEL_ACCESS_TOKEN }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "9b07ef81-28e6-4217-8939-12bb259695bf",
      "name": "Extract Card Info",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        928,
        480
      ],
      "parameters": {
        "text": "Extract the following information from this business card image.\n\n\u3010IMPORTANT RULES\u3011\n- Use the exact key names listed below without any modification\n- Output each item in one line using the format \"Key: Value\"\n- Do not add bullets or numbers at the beginning of lines\n- If information cannot be extracted, output \"\u30fc\" (dash)\n- Do not add explanations or supplementary text\n- If no information can be extracted at all, only output \"Extraction Failed\"\n\n\u3010OUTPUT FORMAT\u3011\nCompany Name: \nContact Person: \nDepartment: \nAddress: \nEmail: \nPhone:",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "type": "HumanMessagePromptTemplate",
              "messageType": "imageBinary"
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "9be197a9-8832-4550-8bce-dac50c87d59f",
      "name": "Parse Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1552,
        304
      ],
      "parameters": {
        "jsCode": "// Input\nconst text =\n  $json?.text ??\n  $json?.data?.text ??\n  $input.first().json?.text ??\n  \"\";\n\n// 1) Normalize text\nconst normalize = (s) =>\n  String(s ?? \"\")\n    .trim()\n    .replace(/[\u30fb\u25cf\u25a0\u25c6\u25c7\u25b6\ufe0e]/g, \"\")        \n    .replace(/[\u3000\\t]+/g, \" \")           \n    .replace(/[\uff1a]/g, \":\")              \n    .replace(/[\uff08(].*?[\uff09)]/g, \"\")       \n    .trim();\n\nconst normKey = (k) =>\n  normalize(k)\n    .toLowerCase()\n    .replace(/\\s+/g, \"\");\n\n// 2) Key aliases mapping\nconst KEY_ALIASES = {\n  company: new Set([\n    \"company name\", \"company\", \"organization\", \"corporation\",\n    \"\u4f1a\u793e\u540d\",\"\u6cd5\u4eba\u540d\",\"\u4f01\u696d\u540d\",\"\u793e\u540d\",\"\u4f1a\u793e\",\"\u6cd5\u4eba\",\"\u4f01\u696d\"\n  ]),\n  name: new Set([\n    \"contact person\", \"name\", \"person\", \"full name\",\n    \"\u62c5\u5f53\u8005\u540d\",\"\u6c0f\u540d\",\"\u540d\u524d\",\"\u62c5\u5f53\u8005\"\n  ]),\n  department: new Set([\n    \"department\", \"division\", \"section\",\n    \"\u62c5\u5f53\u8005\u540d\u306e\u6240\u5c5e\u90e8\u7f72\u540d\",\"\u6240\u5c5e\u90e8\u7f72\",\"\u90e8\u7f72\",\"\u90e8\u9580\",\"\u6240\u5c5e\"\n  ]),\n  address: new Set([\n    \"address\", \"location\",\n    \"\u4f4f\u6240\",\"\u6240\u5728\u5730\"\n  ]),\n  email: new Set([\n    \"email\", \"e-mail\", \"mail\",\n    \"\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\",\"e\u30e1\u30fc\u30eb\",\"\u9023\u7d61\u5148\u30e1\u30fc\u30eb\"\n  ]),\n  phone: new Set([\n    \"phone\", \"telephone\", \"tel\", \"mobile\", \"cell\",\n    \"\u96fb\u8a71\u756a\u53f7\",\"\u643a\u5e2f\",\"\u9023\u7d61\u5148\",\"\u96fb\u8a71\"\n  ]),\n};\n\n// 3) Parse lines\nconst toLines = (t) =>\n  String(t)\n    .replace(/\\\\n/g, \"\\n\")\n    .split(/\\r?\\n/)\n    .map((l) => normalize(l))\n    .filter(Boolean);\n\nconst lines = toLines(text);\n\n// 4) Resolve key\nfunction resolveKey(rawKey) {\n  const nk = normKey(rawKey);\n\n  for (const [fixed, set] of Object.entries(KEY_ALIASES)) {\n    for (const alias of set) {\n      if (nk === normKey(alias)) return fixed;\n    }\n  }\n\n  if (nk.includes(\"tel\") || nk.includes(\"phone\") || nk.includes(\"mobile\") || nk.includes(\"\u643a\u5e2f\")) return \"phone\";\n  if (nk.includes(\"mail\")) return \"email\";\n  if (nk.includes(\"\u4f4f\u6240\") || nk.includes(\"\u6240\u5728\u5730\") || nk.includes(\"address\")) return \"address\";\n  if (nk.includes(\"\u4f1a\u793e\") || nk.includes(\"\u793e\u540d\") || nk.includes(\"company\")) return \"company\";\n  if (nk.includes(\"\u90e8\u7f72\") || nk.includes(\"department\") || nk.includes(\"division\")) return \"department\";\n  if (nk.includes(\"\u62c5\u5f53\") || nk.includes(\"\u6c0f\u540d\") || nk.includes(\"name\")) return \"name\";\n\n  return null;\n}\n\n// 5) Extract phone numbers\nfunction extractPhones(s) {\n  const str = String(s ?? \"\");\n  const matches = str.match(/\\b0\\d{1,4}[-\\s]?\\d{1,4}[-\\s]?\\d{3,4}\\b/g) ?? [];\n  const cleaned = matches\n    .map((p) => p.replace(/\\s+/g, \"-\"))\n    .map((p) => p.replace(/-+/g, \"-\"))\n    .map((p) => p.trim())\n    .filter(Boolean);\n\n  return Array.from(new Set(cleaned));\n}\n\nconst out = {\n  company: \"\",\n  name: \"\",\n  department: \"\",\n  address: \"\",\n  email: \"\",\n  phone: \"\",\n  phoneList: [],\n};\n\nfor (const line of lines) {\n  const m = line.match(/^(.+?):\\s*(.+)$/);\n  if (!m) continue;\n\n  const rawKey = m[1];\n  const value = m[2];\n\n  const fixed = resolveKey(rawKey);\n  if (!fixed) continue;\n\n  if (out[fixed]) continue;\n\n  out[fixed] = value;\n}\n\nconst phones = extractPhones(out.phone);\nout.phoneList = phones;\n\nif (phones.length) out.phone = phones.join(\" / \");\n\nif (out.email) out.email = String(out.email).trim().toLowerCase();\n\nfor (const k of Object.keys(out)) {\n  if (typeof out[k] === \"string\" && out[k].trim() === \"\") delete out[k];\n  if (Array.isArray(out[k]) && out[k].length === 0) delete out[k];\n}\n\nreturn [{ \n  json: out,\n  binary: $input.first().binary\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "1ef2af98-caef-4fe3-bc9b-15c271a6152f",
      "name": "Save to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2224,
        208
      ],
      "parameters": {
        "columns": {
          "value": {
            "\u4f4f\u6240": "={{ $('Parse Data').item.json.address }}",
            "\u4f1a\u793e\u540d": "={{ $('Parse Data').item.json.company }}",
            "\u62c5\u5f53\u8005\u540d": "={{ $('Parse Data').item.json.name }}",
            "\u96fb\u8a71\u756a\u53f7": "={{ $('Parse Data').item.json.phoneList.join(\"/\") }}",
            "\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9": "={{ $('Parse Data').item.json.email }}",
            "\u62c5\u5f53\u8005\u306e\u6240\u5c5e\u90e8\u7f72\u540d": "={{ $('Parse Data').item.json.department }}"
          },
          "schema": [
            {
              "id": "\u4f1a\u793e\u540d",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "\u4f1a\u793e\u540d",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "\u62c5\u5f53\u8005\u540d",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "\u62c5\u5f53\u8005\u540d",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "\u62c5\u5f53\u8005\u306e\u6240\u5c5e\u90e8\u7f72\u540d",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "\u62c5\u5f53\u8005\u306e\u6240\u5c5e\u90e8\u7f72\u540d",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "\u4f4f\u6240",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "\u4f4f\u6240",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "\u96fb\u8a71\u756a\u53f7",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "\u96fb\u8a71\u756a\u53f7",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Config').item.json.GOOGLE_SHEETS_ID }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "df3c9ba2-ea61-4025-bb93-4594f8deab42",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        1776,
        208
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "85f0b504-a3c0-44dd-9aca-aaedf07205b6",
      "name": "Notify Analysis Failed",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1552,
        576
      ],
      "parameters": {
        "url": "https://api.line.me/v2/bot/message/push",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"to\": \"{{ $('LINE Webhook').item.json.body.events[0].source.userId }}\",\n  \"messages\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"Sorry, we were unable to read any information from your business card image. \\n\\nCould you please send us another image?\"\n    }\n  ]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('Config').item.json.LINE_CHANNEL_ACCESS_TOKEN }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "8df1769e-bd52-4250-beec-d0d08d268b0d",
      "name": "Upload to Google Drive",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        2000,
        208
      ],
      "parameters": {
        "name": "={{ 'BusinessCard_' + $now.format('yyyyMMdd_HHmmss') }}.jpg",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "options": {},
        "folderId": {
          "__rl": true,
          "mode": "list",
          "value": "={{ $('Config').item.json.GOOGLE_DRIVE_FOLDER_ID }}"
        }
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "8056dc52-6da6-40c8-badb-654798bb1b2f",
      "name": "Reply to LINE",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2656,
        208
      ],
      "parameters": {
        "url": "https://api.line.me/v2/bot/message/push",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"to\": \"{{ $('LINE Webhook').item.json.body.events[0].source.userId }}\",\n  \"messages\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"The following information has been extracted.\\nCompany: {{ $('Parse Data').item.json.company }}\\nContact Name: {{ $('Parse Data').item.json.name }}\\nDepartment: {{ $('Parse Data').item.json.department }}\\nAddress: {{ $('Parse Data').item.json.address }}\\nEmail: {{ $('Parse Data').item.json.email }}\\nPhone: {{ $('Parse Data').item.json.phoneList.join('/') }}\"\n    }\n  ]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('Config').item.json.LINE_CHANNEL_ACCESS_TOKEN }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "cd7514ad-f719-46b5-8927-3cd315d1fac2",
      "name": "Send Thank You Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2432,
        208
      ],
      "parameters": {
        "sendTo": "={{ $json['\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9'] }}",
        "message": "=Dear {{ $json['\u62c5\u5f53\u8005\u540d'] }},\n\nIt was a pleasure meeting you today. Thank you very much for taking the time to exchange business cards with me.\n\nI truly appreciated learning more about your work and your insights. I hope we will have the opportunity to stay in touch and explore possible collaboration in the future.\n\nPlease feel free to contact me if you need any further information.\n\nBest regards,\n[Your Name]",
        "options": {},
        "subject": "Thank You for the Opportunity to Meet",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "8d532d8b-d4f9-4a21-a5f9-2862432c8a79",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        1280,
        480
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "412a45c2-b07c-45ed-a629-a5734cc6ca1a",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.text }}",
              "rightValue": "Extraction Failed"
            }
          ]
        }
      },
      "typeVersion": 2.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "bfdf8229-ab5d-4cd7-bac0-5a2ffaf09d9f",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Parse Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Notify Analysis Failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Upload to Google Drive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Check If Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Data": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "LINE Webhook": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check If Image": {
      "main": [
        [
          {
            "node": "Download Image from LINE",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Card Info": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Thank You Email": {
      "main": [
        [
          {
            "node": "Reply to LINE",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Google Sheets": {
      "main": [
        [
          {
            "node": "Send Thank You Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to Google Drive": {
      "main": [
        [
          {
            "node": "Save to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Image from LINE": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          },
          {
            "node": "Extract Card Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Extract Card Info",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  }
}