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.
Pitch Paul. Uses lmChatOpenRouter, telegram, outputParserStructured, supabaseTool. Event-driven trigger; 33 nodes.
Arvifund - Supabase (Fixed v6). Uses httpRequest, telegram, supabase, telegramTrigger. Event-driven trigger; 92 nodes.
Arvifund - Supabase (Fixed v5). Uses httpRequest, telegram, googleSheets, telegramTrigger. Event-driven trigger; 91 nodes.
Arvifund - Supabase. Uses httpRequest, telegram, googleSheets, telegramTrigger. Event-driven trigger; 90 nodes.