AutomationFlowsAI & RAG › Log Food Calories From Images to Google Sheets Using Line and Openai Vision

Log Food Calories From Images to Google Sheets Using Line and Openai Vision

Bykote2 @kote2 on n8n.io

This workflow allows a LINE user to send either text or an image of food to a connected LINE bot.

Webhook trigger★★★★☆ complexityAI-powered17 nodesAgentOpenAI ChatOpenAIMemory Buffer WindowGoogle SheetsHTTP Request
AI & RAG Trigger: Webhook Nodes: 17 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #7228 — we link there as the canonical source.

This workflow follows the Agent → 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 →

Download .json
{
  "id": "HzZvyjb6riqHb2FR",
  "name": "export n8n",
  "tags": [],
  "nodes": [
    {
      "id": "74a3253b-68d1-4745-85a1-70f5fa3260a9",
      "name": "Switch",
      "type": "n8n-nodes-base.switch",
      "position": [
        -592,
        48
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "80b1a421-efcd-487e-8c18-925496da9b0c",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.body.events[0].message.type }}",
                    "rightValue": "text"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "c124e4cd-78b0-4ce1-bf72-26806f7211e8",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.body.events[0].message.type }}",
                    "rightValue": "image"
                  }
                ]
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "adf17e8b-4e45-4ad4-90e9-208b5bf6207f",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        160,
        -160
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "options": {
          "systemMessage": "=You are an AI assistant. Your job is to respond to user messages."
        },
        "promptType": "define"
      },
      "typeVersion": 2.1
    },
    {
      "id": "ef15eb25-64d9-4c30-becc-7f338d46ce55",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        32,
        48
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "17e136f4-34f7-42c8-9793-19d13610aa49",
      "name": "Analyze image",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        -96,
        272
      ],
      "parameters": {
        "text": "=Estimate calories only if the item in this image is a food item, and output the calories in the following simple JSON format:\n\n[{\n\"dishName\": \"result\",\n\"calories\": \"result\",\n}]\n\nDo not output anything but this JSON only if it is food.",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "chatgpt-4o-latest",
          "cachedResultName": "CHATGPT-4O-LATEST"
        },
        "options": {},
        "resource": "image",
        "inputType": "base64",
        "operation": "analyze"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "0359b260-8639-475b-8d47-cc76e004f027",
      "name": "Edit Fields1",
      "type": "n8n-nodes-base.set",
      "position": [
        576,
        272
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "6e9a99a3-62b1-4121-b7d7-148eaebbe9ba",
              "name": "output",
              "type": "string",
              "value": "={{ $json.message }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "0535be0a-4aac-48a8-8909-41abca4944a0",
      "name": "Simple Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        176,
        48
      ],
      "parameters": {
        "sessionKey": "={{ $('LINE webhook').item.json.body.events[0].source.userId }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.3
    },
    {
      "id": "2851ca68-76c5-4b97-910d-2d4fa729754f",
      "name": "Code",
      "type": "n8n-nodes-base.code",
      "position": [
        352,
        608
      ],
      "parameters": {
        "jsCode": "const inputData = $input.all();\nconst results = [];\n\nfor (let i = 0; i < inputData.length; i++) {\n  try {\n\n    let aiResponse = inputData[i].json.output || inputData[i].json.text || inputData[i].json.content || '';\n    \n    if (!aiResponse) {\n      console.log(`\u30a2\u30a4\u30c6\u30e0 ${i}: \u7a7a\u306e\u5fdc\u7b54\u3092\u30b9\u30ad\u30c3\u30d7`);\n      continue;\n    }\n    \n    // \u6587\u5b57\u5217\u306e\u524d\u5f8c\u306e\u7a7a\u767d\u3092\u524a\u9664\n    aiResponse = aiResponse.trim();\n    \n    // JSON\u306e\u524d\u5f8c\u306b\u3042\u308b\u4e0d\u8981\u306a\u30c6\u30ad\u30b9\u30c8\u3092\u524a\u9664\n    // ```json \u3084 ``` \u3067\u56f2\u307e\u308c\u3066\u3044\u308b\u5834\u5408\u306e\u51e6\u7406\n    if (aiResponse.includes('```json')) {\n      const jsonStart = aiResponse.indexOf('```json') + 7;\n      const jsonEnd = aiResponse.indexOf('```', jsonStart);\n      if (jsonEnd !== -1) {\n        aiResponse = aiResponse.substring(jsonStart, jsonEnd).trim();\n      }\n    } else if (aiResponse.includes('```')) {\n      const jsonStart = aiResponse.indexOf('```') + 3;\n      const jsonEnd = aiResponse.indexOf('```', jsonStart);\n      if (jsonEnd !== -1) {\n        aiResponse = aiResponse.substring(jsonStart, jsonEnd).trim();\n      }\n    }\n    \n    // JSON\u306e\u958b\u59cb\u3068\u7d42\u4e86\u3092\u898b\u3064\u3051\u308b\n    const jsonStartIndex = aiResponse.indexOf('[');\n    const jsonEndIndex = aiResponse.lastIndexOf(']');\n    \n    if (jsonStartIndex !== -1 && jsonEndIndex !== -1 && jsonEndIndex > jsonStartIndex) {\n      const jsonString = aiResponse.substring(jsonStartIndex, jsonEndIndex + 1);\n      \n      try {\n        // JSON\u3092\u30d1\u30fc\u30b9\n        const parsedData = JSON.parse(jsonString);\n        \n        // \u914d\u5217\u3067\u306a\u3044\u5834\u5408\u306f\u914d\u5217\u306b\u5909\u63db\n        const dataArray = Array.isArray(parsedData) ? parsedData : [parsedData];\n        \n        // \u5404\u30a2\u30a4\u30c6\u30e0\u3092\u51e6\u7406\n        dataArray.forEach((item, index) => {\n          // \u5fc5\u8981\u306a\u30d5\u30a3\u30fc\u30eb\u30c9\u304c\u5b58\u5728\u3059\u308b\u3053\u3068\u3092\u78ba\u8a8d\n          if (item && typeof item === 'object') {\n            const processedItem = {\n              dishName: item.dishName || item.dish_name || item.name || 'Unknown',\n              calories: item.calories || item.calorie || item.cal || 0,\n              // \u5143\u306e\u30c7\u30fc\u30bf\u3082\u4fdd\u6301\uff08\u30c7\u30d0\u30c3\u30b0\u7528\uff09\n              originalIndex: i,\n              itemIndex: index,\n              timestamp: new Date().toISOString()\n            };\n            \n            // \u30ab\u30ed\u30ea\u30fc\u304c\u6570\u5024\u3067\u306a\u3044\u5834\u5408\u306f\u6570\u5024\u306b\u5909\u63db\u3092\u8a66\u884c\n            if (typeof processedItem.calories === 'string') {\n              const numericCalories = parseInt(processedItem.calories.replace(/[^\\d]/g, ''));\n              processedItem.calories = isNaN(numericCalories) ? 0 : numericCalories;\n            }\n            \n            results.push({ json: processedItem });\n          }\n        });\n        \n      } catch (parseError) {\n        console.log(`\u30a2\u30a4\u30c6\u30e0 ${i}: JSON\u89e3\u6790\u30a8\u30e9\u30fc - ${parseError.message}`);\n        console.log(`\u554f\u984c\u306e\u3042\u308bJSON\u6587\u5b57\u5217: ${jsonString}`);\n        \n        // JSON\u304c\u7121\u52b9\u306a\u5834\u5408\u3067\u3082\u3001\u624b\u52d5\u3067\u30c7\u30fc\u30bf\u3092\u62bd\u51fa\u3092\u8a66\u884c\n        const manualExtract = extractDataManually(aiResponse);\n        if (manualExtract) {\n          results.push({ json: manualExtract });\n        }\n      }\n    } else {\n      console.log(`\u30a2\u30a4\u30c6\u30e0 ${i}: \u6709\u52b9\u306aJSON\u69cb\u9020\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093`);\n      \n      // \u624b\u52d5\u3067\u30c7\u30fc\u30bf\u3092\u62bd\u51fa\u3092\u8a66\u884c\n      const manualExtract = extractDataManually(aiResponse);\n      if (manualExtract) {\n        results.push({ json: manualExtract });\n      }\n    }\n    \n  } catch (error) {\n    console.log(`\u30a2\u30a4\u30c6\u30e0 ${i}: \u51e6\u7406\u30a8\u30e9\u30fc - ${error.message}`);\n    \n    // \u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u3067\u3082\u30c7\u30d5\u30a9\u30eb\u30c8\u5024\u3067\u51e6\u7406\u3092\u7d99\u7d9a\n    results.push({\n      json: {\n        dishName: 'Error',\n        calories: 0,\n        error: error.message,\n        originalIndex: i,\n        timestamp: new Date().toISOString()\n      }\n    });\n  }\n}\n\n// \u624b\u52d5\u3067\u30c7\u30fc\u30bf\u3092\u62bd\u51fa\u3059\u308b\u95a2\u6570\nfunction extractDataManually(text) {\n  try {\n    const dishMatch = text.match(/\"dishName\"\\s*:\\s*\"([^\"]+)\"/i) || \n                     text.match(/\"dish_name\"\\s*:\\s*\"([^\"]+)\"/i) ||\n                     text.match(/\"name\"\\s*:\\s*\"([^\"]+)\"/i);\n    \n    const caloriesMatch = text.match(/\"calories\"\\s*:\\s*\"?(\\d+)\"?/i) ||\n                         text.match(/\"calorie\"\\s*:\\s*\"?(\\d+)\"?/i) ||\n                         text.match(/\"cal\"\\s*:\\s*\"?(\\d+)\"?/i);\n    \n    if (dishMatch || caloriesMatch) {\n      return {\n        dishName: dishMatch ? dishMatch[1] : 'Unknown',\n        calories: caloriesMatch ? parseInt(caloriesMatch[1]) : 0,\n        extractedManually: true,\n        timestamp: new Date().toISOString()\n      };\n    }\n  } catch (error) {\n    console.log('\u624b\u52d5\u62bd\u51fa\u30a8\u30e9\u30fc:', error.message);\n  }\n  return null;\n}\n\n// \u7d50\u679c\u304c\u7a7a\u306e\u5834\u5408\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u51e6\u7406\nif (results.length === 0) {\n  console.log('\u51e6\u7406\u53ef\u80fd\u306a\u30c7\u30fc\u30bf\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f');\n  results.push({\n    json: {\n      dishName: 'No Data',\n      calories: 0,\n      error: 'No valid data found',\n      timestamp: new Date().toISOString()\n    }\n  });\n}\n\nconsole.log(`\u51e6\u7406\u5b8c\u4e86: ${results.length} \u4ef6\u306e\u30a2\u30a4\u30c6\u30e0\u3092\u5909\u63db\u3057\u307e\u3057\u305f`);\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "bff30c01-da43-463d-940e-b396d8237c61",
      "name": "Append row in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        608,
        608
      ],
      "parameters": {
        "columns": {
          "value": {
            "cal": "={{ $json.calories }}",
            "date": "={{ $now.format('yyyy-MM-dd') }}",
            "menu": "={{ $json.dishName }}"
          },
          "schema": [
            {
              "id": "date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "menu",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "menu",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "cal",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "cal",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1178520345,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1upzLlUvmXYgALLtUR6WEHUtxh064mDNrBVM-b3Of_vg/edit#gid=1178520345",
          "cachedResultName": "11_test"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1upzLlUvmXYgALLtUR6WEHUtxh064mDNrBVM-b3Of_vg",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1upzLlUvmXYgALLtUR6WEHUtxh064mDNrBVM-b3Of_vg/edit?usp=drivesdk",
          "cachedResultName": "kote2 n8n sheet"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "1bf7ef0f-1e8d-4e61-b3bd-aaaefcf15366",
      "name": "Code1",
      "type": "n8n-nodes-base.code",
      "position": [
        352,
        272
      ],
      "parameters": {
        "jsCode": "// n8n Code node - AI\u30a8\u30fc\u30b8\u30a7\u30f3\u30c8\u304b\u3089\u306eJSON\u6587\u5b57\u5217\u3092LINE\u30e1\u30c3\u30bb\u30fc\u30b8\u306b\u5909\u63db\nconst inputData = $input.all();\nconst results = [];\n\nfor (let i = 0; i < inputData.length; i++) {\n  try {\n    // AI\u30a8\u30fc\u30b8\u30a7\u30f3\u30c8\u304b\u3089\u306e\u51fa\u529b\u3092\u53d6\u5f97\n    let aiResponse = inputData[i].json.output || inputData[i].json.text || inputData[i].json.content || '';\n    \n    // \u7a7a\u306e\u5834\u5408\u306f\u30b9\u30ad\u30c3\u30d7\n    if (!aiResponse) {\n      console.log(`\u30a2\u30a4\u30c6\u30e0 ${i}: \u7a7a\u306e\u5fdc\u7b54\u3092\u30b9\u30ad\u30c3\u30d7`);\n      continue;\n    }\n    \n    // \u6587\u5b57\u5217\u306e\u524d\u5f8c\u306e\u7a7a\u767d\u3092\u524a\u9664\n    aiResponse = aiResponse.trim();\n    \n    // JSON\u30c7\u30fc\u30bf\u3092\u62bd\u51fa\u3059\u308b\n    const menuItems = extractMenuData(aiResponse);\n    \n    if (menuItems && menuItems.length > 0) {\n      // LINE\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u751f\u6210\n      const lineMessage = generateLineMessage(menuItems);\n      \n      results.push({\n        json: {\n          message: lineMessage,\n          menuCount: menuItems.length,\n          totalCalories: menuItems.reduce((sum, item) => sum + item.calories, 0),\n          timestamp: new Date().toISOString(),\n          originalIndex: i\n        }\n      });\n      \n      console.log(`\u30a2\u30a4\u30c6\u30e0 ${i}: ${menuItems.length}\u500b\u306e\u30e1\u30cb\u30e5\u30fc\u3092\u51e6\u7406\u3057\u307e\u3057\u305f`);\n    } else {\n      // \u98df\u54c1\u304c\u691c\u51fa\u3055\u308c\u306a\u304b\u3063\u305f\u5834\u5408\n      results.push({\n        json: {\n          message: \"\u753b\u50cf\u304b\u3089\u98df\u54c1\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\",\n          menuCount: 0,\n          totalCalories: 0,\n          timestamp: new Date().toISOString(),\n          originalIndex: i\n        }\n      });\n      \n      console.log(`\u30a2\u30a4\u30c6\u30e0 ${i}: \u98df\u54c1\u304c\u691c\u51fa\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f`);\n    }\n    \n  } catch (error) {\n    console.log(`\u30a2\u30a4\u30c6\u30e0 ${i}: \u51e6\u7406\u30a8\u30e9\u30fc - ${error.message}`);\n    \n    // \u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u30e1\u30c3\u30bb\u30fc\u30b8\n    results.push({\n      json: {\n        message: \"\u753b\u50cf\u306e\u89e3\u6790\u3067\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002\",\n        menuCount: 0,\n        totalCalories: 0,\n        error: error.message,\n        timestamp: new Date().toISOString(),\n        originalIndex: i\n      }\n    });\n  }\n}\n\n// JSON\u30c7\u30fc\u30bf\u3092\u62bd\u51fa\u3059\u308b\u95a2\u6570\nfunction extractMenuData(text) {\n  const menuItems = [];\n  \n  try {\n    // JSON\u306e\u524d\u5f8c\u306b\u3042\u308b\u4e0d\u8981\u306a\u30c6\u30ad\u30b9\u30c8\u3092\u524a\u9664\n    let cleanText = text;\n    \n    // ```json \u3084 ``` \u3067\u56f2\u307e\u308c\u3066\u3044\u308b\u5834\u5408\u306e\u51e6\u7406\n    if (cleanText.includes('```json')) {\n      const jsonStart = cleanText.indexOf('```json') + 7;\n      const jsonEnd = cleanText.indexOf('```', jsonStart);\n      if (jsonEnd !== -1) {\n        cleanText = cleanText.substring(jsonStart, jsonEnd).trim();\n      }\n    } else if (cleanText.includes('```')) {\n      const jsonStart = cleanText.indexOf('```') + 3;\n      const jsonEnd = cleanText.indexOf('```', jsonStart);\n      if (jsonEnd !== -1) {\n        cleanText = cleanText.substring(jsonStart, jsonEnd).trim();\n      }\n    }\n    \n    // JSON\u306e\u958b\u59cb\u3068\u7d42\u4e86\u3092\u898b\u3064\u3051\u308b\n    const jsonStartIndex = cleanText.indexOf('[');\n    const jsonEndIndex = cleanText.lastIndexOf(']');\n    \n    if (jsonStartIndex !== -1 && jsonEndIndex !== -1 && jsonEndIndex > jsonStartIndex) {\n      const jsonString = cleanText.substring(jsonStartIndex, jsonEndIndex + 1);\n      \n      try {\n        // JSON\u3092\u30d1\u30fc\u30b9\n        const parsedData = JSON.parse(jsonString);\n        \n        // \u914d\u5217\u3067\u306a\u3044\u5834\u5408\u306f\u914d\u5217\u306b\u5909\u63db\n        const dataArray = Array.isArray(parsedData) ? parsedData : [parsedData];\n        \n        // \u5404\u30a2\u30a4\u30c6\u30e0\u3092\u51e6\u7406\n        dataArray.forEach((item) => {\n          if (item && typeof item === 'object') {\n            const dishName = item.dishName || item.dish_name || item.name || 'Unknown';\n            let calories = item.calories || item.calorie || item.cal || 0;\n            \n            // \u30ab\u30ed\u30ea\u30fc\u304c\u6587\u5b57\u5217\u306e\u5834\u5408\u306f\u6570\u5024\u306b\u5909\u63db\n            if (typeof calories === 'string') {\n              const numericCalories = parseInt(calories.replace(/[^\\d]/g, ''));\n              calories = isNaN(numericCalories) ? 0 : numericCalories;\n            }\n            \n            // \u6709\u52b9\u306a\u30c7\u30fc\u30bf\u306e\u307f\u8ffd\u52a0\n            if (dishName !== 'Unknown' && calories > 0) {\n              menuItems.push({\n                dishName: dishName,\n                calories: calories\n              });\n            }\n          }\n        });\n        \n      } catch (parseError) {\n        console.log('JSON\u89e3\u6790\u30a8\u30e9\u30fc:', parseError.message);\n        // \u624b\u52d5\u3067\u30c7\u30fc\u30bf\u3092\u62bd\u51fa\u3092\u8a66\u884c\n        const manualExtract = extractDataManually(text);\n        if (manualExtract) {\n          menuItems.push(manualExtract);\n        }\n      }\n    } else {\n      // \u624b\u52d5\u3067\u30c7\u30fc\u30bf\u3092\u62bd\u51fa\u3092\u8a66\u884c\n      const manualExtract = extractDataManually(text);\n      if (manualExtract) {\n        menuItems.push(manualExtract);\n      }\n    }\n    \n  } catch (error) {\n    console.log('\u30c7\u30fc\u30bf\u62bd\u51fa\u30a8\u30e9\u30fc:', error.message);\n  }\n  \n  return menuItems;\n}\n\n// \u624b\u52d5\u3067\u30c7\u30fc\u30bf\u3092\u62bd\u51fa\u3059\u308b\u95a2\u6570\nfunction extractDataManually(text) {\n  try {\n    const dishMatch = text.match(/\"dishName\"\\s*:\\s*\"([^\"]+)\"/i) || \n                     text.match(/\"dish_name\"\\s*:\\s*\"([^\"]+)\"/i) ||\n                     text.match(/\"name\"\\s*:\\s*\"([^\"]+)\"/i);\n    \n    const caloriesMatch = text.match(/\"calories\"\\s*:\\s*\"?(\\d+)\"?/i) ||\n                         text.match(/\"calorie\"\\s*:\\s*\"?(\\d+)\"?/i) ||\n                         text.match(/\"cal\"\\s*:\\s*\"?(\\d+)\"?/i);\n    \n    if (dishMatch && caloriesMatch) {\n      const calories = parseInt(caloriesMatch[1]);\n      if (!isNaN(calories) && calories > 0) {\n        return {\n          dishName: dishMatch[1],\n          calories: calories\n        };\n      }\n    }\n  } catch (error) {\n    console.log('\u624b\u52d5\u62bd\u51fa\u30a8\u30e9\u30fc:', error.message);\n  }\n  return null;\n}\n\n// LINE\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u751f\u6210\u3059\u308b\u95a2\u6570\nfunction generateLineMessage(menuItems) {\n  if (!menuItems || menuItems.length === 0) {\n    return \"\u753b\u50cf\u304b\u3089\u98df\u54c1\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\";\n  }\n  \n  let message = \"\u30ab\u30ed\u30ea\u30fc\u8a08\u7b97\uff1a\\n\";\n  \n  // \u5404\u30e1\u30cb\u30e5\u30fc\u30a2\u30a4\u30c6\u30e0\u3092\u8ffd\u52a0\n  menuItems.forEach((item) => {\n    message += `\u30fb${item.dishName}(${item.calories}kcal)\\n`;\n  });\n  \n  message += \"\u30b7\u30fc\u30c8\u306b\u8a18\u5165\u3057\u307e\u3057\u305f\";\n  \n  return message;\n}\n\n// \u7d50\u679c\u304c\u7a7a\u306e\u5834\u5408\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u51e6\u7406\nif (results.length === 0) {\n  console.log('\u51e6\u7406\u53ef\u80fd\u306a\u30c7\u30fc\u30bf\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f');\n  results.push({\n    json: {\n      message: \"\u753b\u50cf\u306e\u51e6\u7406\u3067\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002\",\n      menuCount: 0,\n      totalCalories: 0,\n      timestamp: new Date().toISOString()\n    }\n  });\n}\n\nconsole.log(`LINE \u30e1\u30c3\u30bb\u30fc\u30b8\u751f\u6210\u5b8c\u4e86: ${results.length} \u4ef6\u306e\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u751f\u6210\u3057\u307e\u3057\u305f`);\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "56bfb625-6b6b-480d-a81d-55ad8a629c57",
      "name": "LINE webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1040,
        144
      ],
      "parameters": {
        "path": "0980b700-284e-4e52-a33e-71e087eea37f",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "9e59d336-a880-42aa-b69c-66b445208d1c",
      "name": "user verification",
      "type": "n8n-nodes-base.if",
      "position": [
        -800,
        144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "28c9bc55-ef1b-4fee-8c22-a48b88a82c34",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.body.events[0].source.userId }}",
              "rightValue": "{your id}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "11ddcfb0-a2e8-4002-ba64-bff77a4bf066",
      "name": "images download",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -368,
        272
      ],
      "parameters": {
        "url": "=https://api-data.line.me/v2/bot/message/{{ $json.body.events[0].message.id }}/content",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer {channel access toaken}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "b40c936c-5273-493a-822f-36b44de906b3",
      "name": "only message",
      "type": "n8n-nodes-base.set",
      "position": [
        -128,
        -160
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "8504cdbe-9904-4e83-aee9-362f02a25014",
              "name": "text",
              "type": "string",
              "value": "={{ $json.body.events[0].message.text }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "599b110f-7771-4497-a41f-b231c3ebc3b8",
      "name": "send LINE",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1184,
        128
      ],
      "parameters": {
        "url": "https://api.line.me/v2/bot/message/reply",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"replyToken\": \"{{ $('LINE webhook').item.json.body.events[0].replyToken }}\",\n  \"messages\": [\n    {\n    \"type\": \"text\",\n    \"text\":{{ JSON.stringify($json.output) }}\n    }\n  ]\n} ",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "978b63a0-fab0-445f-8f1b-0ab3b92f22b1",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1504,
        -64
      ],
      "parameters": {
        "width": 368,
        "height": 560,
        "content": "## LINE Food Image Calorie Logger to Google Sheets\n\nThis workflow allows a LINE user to send either text or an image of food to a connected LINE bot.\n\n- **Text message** \u2192 The AI agent responds directly via LINE.  \n- **Image message** \u2192 The workflow downloads it from LINE\u2019s API, analyzes it using OpenAI\u2019s Vision model, estimates calories **only if the image contains food**, and formats the result into JSON.  \n- Detected dishes and calories are appended to a Google Sheet, and a confirmation message is sent back to the user via LINE.\n\n### Key Features\n- Integrates **LINE Messaging API** webhook with n8n\n- Uses **OpenAI Vision** to detect food and estimate calories\n- Automatically logs results into **Google Sheets**\n- Sends **real-time feedback** to the LINE user\n\n### How to Use\n1. Set up a LINE Messaging API channel and get your **channel access token**.\n2. Add your **OpenAI API credentials** in n8n.\n3. Replace placeholders for `{channel access token}`, `{your id}`, and **Google Sheet IDs** with your own.\n4. Activate the workflow and send a food image or text message to your LINE bot.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "78855553-f504-4ee0-913e-1b1c933f0cae",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        192
      ],
      "parameters": {
        "color": 4,
        "width": 704,
        "height": 272,
        "content": "## message to LINE"
      },
      "typeVersion": 1
    },
    {
      "id": "e2180426-a161-4483-a630-63eb54cf0d29",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        512
      ],
      "parameters": {
        "color": 3,
        "width": 704,
        "height": 272,
        "content": "## write to Google Sheets"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "a7eb22fe-627c-4d1a-aaee-a05a9fb4eb81",
  "connections": {
    "Code": {
      "main": [
        [
          {
            "node": "Append row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code1": {
      "main": [
        [
          {
            "node": "Edit Fields1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch": {
      "main": [
        [
          {
            "node": "only message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "images download",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "send LINE",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields1": {
      "main": [
        [
          {
            "node": "send LINE",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LINE webhook": {
      "main": [
        [
          {
            "node": "user verification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "only message": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze image": {
      "main": [
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          },
          {
            "node": "Code1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "images download": {
      "main": [
        [
          {
            "node": "Analyze image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "user verification": {
      "main": [
        [
          {
            "node": "Switch",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow allows a LINE user to send either text or an image of food to a connected LINE bot.

Source: https://n8n.io/workflows/7228/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Universal Expense tracker. Uses telegram, httpRequest, openAi, googleSheets. Webhook trigger; 33 nodes.

Telegram, HTTP Request, OpenAI +7
AI & RAG

This n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La

Google Sheets, HTTP Request, Slack +10
AI & RAG

My workflow 15. Uses httpRequest, memoryBufferWindow, agent, lmChatOpenAi. Webhook trigger; 74 nodes.

HTTP Request, Memory Buffer Window, Agent +5
AI & RAG

leads. Uses supabase, gmail, formTrigger, httpRequest. Webhook trigger; 62 nodes.

Supabase, Gmail, Form Trigger +13
AI & RAG

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.

OpenAI Chat, Memory Buffer Window, Output Parser Structured +11