This workflow corresponds to n8n.io template #12380 — we link there as the canonical source.
This workflow follows the Chainllm → Google Drive 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": "ehgzqMTQUzbfPdBY",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "AI Meal Nutrition Tracker with LINE and Google Sheets",
"tags": [],
"nodes": [
{
"id": "7bfd63f9-80bf-44a9-8e6c-957343f7cd65",
"name": "Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-448,
-64
],
"parameters": {
"width": 540,
"height": 576,
"content": "## \ud83c\udf7d\ufe0f AI Meal Nutrition Tracker\n\nThis workflow creates an AI-powered meal tracking assistant that analyzes food photos and provides nutritional insights.\n\n## How it works\n1. Receive meal photo via LINE webhook\n2. Analyze food using Google Gemini AI vision\n3. Extract nutritional data (calories, protein, carbs, fat, fiber)\n4. Generate personalized health advice\n5. Save meal log to Google Sheets\n6. Upload image to Google Drive for records\n7. Send detailed analysis back via LINE\n\n## Setup steps\n1. Create LINE Messaging API channel and get Channel Access Token\n2. Set up Google Sheets with columns: Date, Time, Meal Type, Food Items, Calories, Protein, Carbs, Fat, Fiber, Health Score, Advice\n3. Create Google Drive folder for meal photo storage\n4. Add your credentials in the Config node\n5. Set webhook URL in LINE Developer Console\n\n\u26a0\ufe0f Note: Nutritional values are AI estimates and should not replace professional dietary advice."
},
"typeVersion": 1
},
{
"id": "1c2d946f-cbac-4a67-a810-2db757d50050",
"name": "Step 1 - Trigger & Config",
"type": "n8n-nodes-base.stickyNote",
"position": [
112,
-64
],
"parameters": {
"color": 7,
"width": 588,
"height": 380,
"content": "## Trigger & Configuration\nReceives LINE webhook events and loads API credentials and configuration settings.\n\n**LINE Webhook**: Listens for incoming messages from LINE\n**Config**: Stores all necessary credentials and IDs"
},
"typeVersion": 1
},
{
"id": "f282d8f4-493e-4857-8b9d-272242d4a126",
"name": "Step 2 - Image Processing",
"type": "n8n-nodes-base.stickyNote",
"position": [
720,
-64
],
"parameters": {
"color": 7,
"width": 420,
"height": 760,
"content": "## AI Food Analysis\nDownloads meal image from LINE and uses Google Gemini to:\n- Identify food items\n- Estimate nutritional content\n- Calculate health score\n- Generate personalized advice"
},
"typeVersion": 1
},
{
"id": "09bcc428-df71-49fc-8981-7069cb629297",
"name": "Step 3 - Data Storage",
"type": "n8n-nodes-base.stickyNote",
"position": [
1168,
-64
],
"parameters": {
"color": 7,
"width": 1140,
"height": 760,
"content": "## Data Processing & Storage\nParses AI response, structures nutritional data, uploads meal photo to Google Drive, and logs everything to Google Sheets for tracking history."
},
"typeVersion": 1
},
{
"id": "01c11aba-87cb-4480-ae7e-849d3cd58d0d",
"name": "Step 4 - Response",
"type": "n8n-nodes-base.stickyNote",
"position": [
2320,
-64
],
"parameters": {
"color": 7,
"width": 440,
"height": 760,
"content": "## LINE Response\nSends formatted nutritional analysis and health advice back to the user via LINE message."
},
"typeVersion": 1
},
{
"id": "507db298-802f-41de-b74b-0d4a31836a55",
"name": "LINE Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
128,
144
],
"parameters": {
"path": "/MealTracker",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "bd86d3fe-6c3e-4439-9215-9d4d6e2af3bc",
"name": "Config",
"type": "n8n-nodes-base.set",
"position": [
352,
144
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "line_token",
"name": "LINE_CHANNEL_ACCESS_TOKEN",
"type": "string",
"value": "YOUR_LINE_CHANNEL_ACCESS_TOKEN_HERE"
},
{
"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"
},
{
"id": "daily_calorie_goal",
"name": "DAILY_CALORIE_GOAL",
"type": "number",
"value": 2000
}
]
}
},
"typeVersion": 3.4
},
{
"id": "9bbd4988-61dc-462e-acb3-78d00177bfc9",
"name": "Check If Image",
"type": "n8n-nodes-base.switch",
"position": [
592,
144
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "check-image-type",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $('LINE Webhook').item.json.body.events[0].message.type }}",
"rightValue": "image"
}
]
}
}
]
},
"options": {}
},
"typeVersion": 3.3
},
{
"id": "330d67a0-d949-4b1a-9560-478a5128bf5c",
"name": "Download Image from LINE",
"type": "n8n-nodes-base.httpRequest",
"position": [
864,
144
],
"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": "660c9c78-9daf-47c0-9dd0-6f9a14f0b032",
"name": "Google Gemini Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
784,
560
],
"parameters": {
"options": {}
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "aff67f4f-8d62-4c63-ac26-2b8ce6270414",
"name": "Analyze Meal with AI",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
784,
384
],
"parameters": {
"text": "You are a professional nutritionist AI assistant. Analyze this meal photo and provide detailed nutritional information.\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- Estimate nutritional values based on typical serving sizes visible in the image\n- If you cannot identify certain foods, make reasonable assumptions\n- Provide practical, encouraging health advice\n- If the image is not food-related, output \"Not a food image\"\n\n\u3010OUTPUT FORMAT\u3011\nFood Items: (list all identified foods separated by commas)\nMeal Type: (Breakfast/Lunch/Dinner/Snack)\nEstimated Calories: (total kcal as number only)\nProtein: (grams as number only)\nCarbohydrates: (grams as number only)\nFat: (grams as number only)\nFiber: (grams as number only)\nHealth Score: (1-10 based on nutritional balance)\nPositive Points: (what's good about this meal)\nImprovement Tips: (suggestions for healthier choices)\nAdvice: (personalized health tip in 1-2 sentences)",
"batching": {},
"messages": {
"messageValues": [
{
"type": "HumanMessagePromptTemplate",
"messageType": "imageBinary"
}
]
},
"promptType": "define"
},
"typeVersion": 1.7
},
{
"id": "9ed1be62-cd91-4130-ab13-3726de92d1f4",
"name": "Check Analysis Result",
"type": "n8n-nodes-base.if",
"position": [
1216,
384
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "check-not-food",
"operator": {
"type": "string",
"operation": "notContains"
},
"leftValue": "={{ $json.text }}",
"rightValue": "Not a food image"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "c423df6f-e10f-4c0c-9d75-eaacb77d9158",
"name": "Parse Nutrition Data",
"type": "n8n-nodes-base.code",
"position": [
1440,
192
],
"parameters": {
"jsCode": "// Input\nconst text = $json?.text ?? $input.first().json?.text ?? \"\";\nconst dailyGoal = $('Config').item.json.DAILY_CALORIE_GOAL || 2000;\n\n// Normalize text\nconst normalize = (s) =>\n String(s ?? \"\")\n .trim()\n .replace(/[\uff1a]/g, \":\")\n .trim();\n\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// Key mapping\nconst KEY_MAP = {\n \"food items\": \"foodItems\",\n \"meal type\": \"mealType\",\n \"estimated calories\": \"calories\",\n \"protein\": \"protein\",\n \"carbohydrates\": \"carbs\",\n \"fat\": \"fat\",\n \"fiber\": \"fiber\",\n \"health score\": \"healthScore\",\n \"positive points\": \"positivePoints\",\n \"improvement tips\": \"improvementTips\",\n \"advice\": \"advice\"\n};\n\nconst out = {\n date: $now.toFormat('yyyy-MM-dd'),\n time: $now.toFormat('HH:mm'),\n foodItems: \"\",\n mealType: \"\",\n calories: 0,\n protein: 0,\n carbs: 0,\n fat: 0,\n fiber: 0,\n healthScore: 0,\n positivePoints: \"\",\n improvementTips: \"\",\n advice: \"\",\n dailyGoal: dailyGoal,\n remainingCalories: dailyGoal\n};\n\nfor (const line of lines) {\n const m = line.match(/^(.+?):\\s*(.+)$/);\n if (!m) continue;\n\n const rawKey = m[1].toLowerCase().trim();\n const value = m[2].trim();\n\n const mappedKey = KEY_MAP[rawKey];\n if (!mappedKey) continue;\n\n // Parse numeric values\n if ([\"calories\", \"protein\", \"carbs\", \"fat\", \"fiber\", \"healthScore\"].includes(mappedKey)) {\n const numMatch = value.match(/([\\d.]+)/);\n out[mappedKey] = numMatch ? parseFloat(numMatch[1]) : 0;\n } else {\n out[mappedKey] = value;\n }\n}\n\n// Calculate remaining calories\nout.remainingCalories = out.dailyGoal - out.calories;\n\n// Generate health emoji based on score\nconst score = out.healthScore;\nif (score >= 8) {\n out.healthEmoji = \"\ud83c\udf1f\";\n out.healthLevel = \"Excellent\";\n} else if (score >= 6) {\n out.healthEmoji = \"\u2705\";\n out.healthLevel = \"Good\";\n} else if (score >= 4) {\n out.healthEmoji = \"\u26a0\ufe0f\";\n out.healthLevel = \"Fair\";\n} else {\n out.healthEmoji = \"\ud83d\udd34\";\n out.healthLevel = \"Needs Improvement\";\n}\n\nreturn [{ \n json: out,\n binary: $input.first().binary\n}];"
},
"typeVersion": 2
},
{
"id": "fdaa95dd-91af-4d8c-8226-1e5347fc9a46",
"name": "Notify Not Food Image",
"type": "n8n-nodes-base.httpRequest",
"position": [
1440,
480
],
"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\": \"\ud83c\udf7d\ufe0f Oops! That doesn't look like a food image.\\n\\nPlease send a clear photo of your meal, and I'll analyze its nutritional content for you! \ud83d\udcf8\"\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": "51d7e19c-72c8-479a-b014-611cb4c3f59b",
"name": "Merge Data",
"type": "n8n-nodes-base.merge",
"position": [
1680,
96
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"typeVersion": 3
},
{
"id": "c1a3d420-6eb5-4c64-89bb-9a8466e23247",
"name": "Upload to Google Drive",
"type": "n8n-nodes-base.googleDrive",
"position": [
1904,
96
],
"parameters": {
"name": "={{ 'Meal_' + $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": "3ddf8488-c0f3-4636-ab42-8d9f510ef972",
"name": "Save to Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
2128,
96
],
"parameters": {
"columns": {
"value": {
"Date": "={{ $('Parse Nutrition Data').item.json.date }}",
"Time": "={{ $('Parse Nutrition Data').item.json.time }}",
"Advice": "={{ $('Parse Nutrition Data').item.json.advice }}",
"Fat (g)": "={{ $('Parse Nutrition Data').item.json.fat }}",
"Calories": "={{ $('Parse Nutrition Data').item.json.calories }}",
"Carbs (g)": "={{ $('Parse Nutrition Data').item.json.carbs }}",
"Fiber (g)": "={{ $('Parse Nutrition Data').item.json.fiber }}",
"Image URL": "={{ $('Upload to Google Drive').item.json.webViewLink }}",
"Meal Type": "={{ $('Parse Nutrition Data').item.json.mealType }}",
"Food Items": "={{ $('Parse Nutrition Data').item.json.foodItems }}",
"Protein (g)": "={{ $('Parse Nutrition Data').item.json.protein }}",
"Health Score": "={{ $('Parse Nutrition Data').item.json.healthScore }}"
},
"schema": [
{
"id": "Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Time",
"type": "string",
"display": true,
"required": false,
"displayName": "Time",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Meal Type",
"type": "string",
"display": true,
"required": false,
"displayName": "Meal Type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Food Items",
"type": "string",
"display": true,
"required": false,
"displayName": "Food Items",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Calories",
"type": "number",
"display": true,
"required": false,
"displayName": "Calories",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Protein (g)",
"type": "number",
"display": true,
"required": false,
"displayName": "Protein (g)",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Carbs (g)",
"type": "number",
"display": true,
"required": false,
"displayName": "Carbs (g)",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Fat (g)",
"type": "number",
"display": true,
"required": false,
"displayName": "Fat (g)",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Fiber (g)",
"type": "number",
"display": true,
"required": false,
"displayName": "Fiber (g)",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Health Score",
"type": "number",
"display": true,
"required": false,
"displayName": "Health Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Advice",
"type": "string",
"display": true,
"required": false,
"displayName": "Advice",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Image URL",
"type": "string",
"display": true,
"required": false,
"displayName": "Image URL",
"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": "fd97ad04-0b00-4912-8a2e-10abd844ee98",
"name": "Reply to LINE",
"type": "n8n-nodes-base.httpRequest",
"position": [
2496,
96
],
"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\": \"\ud83c\udf7d\ufe0f Meal Analysis Complete!\\n\\n\ud83d\udccb {{ $('Parse Nutrition Data').item.json.mealType }}\\n\ud83c\udf74 {{ $('Parse Nutrition Data').item.json.foodItems }}\\n\\n\u2501\u2501\u2501 Nutrition Facts \u2501\u2501\u2501\\n\ud83d\udd25 Calories: {{ $('Parse Nutrition Data').item.json.calories }} kcal\\n\ud83e\udd69 Protein: {{ $('Parse Nutrition Data').item.json.protein }}g\\n\ud83c\udf5e Carbs: {{ $('Parse Nutrition Data').item.json.carbs }}g\\n\ud83e\uddc8 Fat: {{ $('Parse Nutrition Data').item.json.fat }}g\\n\ud83e\udd6c Fiber: {{ $('Parse Nutrition Data').item.json.fiber }}g\\n\\n\u2501\u2501\u2501 Health Assessment \u2501\u2501\u2501\\n{{ $('Parse Nutrition Data').item.json.healthEmoji }} Score: {{ $('Parse Nutrition Data').item.json.healthScore }}/10 ({{ $('Parse Nutrition Data').item.json.healthLevel }})\\n\\n\u2728 Good: {{ $('Parse Nutrition Data').item.json.positivePoints }}\\n\\n\ud83d\udca1 Tip: {{ $('Parse Nutrition Data').item.json.improvementTips }}\\n\\n\ud83c\udfaf {{ $('Parse Nutrition Data').item.json.advice }}\\n\\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n\ud83d\udcca Daily Goal: {{ $('Parse Nutrition Data').item.json.dailyGoal }} kcal\\n\u23f3 Remaining: {{ $('Parse Nutrition Data').item.json.remainingCalories }} kcal\"\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
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "9f1d72f0-34cf-4868-b504-d7a3b8e5f603",
"connections": {
"Config": {
"main": [
[
{
"node": "Check If Image",
"type": "main",
"index": 0
}
]
]
},
"Merge Data": {
"main": [
[
{
"node": "Upload to Google Drive",
"type": "main",
"index": 0
}
]
]
},
"LINE Webhook": {
"main": [
[
{
"node": "Config",
"type": "main",
"index": 0
}
]
]
},
"Check If Image": {
"main": [
[
{
"node": "Download Image from LINE",
"type": "main",
"index": 0
}
]
]
},
"Analyze Meal with AI": {
"main": [
[
{
"node": "Check Analysis Result",
"type": "main",
"index": 0
}
]
]
},
"Parse Nutrition Data": {
"main": [
[
{
"node": "Merge Data",
"type": "main",
"index": 1
}
]
]
},
"Check Analysis Result": {
"main": [
[
{
"node": "Parse Nutrition Data",
"type": "main",
"index": 0
}
],
[
{
"node": "Notify Not Food Image",
"type": "main",
"index": 0
}
]
]
},
"Save to Google Sheets": {
"main": [
[
{
"node": "Reply to LINE",
"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 Data",
"type": "main",
"index": 0
},
{
"node": "Analyze Meal with AI",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "Analyze Meal with AI",
"type": "ai_languageModel",
"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.
googleDriveOAuth2ApigooglePalmApigoogleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow is designed for health-conscious individuals, fitness enthusiasts, and anyone who wants to track their daily food intake without manual calorie counting.
Source: https://n8n.io/workflows/12380/ — 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
Categories: Accounting Automation • OCR Processing • AI Data Extraction • Business Tools
This workflow converts handwritten memo images sent via LINE into structured, searchable knowledge using AI.Users simply send a handwritten memo photo. The workflow automatically performs OCR, summari