This workflow corresponds to n8n.io template #9619 — we link there as the canonical source.
This workflow follows the Agent → Gmail 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": "aZU2RbdXlp3eXCZh",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Infer Ingredients from a Food Image and Estimate Calories",
"tags": [],
"nodes": [
{
"id": "agent1",
"name": "AI Agent - \u98df\u6750\u5206\u6790",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
752,
400
],
"parameters": {
"text": "=\u3042\u306a\u305f\u306f\u6599\u7406\u3068\u6804\u990a\u306e\u5c02\u9580\u5bb6\u3067\u3059\u3002\u63d0\u4f9b\u3055\u308c\u305f\u6599\u7406\u306e\u753b\u50cf\u3092\u5206\u6790\u3057\u3066\u3001\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u65e5\u672c\u8a9e\u3067\u63d0\u4f9b\u3057\u3066\u304f\u3060\u3055\u3044\uff1a\n\n1. \u6599\u7406\u540d\n2. \u63a8\u5b9a\u3055\u308c\u308b\u4e3b\u306a\u98df\u6750\u3068\u305d\u306e\u5206\u91cf\uff08\u30b0\u30e9\u30e0\uff09\n3. \u5404\u98df\u6750\u306e\u30ab\u30ed\u30ea\u30fc\n4. \u5408\u8a08\u63a8\u5b9a\u30ab\u30ed\u30ea\u30fc\n5. \u6804\u990a\u30d0\u30e9\u30f3\u30b9\u306e\u7c21\u5358\u306a\u8a55\u4fa1\n\n\u753b\u50cfURL: {{ $json.message.photo[0].file_id }}",
"options": {
"systemMessage": "\u4ee5\u4e0b\u306e\u6307\u793a\u306b\u5f93\u3063\u3066\u3001\u30e6\u30fc\u30b6\u30fc\u304c\u5165\u529b\u3057\u305f\u6599\u7406\u306e\u60c5\u5831\u3092\u5206\u6790\u3057\u3001JSON\u5f62\u5f0f\u3067\u51fa\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n1. **\u5f79\u5272:** \u6599\u7406\u30ec\u30b7\u30d4\u30a2\u30ca\u30e9\u30a4\u30b6\u30fc\n2. **\u5165\u529b:** \u30e6\u30fc\u30b6\u30fc\u304b\u3089\u306e\u6599\u7406\u540d\u3084\u98df\u6750\u306b\u95a2\u3059\u308b\u30c6\u30ad\u30b9\u30c8\n3. **\u51e6\u7406:**\n * \u6599\u7406\u540d\u3092\u7279\u5b9a\u3059\u308b\u3002\n * \u98df\u6750\u30ea\u30b9\u30c8\u3092\u62bd\u51fa\u3057\u3001\u5404\u98df\u6750\u306e\u300c\u540d\u524d(name)\u300d\u300c\u5206\u91cf(amount)\u300d\u300c\u30ab\u30ed\u30ea\u30fc(calories)\u300d\u3092\u7279\u5b9a\u3059\u308b\u3002\u5206\u91cf\u3068\u30ab\u30ed\u30ea\u30fc\u306f\u4e00\u822c\u7684\u306a\u5024\u3092\u63a8\u5b9a\u3057\u3066\u3082\u3088\u3044\u3002\n * \u5168\u98df\u6750\u306e\u30ab\u30ed\u30ea\u30fc\u3092\u5408\u8a08\u3057\u3001\u300c\u5408\u8a08\u30ab\u30ed\u30ea\u30fc(totalCalories)\u300d\u3092\u8a08\u7b97\u3059\u308b\u3002\n * \u8a08\u7b97\u7d50\u679c\u306b\u57fa\u3065\u304d\u3001\u300c\u6804\u990a\u30d0\u30e9\u30f3\u30b9\u306e\u8a55\u4fa1(nutritionEvaluation)\u300d\u3092\u4f5c\u6210\u3059\u308b\u3002\n4. **\u51fa\u529b:** \u4ee5\u4e0b\u306eJSON\u30b9\u30ad\u30fc\u30de\u306b\u53b3\u5bc6\u306b\u5f93\u3063\u305fJSON\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u306e\u307f\u3092\u51fa\u529b\u3059\u308b\u3002\n * `dishName`: string\n * `ingredients`: array of objects\n * `name`: string\n * `amount`: number (\u5358\u4f4d: g)\n * `calories`: number (\u5358\u4f4d: kcal)\n * `totalCalories`: number (\u5358\u4f4d: kcal)\n * `nutritionEvaluation`: string\n5. **\u6ce8\u610f:**\n * \u51fa\u529b\u306bJSON\u4ee5\u5916\u306e\u30c6\u30ad\u30b9\u30c8\uff08\u6328\u62f6\u3001\u8aac\u660e\u306a\u3069\uff09\u3092\u542b\u3081\u306a\u3044\u3067\u304f\u3060\u3055\u3044\u3002\n * JSON\u306e\u30ad\u30fc\u540d\u306f\u6307\u5b9a\u901a\u308a\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 2
},
{
"id": "parser1",
"name": "Structured Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
912,
608
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"dishName\": {\n \"type\": \"string\",\n \"description\": \"\u6599\u7406\u540d\"\n },\n \"ingredients\": {\n \"type\": \"array\",\n \"description\": \"\u98df\u6750\u30ea\u30b9\u30c8\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"\u98df\u6750\u540d\"\n },\n \"amount\": {\n \"type\": \"number\",\n \"description\": \"\u5206\u91cf\uff08\u30b0\u30e9\u30e0\uff09\"\n },\n \"calories\": {\n \"type\": \"number\",\n \"description\": \"\u30ab\u30ed\u30ea\u30fc\uff08kcal\uff09\"\n }\n }\n }\n },\n \"totalCalories\": {\n \"type\": \"number\",\n \"description\": \"\u5408\u8a08\u30ab\u30ed\u30ea\u30fc\uff08kcal\uff09\"\n },\n \"nutritionEvaluation\": {\n \"type\": \"string\",\n \"description\": \"\u6804\u990a\u30d0\u30e9\u30f3\u30b9\u306e\u8a55\u4fa1\"\n }\n },\n \"required\": [\"dishName\", \"ingredients\", \"totalCalories\", \"nutritionEvaluation\"]\n}"
},
"typeVersion": 1.2
},
{
"id": "52bb2509-5870-4f4c-96ea-c607548b15b1",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"position": [
480,
400
],
"parameters": {
"updates": [
"message"
],
"additionalFields": {
"download": true
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "a8ee11fd-47bc-4524-89a5-a19380041613",
"name": "OpenRouter Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"position": [
608,
608
],
"parameters": {
"model": "openai/gpt-5-mini",
"options": {
"temperature": 0.3
}
},
"credentials": {
"openRouterApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "7397c3e2-c057-4f6b-9afa-010b0a610789",
"name": "Format for Gmail",
"type": "n8n-nodes-base.code",
"position": [
1152,
400
],
"parameters": {
"jsCode": "// AI Agent\u304b\u3089\u51fa\u529b\u3055\u308c\u305fJSON\u5f62\u5f0f\u306e\u6599\u7406\u5206\u6790\u7d50\u679c\u3092\u53d6\u5f97\u3057\u307e\u3059\u3002\nconst analysisResult = $input.first().json.output;\n\n// \u7d50\u679c\u304c\u5b58\u5728\u3057\u306a\u3044\u3001\u307e\u305f\u306f\u671f\u5f85\u3057\u305f\u5f62\u5f0f\u3067\u306a\u3044\u5834\u5408\u306f\u30a8\u30e9\u30fc\u3092\u9632\u3050\u305f\u3081\u306e\u51e6\u7406\nif (!analysisResult || !analysisResult.dishName) {\n return [{\n json: {\n subject: \"\u6599\u7406\u5206\u6790\u30a8\u30e9\u30fc\",\n htmlBody: \"<p>AI\u306b\u3088\u308b\u5206\u6790\u7d50\u679c\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002</p>\"\n }\n }];\n}\n\n// 1. \u30e1\u30fc\u30eb\u306e\u4ef6\u540d\u3092\u4f5c\u6210\nconst subject = `\u3010\u6599\u7406\u5206\u6790\u30ec\u30dd\u30fc\u30c8\u3011 ${analysisResult.dishName}`;\n\n// 2. HTML\u30e1\u30fc\u30eb\u306e\u672c\u6587\u3092\u751f\u6210\nlet htmlBody = `\n<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <style>\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; background-color: #f9f9f9; margin: 0; padding: 0; }\n .container { max-width: 600px; margin: 20px auto; padding: 25px; background-color: #ffffff; border: 1px solid #e0e0e0; border-radius: 12px; box-shadow: 0 4px 8px rgba(0,0,0,0.05); }\n h1 { font-size: 26px; color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; margin-top: 0; }\n h2 { font-size: 20px; color: #e74c3c; margin-bottom: 15px; }\n h3 { font-size: 18px; color: #34495e; margin-top: 30px; border-bottom: 1px solid #eeeeee; padding-bottom: 8px;}\n p { margin-top: 0; color: #555; }\n table { width: 100%; border-collapse: collapse; margin-top: 15px; }\n th, td { text-align: left; padding: 12px; border-bottom: 1px solid #dddddd; }\n th { background-color: #f2f2f2; color: #333; }\n tr:last-child td { border-bottom: none; }\n .total-calories { text-align: center; background-color: #fff3cd; padding: 15px; border-radius: 8px; margin-top: 20px; }\n .evaluation { background-color: #eafaf1; padding: 15px; border-left: 5px solid #2ecc71; border-radius: 5px; margin-top: 20px;}\n .footer { text-align: center; margin-top: 25px; font-size: 12px; color: #999; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>${analysisResult.dishName}</h1>\n \n <div class=\"total-calories\">\n <h2>\u5408\u8a08\u63a8\u5b9a\u30ab\u30ed\u30ea\u30fc: ${analysisResult.totalCalories} kcal</h2>\n </div>\n\n <h3>\u8a73\u7d30\u306a\u98df\u6750\u30ea\u30b9\u30c8</h3>\n <table>\n <thead>\n <tr>\n <th>\u98df\u6750\u540d</th>\n <th>\u5206\u91cf (g)</th>\n <th>\u30ab\u30ed\u30ea\u30fc (kcal)</th>\n </tr>\n </thead>\n <tbody>\n`;\n\n// \u5404\u98df\u6750\u306e\u60c5\u5831\u3092\u30c6\u30fc\u30d6\u30eb\u306e\u884c\u3068\u3057\u3066\u8ffd\u52a0\nif (analysisResult.ingredients && analysisResult.ingredients.length > 0) {\n analysisResult.ingredients.forEach(ingredient => {\n htmlBody += `\n <tr>\n <td>${ingredient.name}</td>\n <td>${ingredient.amount}</td>\n <td>${ingredient.calories}</td>\n </tr>\n `;\n });\n}\n\nhtmlBody += `\n </tbody>\n </table>\n\n <h3>\u6804\u990a\u30d0\u30e9\u30f3\u30b9\u306e\u8a55\u4fa1</h3>\n <div class=\"evaluation\">\n <p>${analysisResult.nutritionEvaluation}</p>\n </div>\n\n <div class=\"footer\">\n <p>\u3053\u306e\u30ec\u30dd\u30fc\u30c8\u306fAI\u306b\u3088\u3063\u3066\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3057\u305f\u3002</p>\n </div>\n </div>\n</body>\n</html>\n`;\n\n// 3. \u5f8c\u7d9a\u306eGmail\u30ce\u30fc\u30c9\u3067\u4f7f\u3048\u308b\u3088\u3046\u306b\u30c7\u30fc\u30bf\u3092\u8fd4\u3059\nreturn [{\n json: {\n subject: subject,\n htmlBody: htmlBody\n }\n}];"
},
"typeVersion": 2
},
{
"id": "ae0d956b-599d-4bba-b14b-67245e3287fb",
"name": "Send a message1",
"type": "n8n-nodes-base.gmail",
"position": [
1408,
400
],
"parameters": {
"sendTo": "user@example.com",
"message": "={{ $json.htmlBody }}",
"options": {
"appendAttribution": false
},
"subject": "={{ $json.subject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "9be8d7fb-d689-41bb-b638-755bddd7a618",
"name": "Sticky 1: Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-16,
80
],
"parameters": {
"color": 5,
"width": 520,
"height": 260,
"content": "## \ud83d\uddd2\ufe0f Sticky 1: Overview\n**What this template does**\n- Accepts a **food image** and runs **AI-based ingredient inference** \u2192 **calorie estimation** \u2192 **concise summary**\n- Input can be an **image URL** or **Base64 image** (via Webhook or a messaging app)\n\n**Why it\u2019s useful**\n- Quickly generates a calorie rough estimate and a short nutrition comment for logging, sharing, or feedback loops\n\n**No hardcoded secrets**\n- All API keys must be set via **Credentials/Environment Variables** (never hardcode keys in nodes)"
},
"typeVersion": 1
},
{
"id": "0bd3d0eb-a030-4d79-a390-17e41d705f7e",
"name": "Sticky 2: Prerequisites",
"type": "n8n-nodes-base.stickyNote",
"position": [
608,
-16
],
"parameters": {
"color": 4,
"width": 520,
"height": 280,
"content": "## \ud83d\uddd2\ufe0f Sticky 2: Prerequisites & Credentials\n**Connections**\n- LLM (OpenAI or compatible) with **vision** capability\n- One input channel (Webhook **or** Telegram/LINE, etc.)\n\n**Environment Variables (examples)**\n- `LLM_MODEL` (e.g., `gpt-4o` or any vision-capable model)\n- `LLM_TEMPERATURE` (e.g., `0.3`)\n- `WEBHOOK_SECRET` (optional, if you validate requests)\n\n**Scopes / Permissions**\n- If image URLs are external, ensure they are publicly accessible or use signed/temporary URLs"
},
"typeVersion": 1
},
{
"id": "ff6d1495-3223-4305-a354-57a22adf71f3",
"name": "Sticky 3: Input Format",
"type": "n8n-nodes-base.stickyNote",
"position": [
-144,
560
],
"parameters": {
"color": 2,
"width": 520,
"height": 300,
"content": "## \ud83d\uddd2\ufe0f Sticky 3: Input Format (Sample)\n**Expected JSON**\n```json\n{\n \"imageUrl\": \"https://example.com/dish.jpg\"\n // or\n // \"imageBase64\": \"data:image/jpeg;base64,....\"\n}\n```\n**Notes**\n- Template assumes **one image** per request (extend with arrays for multiple images)\n- If you receive platform-specific payloads (e.g., Telegram `file_id`, LINE message objects), **normalize** them first to `imageUrl` or `imageBase64` before calling the LLM"
},
"typeVersion": 1
},
{
"id": "1bd2ee7d-a5f9-42f4-9974-4698fb05b553",
"name": "Sticky 4: Model & Prompt",
"type": "n8n-nodes-base.stickyNote",
"position": [
528,
864
],
"parameters": {
"color": 3,
"width": 520,
"height": 260,
"content": "## \ud83d\uddd2\ufe0f Sticky 4: Model & Prompt Policy\n**Model**\n- Use a **vision-capable** LLM (e.g., `gpt-4o`)\n- Keep `temperature` low (0.2\u20130.3) for stable outputs\n\n**Prompt Rules**\n- The model must return **strict JSON only**, with the fields below (no extra text)\n- Keep names concise (e.g., \"grilled chicken\", \"white rice\", \"mixed salad\")\n- Provide amounts in grams when possible (estimates are fine)"
},
"typeVersion": 1
},
{
"id": "84f0f90c-e819-4022-a76e-b63353ca9968",
"name": "Sticky 5: Output Schema",
"type": "n8n-nodes-base.stickyNote",
"position": [
1184,
-16
],
"parameters": {
"width": 520,
"height": 280,
"content": "## \ud83d\uddd2\ufe0f Sticky 5: Output Schema (Fixed)\n**JSON structure**\n```json\n{\n \"dishName\": \"string\",\n \"ingredients\": [\n { \"name\": \"string\", \"amount\": 0, \"calories\": 0 }\n ],\n \"totalCalories\": 0,\n \"nutritionEvaluation\": \"string\"\n}\n```\n**Validation**\n- If fields are empty/missing, route to an **error branch** and return a helpful message for the user"
},
"typeVersion": 1
},
{
"id": "fe6e558b-4f6a-4c0a-9b06-e3939b07be3f",
"name": "Sticky 6: Test Steps",
"type": "n8n-nodes-base.stickyNote",
"position": [
1200,
816
],
"parameters": {
"color": 5,
"width": 520,
"height": 220,
"content": "## \ud83d\uddd2\ufe0f Sticky 6: Test Steps (Under 1 Minute)\n1) Turn on the **Webhook** node and copy the **Test URL**\n2) Send a sample payload with a valid `imageUrl`\n3) Confirm the run returns the **strict JSON** structure\n4) If using Telegram/Slack replies, authenticate and send a test message to verify delivery"
},
"typeVersion": 1
},
{
"id": "24bbd216-9c8b-4a4f-b446-dd82585117fd",
"name": "Sticky 7: Errors & Limits",
"type": "n8n-nodes-base.stickyNote",
"position": [
1696,
0
],
"parameters": {
"color": 4,
"width": 520,
"height": 260,
"content": "## \ud83d\uddd2\ufe0f Sticky 7: Error Handling & Limits\n**Retries**\n- For LLM calls, implement **exponential backoff** on 429/5xx\n- If the image fetch fails, **retry**, then go to **error branch**\n\n**Limits**\n- Watch for model **context/vision limits**; downscale/thumbnail images if needed\n- When rate-limited, use **Delay** + **Retry** to recover gracefully"
},
"typeVersion": 1
},
{
"id": "55619913-3b70-43d8-ab38-f5b8eb4a6375",
"name": "Sticky 8: Security",
"type": "n8n-nodes-base.stickyNote",
"position": [
1776,
784
],
"parameters": {
"color": 3,
"width": 520,
"height": 220,
"content": "## \ud83d\uddd2\ufe0f Sticky 8: Security & PII\n- Store API keys/tokens in **Credentials/Env Vars** (never in node fields)\n- If image URLs are private, use **signed/temporary URLs**\n- **Do not log** personal data; mask/remove PII from execution logs\n- Use **HTTPS only** for all external requests"
},
"typeVersion": 1
},
{
"id": "95833663-f776-40c5-9225-30f07e580acb",
"name": "Sticky 9: Delivery",
"type": "n8n-nodes-base.stickyNote",
"position": [
2272,
0
],
"parameters": {
"color": 2,
"width": 520,
"height": 220,
"content": "## \ud83d\uddd2\ufe0f Sticky 9: Delivery Options (No Gmail)\n- Reply via **Telegram/Slack** or return via **HTTP Response**\n- Optionally log results to **Google Sheets** or **Notion** for history/analytics\n- If email is required, use **SMTP** or a non-Gmail provider (per your stack)"
},
"typeVersion": 1
},
{
"id": "d1f31505-89ed-4483-bb52-d1f98c02db36",
"name": "Sticky 10: Extensibility",
"type": "n8n-nodes-base.stickyNote",
"position": [
1840,
400
],
"parameters": {
"width": 520,
"height": 240,
"content": "## \ud83d\uddd2\ufe0f Sticky 10: Extensibility (Review Bonus)\n- **Multilingual output** (ja/en) switch\n- Normalize serving units (piece/slice) \u2192 **gram conversion**\n- Categorize components (main dish / side / sauce)\n- Add **evidence-based** nutrition comments (e.g., high fat/high carb) with short rationale"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"timezone": "Asia/Tokyo",
"errorWorkflow": "",
"executionOrder": "v1",
"saveManualExecutions": true,
"saveExecutionProgress": true,
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all"
},
"versionId": "8766f935-4d36-4d19-9f54-9179e0a59262",
"connections": {
"Format for Gmail": {
"main": [
[
{
"node": "Send a message1",
"type": "main",
"index": 0
}
]
]
},
"Telegram Trigger": {
"main": [
[
{
"node": "AI Agent - \u98df\u6750\u5206\u6790",
"type": "main",
"index": 0
}
]
]
},
"OpenRouter Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent - \u98df\u6750\u5206\u6790",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"AI Agent - \u98df\u6750\u5206\u6790": {
"main": [
[
{
"node": "Format for Gmail",
"type": "main",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "AI Agent - \u98df\u6750\u5206\u6790",
"type": "ai_outputParser",
"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.
gmailOAuth2openRouterApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Teams building health/fitness apps, coaches running check-ins in chat, and anyone who needs quick, structured nutrition insights from food photos—without manual logging.
Source: https://n8n.io/workflows/9619/ — 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.
Pitch Paul. Uses lmChatOpenRouter, telegram, outputParserStructured, supabaseTool. Event-driven trigger; 33 nodes.
RAG CHATBOT Main. Uses telegram, telegramTrigger, lmChatOpenAi, n8n-nodes-mcp. Event-driven trigger; 87 nodes.
Who is this for? Agencies, consultants, and service providers who conduct discovery calls and need to quickly turn conversations into professional proposals.
This workflow transforms your Telegram bot into an intelligent creative assistant. It can chat conversationally, fetch trending image prompts from PromptHero for inspiration, or perform a deep "remix"
leads. Uses supabase, gmail, formTrigger, httpRequest. Webhook trigger; 62 nodes.