AutomationFlowsAI & RAG › Telegram AI Nutrition Tracker with Google Sheets

Telegram AI Nutrition Tracker with Google Sheets

Original n8n title: Nutrition Tracker & Meal Logger with Telegram, Gemini AI and Google Sheets

ByJohn Alejandro SIlva @alejandro-silva on n8n.io

> AI-powered nutrition assistant for Telegram — log meals, set goals, and get personalized daily reports with Google Sheets integration.

Event trigger★★★★★ complexityAI-powered63 nodesTelegramGoogle GeminiGoogle Gemini ChatMemory Buffer WindowGoogle SheetsGoogle Sheets ToolAgentTool Workflow
AI & RAG Trigger: Event Nodes: 63 Complexity: ★★★★★ AI nodes: yes Added:

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

This workflow follows the Agent → Execute Workflow Trigger 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
{
  "meta": {
    "templateId": "7756",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "c142f5dd-fbeb-4b25-a0fe-f684546e6eff",
      "name": "Download Voice Message",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2144,
        1072
      ],
      "parameters": {
        "fileId": "={{ $('Telegram Trigger').item.json.message.voice.file_id }}",
        "resource": "file",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e15bb617-61da-4620-936d-15b0b5cf9c59",
      "name": "Input Message Router1",
      "type": "n8n-nodes-base.switch",
      "position": [
        1776,
        1136
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Text",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "fcb767ee-565e-4b56-a54e-6f97f739fc24",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    },
                    "leftValue": "={{ $('Telegram Trigger').item.json.message.text }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Voice Message",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "c1016c40-f8f2-4e08-8ec8-5cdb88f5c87a",
                    "operator": {
                      "type": "object",
                      "operation": "exists",
                      "singleValue": true
                    },
                    "leftValue": "={{ $('Telegram Trigger').item.json.message.voice }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Image",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "f8150ac7-eea4-4658-8da9-f7a1c88a471d",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    },
                    "leftValue": "={{ $('Telegram Trigger').item.json.message.photo[0].file_id }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "ignoreCase": false,
          "fallbackOutput": "extra",
          "allMatchingOutputs": true
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "68f15a08-cbe2-4ad9-9cfe-8b7a7c60787c",
      "name": "get_message (text)",
      "type": "n8n-nodes-base.set",
      "position": [
        2144,
        912
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "801ec600-22ad-4a94-a2b4-ae72eb271df0",
              "name": "message",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.text }}"
            },
            {
              "id": "263071fb-bcdf-42b0-bb46-71b75fa0bf2a",
              "name": "chat_id",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "6561a9a0-2d00-424d-ba86-8852dcb7e935",
      "name": "Download IMAGE",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2144,
        1248
      ],
      "parameters": {
        "fileId": "={{ $('Telegram Trigger').item.json.message.photo[3]?.file_id || $('Telegram Trigger').item.json.message.photo[2]?.file_id || $('Telegram Trigger').item.json.message.photo[1]?.file_id }}",
        "resource": "file",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "b975dcce-68d8-4e3d-b50e-330f3f63812d",
      "name": "Analyze image",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        2480,
        1248
      ],
      "parameters": {
        "text": "=You are a Nutrition Vision Assistant. Think like a food scientist and registered dietitian. Reason silently and do not reveal your steps. From a single food photo, identify the meal components, estimate portion weight in grams per component using geometric/visual cues, then compute total calories, protein, carbs, and fat.\n\nEstimation method (internal only; do not output these steps)\n\nIdentify components: list the main foods (e.g., chicken breast, white rice, mixed salad, sauce).\n\nChoose references: map each component to a standard reference food.\n\nEstimate volume/size: use visible objects for scale (plate \u2248 27 cm diameter, fork tines \u2248 3.5 cm, spoon bowl \u2248 5\u20136 cm). Approximate shapes (cuboid, cylinder, dome) to get volume in ml (\u2248 cm\u00b3).\n\nConvert to grams (densities, g/ml): meats 1.05; cooked rice 0.66; cooked pasta 0.60; potato/solid starchy veg 0.80; leafy salad 0.15; sauces creamy 1.00; oils 0.91. If the image clearly suggests deep-fried or glossy/oily coating, account for added oil.\n\nMacros & energy per 100 g (reference values):\n\nWhite rice, cooked: 130 kcal, P 2.7, C 28, F 0.3\n\nPasta, cooked: 131 kcal, P 5.0, C 25, F 1.1\n\nChicken breast, cooked skinless: 165 kcal, P 31, C 0, F 3.6\n\nSalmon, cooked: 208 kcal, P 20, C 0, F 13\n\nLean ground beef (\u224810% fat), cooked: 217 kcal, P 26, C 0, F 12\n\nBlack beans, cooked: 132 kcal, P 8.9, C 23.7, F 0.5\n\nPotato, baked: 93 kcal, P 2.5, C 21, F 0.1\n\nLettuce/leafy salad: 15 kcal, P 1.4, C 2.9, F 0.2\n\nAvocado: 160 kcal, P 2, C 9, F 15\n\nBread (white): 265 kcal, P 9, C 49, F 3.2\n\nEgg, cooked: 155 kcal, P 13, C 1.1, F 11\n\nCheddar cheese: 403 kcal, P 25, C 1.3, F 33\n\nOlive oil: 884 kcal, P 0, C 0, F 100\n(If a food is not listed, pick the closest standard equivalent.)\n\nHidden oil & sauces: if pan-fried or visibly glossy, add ~1 tablespoon oil = 13.5 g = 120 kcal = 13.5 g fat per clearly coated serving; adjust by visual coverage.\n\nSum totals: compute grams per component \u00d7 (per-100 g macros/energy) and add all components.\n\nValidation: enforce Calories \u2248 4\u00d7Protein + 4\u00d7Carbs + 9\u00d7Fat. If off by >8%, adjust fat first (oil/sauce most variable), then carbs (starches), keeping protein consistent with visible lean mass.\n\nRounding: round all final totals to integers. Never output ranges or decimals.\n\nOutput rules (must follow exactly)\n\nPlain text only.\n\nUse this exact structure and field order.\n\nValues are numbers only (no units, no \u201cg\u201d or \u201ckcal\u201d), no extra text, no JSON, no notes.\n\nMeal Description: [short description]\nCalories: [number]\nProteins: [number]\nCarbs: [number]\nFat: [number]",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-pro",
          "cachedResultName": "models/gemini-2.5-pro"
        },
        "options": {},
        "resource": "image",
        "inputType": "binary",
        "operation": "analyze"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "04b43855-1600-424d-a2ae-458fb72a0525",
      "name": "Analyze voice message",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        2480,
        1072
      ],
      "parameters": {
        "text": "What's in this audio message from telegram user?",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-pro",
          "cachedResultName": "models/gemini-2.5-pro"
        },
        "options": {},
        "resource": "audio",
        "inputType": "binary",
        "operation": "analyze"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3d5d43cc-4639-4d46-a095-b6de79d3aede",
      "name": "get_message (Audio/Video message)",
      "type": "n8n-nodes-base.set",
      "position": [
        2656,
        1072
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d8935452-fe20-469d-a68d-1aad056cb8dd",
              "name": "message",
              "type": "string",
              "value": "=Voice message description:{{ $json.candidates?.[0]?.content?.parts?.[0]?.text || $json.content?.parts?.[0]?.text }}"
            },
            {
              "id": "93f1bba1-1180-404a-93ca-c34cf1d1b7ac",
              "name": "chat_id",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "60e92d69-3fd6-4311-ba91-ea445adf9727",
      "name": "get_message (Media  message)",
      "type": "n8n-nodes-base.set",
      "position": [
        2656,
        1248
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d8935452-fe20-469d-a68d-1aad056cb8dd",
              "name": "message",
              "type": "string",
              "value": "=Content:\n{{ $json.content.parts[0].text }}"
            },
            {
              "id": "53e34499-7dad-4f94-aa7d-f778321f13f4",
              "name": "chat_id",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "563fd16d-25d2-4496-94eb-d6a8be600e92",
      "name": "Typing\u2026",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1296,
        1040
      ],
      "parameters": {
        "chatId": "={{ $json.message.chat.id }}",
        "operation": "sendChatAction"
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a07eaf7d-8e67-41c8-b5bd-367fbc5917f1",
      "name": "Fix mime",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        1072
      ],
      "parameters": {
        "jsCode": "// --- Mapa Extendido de Tipos MIME ---\n// Una lista completa para cubrir la mayor\u00eda de los formatos de archivo comunes.\nconst mimeMap = {\n  // --- Formatos de Documentos ---\n  'pdf': 'application/pdf',\n  'txt': 'text/plain',\n  'rtf': 'application/rtf',\n  'csv': 'text/csv',\n  'html': 'text/html',\n  'htm': 'text/html',\n  'json': 'application/json',\n  'xml': 'application/xml', // 'text/xml' tambi\u00e9n es v\u00e1lido pero 'application/xml' es m\u00e1s com\u00fan\n  'yaml': 'application/x-yaml',\n  'yml': 'application/x-yaml',\n\n  // --- Formatos de Microsoft Office ---\n  'doc': 'application/msword',\n  'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n  'xls': 'application/vnd.ms-excel',\n  'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n  'ppt': 'application/vnd.ms-powerpoint',\n  'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n  'pub': 'application/vnd.ms-publisher',\n\n  // --- Formatos de OpenOffice / LibreOffice ---\n  'odt': 'application/vnd.oasis.opendocument.text',\n  'ods': 'application/vnd.oasis.opendocument.spreadsheet',\n  'odp': 'application/vnd.oasis.opendocument.presentation',\n  'odg': 'application/vnd.oasis.opendocument.graphics',\n\n  // --- Formatos de Apple iWork ---\n  'pages': 'application/vnd.apple.pages',\n  'numbers': 'application/vnd.apple.numbers',\n  'key': 'application/vnd.apple.keynote',\n\n  // --- Formatos de Imagen ---\n  'png': 'image/png',\n  'jpg': 'image/jpeg',\n  'jpeg': 'image/jpeg',\n  'gif': 'image/gif',\n  'webp': 'image/webp',\n  'svg': 'image/svg+xml',\n  'bmp': 'image/bmp',\n  'ico': 'image/vnd.microsoft.icon',\n  'tif': 'image/tiff',\n  'tiff': 'image/tiff',\n  'heic': 'image/heic',\n  'heif': 'image/heif',\n\n  // --- Formatos de Audio ---\n  'mp3': 'audio/mpeg',\n  'wav': 'audio/wav',\n  'oga': 'audio/ogg',\n  'ogg': 'audio/ogg',\n  'flac': 'audio/flac',\n  'm4a': 'audio/mp4',\n  'aac': 'audio/aac',\n  'opus': 'audio/opus',\n  'wma': 'audio/x-ms-wma',\n  'mid': 'audio/midi',\n  'midi': 'audio/midi',\n\n  // --- Formatos de Video ---\n  'mp4': 'video/mp4',\n  'mov': 'video/quicktime',\n  'webm': 'video/webm',\n  'mpeg': 'video/mpeg',\n  'mpg': 'video/mpeg',\n  'avi': 'video/x-msvideo',\n  'wmv': 'video/x-ms-wmv',\n  'flv': 'video/x-flv',\n  'mkv': 'video/x-matroska',\n\n  // --- Formatos de Archivos y Compresi\u00f3n ---\n  'zip': 'application/zip',\n  'rar': 'application/vnd.rar',\n  '7z': 'application/x-7z-compressed',\n  'tar': 'application/x-tar',\n  'gz': 'application/gzip',\n  'bz2': 'application/x-bzip2',\n\n  // --- Otros Formatos ---\n  'epub': 'application/epub+zip',\n  'ics': 'text/calendar',\n  'vcf': 'text/vcard',\n  'js': 'text/javascript',\n  'css': 'text/css',\n  'sh': 'application/x-sh',\n  'py': 'text/x-python',\n};\n\n// --- L\u00f3gica de Procesamiento (sin cambios) ---\n\n// Obtenemos todos los items que llegan al nodo\nconst items = $input.all();\n\n// Iteramos sobre cada item para procesarlo\nfor (const item of items) {\n  // Verificamos que el item tenga datos binarios para procesar\n  if (item.binary && item.binary['data']) {\n    // Obtenemos el nombre del archivo de forma segura\n    const fileName = item.binary['data'].fileName || '';\n    if (!fileName) {\n      continue; // Si no hay nombre de archivo, pasamos al siguiente item\n    }\n\n    // Extraemos la extensi\u00f3n del archivo de forma robusta\n    const extension = fileName.slice((fileName.lastIndexOf(\".\") - 1 >>> 0) + 2).toLowerCase();\n\n    // Buscamos la extensi\u00f3n en nuestro mapa\n    const newMimeType = mimeMap[extension];\n\n    // Si encontramos una coincidencia en el mapa, actualizamos el mimeType\n    if (newMimeType) {\n      if(item.binary['data'].mimeType !== newMimeType) {\n        console.log(`Cambiando mimeType para '${fileName}' de '${item.binary['data'].mimeType}' a '${newMimeType}'.`);\n        item.binary['data'].mimeType = newMimeType;\n      }\n    }\n  }\n}\n\n// Devolvemos todos los items, modificados o no\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "96ff38b5-261b-4359-bfec-8f48db43c005",
      "name": "Fix mime5",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        1248
      ],
      "parameters": {
        "jsCode": "// --- Mapa Extendido de Tipos MIME ---\n// Una lista completa para cubrir la mayor\u00eda de los formatos de archivo comunes.\nconst mimeMap = {\n  // --- Formatos de Documentos ---\n  'pdf': 'application/pdf',\n  'txt': 'text/plain',\n  'rtf': 'application/rtf',\n  'csv': 'text/csv',\n  'html': 'text/html',\n  'htm': 'text/html',\n  'json': 'application/json',\n  'xml': 'application/xml', // 'text/xml' tambi\u00e9n es v\u00e1lido pero 'application/xml' es m\u00e1s com\u00fan\n  'yaml': 'application/x-yaml',\n  'yml': 'application/x-yaml',\n\n  // --- Formatos de Microsoft Office ---\n  'doc': 'application/msword',\n  'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n  'xls': 'application/vnd.ms-excel',\n  'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n  'ppt': 'application/vnd.ms-powerpoint',\n  'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n  'pub': 'application/vnd.ms-publisher',\n\n  // --- Formatos de OpenOffice / LibreOffice ---\n  'odt': 'application/vnd.oasis.opendocument.text',\n  'ods': 'application/vnd.oasis.opendocument.spreadsheet',\n  'odp': 'application/vnd.oasis.opendocument.presentation',\n  'odg': 'application/vnd.oasis.opendocument.graphics',\n\n  // --- Formatos de Apple iWork ---\n  'pages': 'application/vnd.apple.pages',\n  'numbers': 'application/vnd.apple.numbers',\n  'key': 'application/vnd.apple.keynote',\n\n  // --- Formatos de Imagen ---\n  'png': 'image/png',\n  'jpg': 'image/jpeg',\n  'jpeg': 'image/jpeg',\n  'gif': 'image/gif',\n  'webp': 'image/webp',\n  'svg': 'image/svg+xml',\n  'bmp': 'image/bmp',\n  'ico': 'image/vnd.microsoft.icon',\n  'tif': 'image/tiff',\n  'tiff': 'image/tiff',\n  'heic': 'image/heic',\n  'heif': 'image/heif',\n\n  // --- Formatos de Audio ---\n  'mp3': 'audio/mpeg',\n  'wav': 'audio/wav',\n  'oga': 'audio/ogg',\n  'ogg': 'audio/ogg',\n  'flac': 'audio/flac',\n  'm4a': 'audio/mp4',\n  'aac': 'audio/aac',\n  'opus': 'audio/opus',\n  'wma': 'audio/x-ms-wma',\n  'mid': 'audio/midi',\n  'midi': 'audio/midi',\n\n  // --- Formatos de Video ---\n  'mp4': 'video/mp4',\n  'mov': 'video/quicktime',\n  'webm': 'video/webm',\n  'mpeg': 'video/mpeg',\n  'mpg': 'video/mpeg',\n  'avi': 'video/x-msvideo',\n  'wmv': 'video/x-ms-wmv',\n  'flv': 'video/x-flv',\n  'mkv': 'video/x-matroska',\n\n  // --- Formatos de Archivos y Compresi\u00f3n ---\n  'zip': 'application/zip',\n  'rar': 'application/vnd.rar',\n  '7z': 'application/x-7z-compressed',\n  'tar': 'application/x-tar',\n  'gz': 'application/gzip',\n  'bz2': 'application/x-bzip2',\n\n  // --- Otros Formatos ---\n  'epub': 'application/epub+zip',\n  'ics': 'text/calendar',\n  'vcf': 'text/vcard',\n  'js': 'text/javascript',\n  'css': 'text/css',\n  'sh': 'application/x-sh',\n  'py': 'text/x-python',\n};\n\n// --- L\u00f3gica de Procesamiento (sin cambios) ---\n\n// Obtenemos todos los items que llegan al nodo\nconst items = $input.all();\n\n// Iteramos sobre cada item para procesarlo\nfor (const item of items) {\n  // Verificamos que el item tenga datos binarios para procesar\n  if (item.binary && item.binary['data']) {\n    // Obtenemos el nombre del archivo de forma segura\n    const fileName = item.binary['data'].fileName || '';\n    if (!fileName) {\n      continue; // Si no hay nombre de archivo, pasamos al siguiente item\n    }\n\n    // Extraemos la extensi\u00f3n del archivo de forma robusta\n    const extension = fileName.slice((fileName.lastIndexOf(\".\") - 1 >>> 0) + 2).toLowerCase();\n\n    // Buscamos la extensi\u00f3n en nuestro mapa\n    const newMimeType = mimeMap[extension];\n\n    // Si encontramos una coincidencia en el mapa, actualizamos el mimeType\n    if (newMimeType) {\n      if(item.binary['data'].mimeType !== newMimeType) {\n        console.log(`Cambiando mimeType para '${fileName}' de '${item.binary['data'].mimeType}' a '${newMimeType}'.`);\n        item.binary['data'].mimeType = newMimeType;\n      }\n    }\n  }\n}\n\n// Devolvemos todos los items, modificados o no\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "29051108-72c1-49da-8b51-25350affe0de",
      "name": "get_error_message1",
      "type": "n8n-nodes-base.set",
      "position": [
        2144,
        1408
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d8935452-fe20-469d-a68d-1aad056cb8dd",
              "name": "message",
              "type": "string",
              "value": "=It was not possible to process the file.File type not supported."
            },
            {
              "id": "38ba2498-2141-4a04-a22a-64563fe2ee6f",
              "name": "chat_id",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "41cf82ed-b4f4-4224-93b2-e2c96f4c0f4c",
      "name": "Send a text message",
      "type": "n8n-nodes-base.telegram",
      "position": [
        3680,
        1136
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "additionalFields": {
          "parse_mode": "MarkdownV2",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "938dbd1b-a566-4bcf-9c9c-7abf90174f8b",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        3072,
        1312
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ad29c874-9686-41aa-af26-5db22169f1dd",
      "name": "Simple Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        3200,
        1328
      ],
      "parameters": {
        "sessionKey": "={{ $json.chat_id }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.3
    },
    {
      "id": "dcbd0b50-3cb5-4e3e-be26-026199676bb7",
      "name": "MarkdownV2",
      "type": "n8n-nodes-base.code",
      "position": [
        3520,
        1136
      ],
      "parameters": {
        "jsCode": "/**\n * MarkdownV2-safe formatter + auto-chunker for Telegram (n8n Code node)\n * --------------------------------------------------------------------\n * - Allows: *bold*, _italic_, ||spoiler||, [label](url)\n * - Escapes everything else for Telegram MarkdownV2\n * - Validates/normalizes URLs\n * - Converts \"# Heading\" lines to bold titles\n * - Splits long messages into <= 4096-char chunks (uses a 4000-char budget)\n * - Outputs one item per chunk so the Telegram node sends all parts\n *\n * Recommended: Run this node in \"Run Once for All Items\".\n */\n\nconst MAX_TELEGRAM = 4096;\nconst SAFE_BUDGET = 4000; // small margin to avoid edge overflows\n\n// ============ MarkdownV2 helpers ============\nfunction escapeMarkdownV2(text) {\n  if (!text) return '';\n  return String(text).replace(/([\\\\_*[\\]()~`>#+\\-=|{}.!])/g, '\\\\$1');\n}\n\nfunction escapeForUrl(url) {\n  return String(url).replace(/[)\\\\]/g, '\\\\$&');\n}\n\nfunction normalizeAndValidateUrl(url) {\n  let raw = String(url || '').trim();\n  try {\n    const u = new URL(raw);\n    return u.toString();\n  } catch {}\n  // Try https:// for bare domains\n  const domainLike = /^[a-z0-9.-]+\\.[a-z]{2,}([/:?#].*)?$/i.test(raw);\n  if (domainLike) {\n    try {\n      const u2 = new URL('https://' + raw);\n      return u2.toString();\n    } catch {}\n  }\n  return null;\n}\n\nfunction normalizeHeadings(text) {\n  // Turn \"# Title\" \u2192 \"*Title*\"\n  return text.replace(/^(#{1,6})\\s+(.*)$/gm, (m, hashes, title) => `*${title.trim()}*`);\n}\n\nfunction normalizeCommonMd(text) {\n  return String(text)\n    .replace(/\\*\\*([\\s\\S]*?)\\*\\*/g, '*$1*') // **bold** \u2192 *bold*\n    .replace(/__([\\s\\S]*?)__/g, '_$1_');    // __italic__ \u2192 _italic_\n}\n\n/**\n * Convert incoming text to Telegram-safe MarkdownV2.\n */\nfunction processMarkdownV2Safe(inputText) {\n  if (!inputText) return '';\n\n  let text = normalizeCommonMd(String(inputText));\n  text = normalizeHeadings(text);\n\n  const placeholders = { links: [], bolds: [], italics: [], spoilers: [] };\n\n  // Links: keep safe via placeholders during escaping\n  text = text.replace(/\\[([^\\]\\n]+)\\]\\(([^)]+)\\)/g, (m, label, url) => {\n    const normalizedUrl = normalizeAndValidateUrl(url);\n    if (!normalizedUrl) return escapeMarkdownV2(label);\n    const idx = placeholders.links.length;\n    const ph = `\u27ecL${idx}\u27ed`;\n    const safeLabel = escapeMarkdownV2(label);\n    const safeUrl = escapeForUrl(normalizedUrl);\n    placeholders.links.push(`[${safeLabel}](${safeUrl})`);\n    return ph;\n  });\n\n  // Bold\n  text = text.replace(/\\*([\\s\\S]+?)\\*/g, (m, inner) => {\n    const idx = placeholders.bolds.length;\n    const ph = `\u27ecB${idx}\u27ed`;\n    placeholders.bolds.push(`*${escapeMarkdownV2(inner)}*`);\n    return ph;\n  });\n\n  // Italic\n  text = text.replace(/_([\\s\\S]+?)_/g, (m, inner) => {\n    const idx = placeholders.italics.length;\n    const ph = `\u27ecI${idx}\u27ed`;\n    placeholders.italics.push(`_${escapeMarkdownV2(inner)}_`);\n    return ph;\n  });\n\n  // Spoilers\n  text = text.replace(/\\|\\|([\\s\\S]+?)\\|\\|/g, (m, inner) => {\n    const idx = placeholders.spoilers.length;\n    const ph = `\u27ecS${idx}\u27ed`;\n    placeholders.spoilers.push(`||${escapeMarkdownV2(inner)}||`);\n    return ph;\n  });\n\n  // Escape everything else\n  text = escapeMarkdownV2(text);\n\n  // Restore placeholders\n  placeholders.links.forEach((md, i) => { text = text.replace(`\u27ecL${i}\u27ed`, md); });\n  placeholders.bolds.forEach((md, i) => { text = text.replace(`\u27ecB${i}\u27ed`, md); });\n  placeholders.italics.forEach((md, i) => { text = text.replace(`\u27ecI${i}\u27ed`, md); });\n  placeholders.spoilers.forEach((md, i) => { text = text.replace(`\u27ecS${i}\u27ed`, md); });\n\n  return text;\n}\n\n// ============ Chunking helpers ============\n/**\n * Split text into Telegram-safe chunks <= maxLen.\n * Prefers paragraph boundaries, then sentence boundaries, then words.\n * Falls back to hard cuts only when unavoidable (e.g., extremely long URL).\n */\nfunction chunkForTelegram(text, maxLen = SAFE_BUDGET) {\n  if (!text || text.length <= maxLen) return [text || ''];\n\n  const parts = [];\n  let buffer = '';\n\n  const flush = () => {\n    if (buffer) {\n      parts.push(buffer);\n      buffer = '';\n    }\n  };\n\n  // 1) Paragraph-level packing\n  const paragraphs = text.split(/\\n{2,}/);\n  for (const pRaw of paragraphs) {\n    const p = pRaw; // keep paragraph as-is\n    const candidate = buffer ? buffer + '\\n\\n' + p : p;\n    if (candidate.length <= maxLen) {\n      buffer = candidate;\n      continue;\n    }\n    if (p.length <= maxLen) {\n      flush();\n      buffer = p;\n      continue;\n    }\n\n    // 2) Sentence-level packing (paragraph is still too big)\n    flush();\n    const sentences = p.split(/(?<=[.!?\u2026])\\s+(?=[^\\s])/u);\n    let sBuf = '';\n    for (const s of sentences) {\n      const sCandidate = sBuf ? sBuf + ' ' + s : s;\n      if (sCandidate.length <= maxLen) {\n        sBuf = sCandidate;\n        continue;\n      }\n      if (s.length <= maxLen) {\n        if (sBuf) parts.push(sBuf);\n        sBuf = s;\n        continue;\n      }\n\n      // 3) Word-level packing (sentence is still too big)\n      if (sBuf) { parts.push(sBuf); sBuf = ''; }\n      let wBuf = '';\n      const words = s.split(/\\s+/);\n      for (const w of words) {\n        const wCandidate = wBuf ? wBuf + ' ' + w : w;\n        if (wCandidate.length <= maxLen) {\n          wBuf = wCandidate;\n          continue;\n        }\n        if (w.length <= maxLen) {\n          if (wBuf) parts.push(wBuf);\n          wBuf = w;\n          continue;\n        }\n        // 4) Hard split (extremely long token, e.g., massive URL)\n        if (wBuf) { parts.push(wBuf); wBuf = ''; }\n        const re = new RegExp(`.{1,${maxLen}}`, 'g');\n        const hardPieces = w.match(re) || [];\n        parts.push(...hardPieces);\n      }\n      if (wBuf) parts.push(wBuf);\n    }\n    if (sBuf) parts.push(sBuf);\n  }\n  if (buffer) parts.push(buffer);\n\n  // Final safety pass: trim chunks that might still exceed MAX_TELEGRAM\n  return parts.flatMap(part => {\n    if (part.length <= MAX_TELEGRAM) return [part];\n    const re = new RegExp(`.{1,${SAFE_BUDGET}}`, 'g');\n    return part.match(re) || [];\n  });\n}\n\n// ============ Main ============\nconst inputItems = $input.all();\nconst out = [];\n\nfor (const item of inputItems) {\n  const j = item.json || {};\n  const raw =\n    j.message ?? j.output ?? j.text ?? j.content ?? '';\n\n  const formatted = processMarkdownV2Safe(raw);\n  const chunks = chunkForTelegram(formatted, SAFE_BUDGET);\n\n  chunks.forEach((chunk, idx) => {\n    out.push({\n      json: {\n        ...j,\n        message: chunk,\n        message_part_index: idx + 1,\n        message_parts_total: chunks.length,\n      },\n      binary: item.binary,\n    });\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "9e4623d1-a1d6-426b-bb97-186edb14f3f5",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        1472,
        1200
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "b9e63bbf-24e6-424b-ba4e-6acd3f17b57c",
              "operator": {
                "type": "number",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.User_ID }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1d9a7fa5-5b3c-4531-a268-99ec7befc78f",
      "name": "Registered?",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "position": [
        1296,
        1200
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
              "lookupColumn": "User_ID"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit#gid=0",
          "cachedResultName": "Profile"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_YOUR_AWS_SECRET_KEY_HERE",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7,
      "alwaysOutputData": true
    },
    {
      "id": "2a6e36c8-12fc-48c2-a909-40ffbe07f6f9",
      "name": "Send a text message1",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2480,
        1776
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "additionalFields": {
          "parse_mode": "MarkdownV2",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6d1ab7a7-b072-45d0-ad0d-984d09f8b371",
      "name": "Google Gemini Chat Model1",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        1872,
        1968
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3c36bb0c-84d9-43b2-83c0-69e36226c098",
      "name": "Simple Memory1",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        2032,
        2032
      ],
      "parameters": {
        "sessionKey": "={{ $json.chat_id }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.3
    },
    {
      "id": "f371ad0b-bf0b-4927-a169-9b56cc8fce63",
      "name": "MarkdownV",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        1776
      ],
      "parameters": {
        "jsCode": "/**\n * MarkdownV2-safe formatter + auto-chunker for Telegram (n8n Code node)\n * --------------------------------------------------------------------\n * - Allows: *bold*, _italic_, ||spoiler||, [label](url)\n * - Escapes everything else for Telegram MarkdownV2\n * - Validates/normalizes URLs\n * - Converts \"# Heading\" lines to bold titles\n * - Splits long messages into <= 4096-char chunks (uses a 4000-char budget)\n * - Outputs one item per chunk so the Telegram node sends all parts\n *\n * Recommended: Run this node in \"Run Once for All Items\".\n */\n\nconst MAX_TELEGRAM = 4096;\nconst SAFE_BUDGET = 4000; // small margin to avoid edge overflows\n\n// ============ MarkdownV2 helpers ============\nfunction escapeMarkdownV2(text) {\n  if (!text) return '';\n  return String(text).replace(/([\\\\_*[\\]()~`>#+\\-=|{}.!])/g, '\\\\$1');\n}\n\nfunction escapeForUrl(url) {\n  return String(url).replace(/[)\\\\]/g, '\\\\$&');\n}\n\nfunction normalizeAndValidateUrl(url) {\n  let raw = String(url || '').trim();\n  try {\n    const u = new URL(raw);\n    return u.toString();\n  } catch {}\n  // Try https:// for bare domains\n  const domainLike = /^[a-z0-9.-]+\\.[a-z]{2,}([/:?#].*)?$/i.test(raw);\n  if (domainLike) {\n    try {\n      const u2 = new URL('https://' + raw);\n      return u2.toString();\n    } catch {}\n  }\n  return null;\n}\n\nfunction normalizeHeadings(text) {\n  // Turn \"# Title\" \u2192 \"*Title*\"\n  return text.replace(/^(#{1,6})\\s+(.*)$/gm, (m, hashes, title) => `*${title.trim()}*`);\n}\n\nfunction normalizeCommonMd(text) {\n  return String(text)\n    .replace(/\\*\\*([\\s\\S]*?)\\*\\*/g, '*$1*') // **bold** \u2192 *bold*\n    .replace(/__([\\s\\S]*?)__/g, '_$1_');    // __italic__ \u2192 _italic_\n}\n\n/**\n * Convert incoming text to Telegram-safe MarkdownV2.\n */\nfunction processMarkdownV2Safe(inputText) {\n  if (!inputText) return '';\n\n  let text = normalizeCommonMd(String(inputText));\n  text = normalizeHeadings(text);\n\n  const placeholders = { links: [], bolds: [], italics: [], spoilers: [] };\n\n  // Links: keep safe via placeholders during escaping\n  text = text.replace(/\\[([^\\]\\n]+)\\]\\(([^)]+)\\)/g, (m, label, url) => {\n    const normalizedUrl = normalizeAndValidateUrl(url);\n    if (!normalizedUrl) return escapeMarkdownV2(label);\n    const idx = placeholders.links.length;\n    const ph = `\u27ecL${idx}\u27ed`;\n    const safeLabel = escapeMarkdownV2(label);\n    const safeUrl = escapeForUrl(normalizedUrl);\n    placeholders.links.push(`[${safeLabel}](${safeUrl})`);\n    return ph;\n  });\n\n  // Bold\n  text = text.replace(/\\*([\\s\\S]+?)\\*/g, (m, inner) => {\n    const idx = placeholders.bolds.length;\n    const ph = `\u27ecB${idx}\u27ed`;\n    placeholders.bolds.push(`*${escapeMarkdownV2(inner)}*`);\n    return ph;\n  });\n\n  // Italic\n  text = text.replace(/_([\\s\\S]+?)_/g, (m, inner) => {\n    const idx = placeholders.italics.length;\n    const ph = `\u27ecI${idx}\u27ed`;\n    placeholders.italics.push(`_${escapeMarkdownV2(inner)}_`);\n    return ph;\n  });\n\n  // Spoilers\n  text = text.replace(/\\|\\|([\\s\\S]+?)\\|\\|/g, (m, inner) => {\n    const idx = placeholders.spoilers.length;\n    const ph = `\u27ecS${idx}\u27ed`;\n    placeholders.spoilers.push(`||${escapeMarkdownV2(inner)}||`);\n    return ph;\n  });\n\n  // Escape everything else\n  text = escapeMarkdownV2(text);\n\n  // Restore placeholders\n  placeholders.links.forEach((md, i) => { text = text.replace(`\u27ecL${i}\u27ed`, md); });\n  placeholders.bolds.forEach((md, i) => { text = text.replace(`\u27ecB${i}\u27ed`, md); });\n  placeholders.italics.forEach((md, i) => { text = text.replace(`\u27ecI${i}\u27ed`, md); });\n  placeholders.spoilers.forEach((md, i) => { text = text.replace(`\u27ecS${i}\u27ed`, md); });\n\n  return text;\n}\n\n// ============ Chunking helpers ============\n/**\n * Split text into Telegram-safe chunks <= maxLen.\n * Prefers paragraph boundaries, then sentence boundaries, then words.\n * Falls back to hard cuts only when unavoidable (e.g., extremely long URL).\n */\nfunction chunkForTelegram(text, maxLen = SAFE_BUDGET) {\n  if (!text || text.length <= maxLen) return [text || ''];\n\n  const parts = [];\n  let buffer = '';\n\n  const flush = () => {\n    if (buffer) {\n      parts.push(buffer);\n      buffer = '';\n    }\n  };\n\n  // 1) Paragraph-level packing\n  const paragraphs = text.split(/\\n{2,}/);\n  for (const pRaw of paragraphs) {\n    const p = pRaw; // keep paragraph as-is\n    const candidate = buffer ? buffer + '\\n\\n' + p : p;\n    if (candidate.length <= maxLen) {\n      buffer = candidate;\n      continue;\n    }\n    if (p.length <= maxLen) {\n      flush();\n      buffer = p;\n      continue;\n    }\n\n    // 2) Sentence-level packing (paragraph is still too big)\n    flush();\n    const sentences = p.split(/(?<=[.!?\u2026])\\s+(?=[^\\s])/u);\n    let sBuf = '';\n    for (const s of sentences) {\n      const sCandidate = sBuf ? sBuf + ' ' + s : s;\n      if (sCandidate.length <= maxLen) {\n        sBuf = sCandidate;\n        continue;\n      }\n      if (s.length <= maxLen) {\n        if (sBuf) parts.push(sBuf);\n        sBuf = s;\n        continue;\n      }\n\n      // 3) Word-level packing (sentence is still too big)\n      if (sBuf) { parts.push(sBuf); sBuf = ''; }\n      let wBuf = '';\n      const words = s.split(/\\s+/);\n      for (const w of words) {\n        const wCandidate = wBuf ? wBuf + ' ' + w : w;\n        if (wCandidate.length <= maxLen) {\n          wBuf = wCandidate;\n          continue;\n        }\n        if (w.length <= maxLen) {\n          if (wBuf) parts.push(wBuf);\n          wBuf = w;\n          continue;\n        }\n        // 4) Hard split (extremely long token, e.g., massive URL)\n        if (wBuf) { parts.push(wBuf); wBuf = ''; }\n        const re = new RegExp(`.{1,${maxLen}}`, 'g');\n        const hardPieces = w.match(re) || [];\n        parts.push(...hardPieces);\n      }\n      if (wBuf) parts.push(wBuf);\n    }\n    if (sBuf) parts.push(sBuf);\n  }\n  if (buffer) parts.push(buffer);\n\n  // Final safety pass: trim chunks that might still exceed MAX_TELEGRAM\n  return parts.flatMap(part => {\n    if (part.length <= MAX_TELEGRAM) return [part];\n    const re = new RegExp(`.{1,${SAFE_BUDGET}}`, 'g');\n    return part.match(re) || [];\n  });\n}\n\n// ============ Main ============\nconst inputItems = $input.all();\nconst out = [];\n\nfor (const item of inputItems) {\n  const j = item.json || {};\n  const raw =\n    j.message ?? j.output ?? j.text ?? j.content ?? '';\n\n  const formatted = processMarkdownV2Safe(raw);\n  const chunks = chunkForTelegram(formatted, SAFE_BUDGET);\n\n  chunks.forEach((chunk, idx) => {\n    out.push({\n      json: {\n        ...j,\n        message: chunk,\n        message_part_index: idx + 1,\n        message_parts_total: chunks.length,\n      },\n      binary: item.binary,\n    });\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "0086d3f2-cc2f-45a8-9db7-a7b1a803700a",
      "name": "Get Meals Info",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3056,
        1808
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.Date }}",
              "lookupColumn": "Date"
            },
            {
              "lookupValue": "={{ $json.User_ID }}",
              "lookupColumn": "User_ID"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 403788598,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit#gid=403788598",
          "cachedResultName": "Meals"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_YOUR_AWS_SECRET_KEY_HERE",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7,
      "alwaysOutputData": true
    },
    {
      "id": "6e61ba86-d1ad-4140-b59d-715fdf7472fa",
      "name": "Get User Info",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3056,
        2064
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.User_ID }}",
              "lookupColumn": "User_ID"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit#gid=0",
          "cachedResultName": "Profile"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_YOUR_AWS_SECRET_KEY_HERE",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "49cbbd8e-dc4f-4ac9-b17a-afc6f8f5431b",
      "name": "get_message (register)",
      "type": "n8n-nodes-base.set",
      "position": [
        1728,
        1776
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "801ec600-22ad-4a94-a2b4-ae72eb271df0",
              "name": "message",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.text }}"
            },
            {
              "id": "263071fb-bcdf-42b0-bb46-71b75fa0bf2a",
              "name": "chat_id",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "544b7ab2-d054-4235-b46b-2a3cbcbd3583",
      "name": "Register User",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        2192,
        2032
      ],
      "parameters": {
        "columns": {
          "value": {
            "Name": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Name', ``, 'string') }}",
            "User_ID": "={{ $json.chat_id }}",
            "Protein_target": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Protein_target', ``, 'string') }}",
            "Calories_target": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Calories_target', ``, 'string') }}"
          },
          "schema": [
            {
              "id": "User_ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "User_ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Calories_target",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Calories_target",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Protein_target",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Protein_target",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit#gid=0",
          "cachedResultName": "Profile"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_YOUR_AWS_SECRET_KEY_HERE",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "615bc756-bc1c-49a7-86a2-af2c940e474a",
      "name": "Register Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2000,
        1776
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "options": {
          "systemMessage": "=You are Cal AI \ud83c\udfcb\ufe0f\u200d\u2642\ufe0f\ud83e\udd66, the friendly fitness & nutrition coach assistant.\nYour single job is to register a new user into the Users table while keeping the tone supportive, clear, and motivational. Always use emojis related to health, training, and food (\ud83d\udd25\ud83d\udcaa\ud83e\udd66\ud83c\udf57\ud83c\udf3e\ud83e\udd51).\n\n\ud83d\udd11 Registration Rules\n\nYou MUST collect these fields:\n\nuser_id (not asked, system provides it)\n\nname\n\ncalories_target\n\nprotein_target\n\n\ud83d\udccc When sending data to the Register User tool, always send numbers only (no units, no text, no emojis) for calories_target and protein_targe\n\nIf the user does not know their numeric targets, do not invent them. Instead, coach them by politely asking for:\n\nweight \u2696\ufe0f\n\nheight \ud83d\udccf\n\nage \ud83c\udf82\n\ngoal \ud83c\udfaf (gain muscle \ud83d\udcaa, lose fat \ud83d\udd25, maintain \u2696\ufe0f)\n\nBased on this info, calculate precise calorie and protein targets.\n\nOnly when all fields are collected and confirmed \u2192 call Register User tool with the final data.\n\n\u2705 After Successful Registration\n\nSend a short, friendly confirmation like a coach:\n\n\u201cAwesome, champ \ud83d\udcaa! Your nutrition targets are locked in: \ud83d\udd25 [calories] kcal, \ud83c\udf57 [protein] g protein.\u201d\n\nThen, explain clearly how to use Cal AI:\n\n\ud83d\udcf8 Send food photos \u2192 get instant calories + macros.\n\n\u2699\ufe0f View or update your targets anytime.\n\n\ud83d\udcd1 Request daily reports \u2192 compare intake vs. targets with charts.\n\n\ud83d\udde3\ufe0f Style Guide\n\nAlways keep responses simple, clear, and concise.\n\nSpeak like a friendly trainer/coach who motivates the user.\n\nUse emojis to highlight key concepts.\n\nBe warm, supportive, and practical:\n\n\u201cLet\u2019s get you set up for success \ud83c\udfcb\ufe0f\u200d\u2642\ufe0f\ud83d\udd25\u201d\n\n\u201cStrong start, [Name]! Targets ready \ud83d\udcaa\ud83e\udd66.\u201d"
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "6d4404a0-3993-4719-b830-e5786e0a4ab5",
      "name": "Cal IA Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        3200,
        1136
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "options": {
          "systemMessage": "=You are Cal AI \ud83c\udfcb\ufe0f\u200d\u2642\ufe0f\ud83e\udd66, your friendly fitness coach and nutrition orchestrator.\nYour mission is to guide the user with motivation, clarity, and precision while managing their nutrition data. Speak in a supportive, energetic tone like a personal trainer, and use relevant emojis (\ud83d\udd25\ud83d\udcaa\ud83e\udd66\ud83c\udf57\ud83c\udf3e\ud83e\udd51) to keep the conversation fun and engaging.\n\nYou have four tools available:\n\nappendMealData(tool) \u2192 store a meal row in Meals sheet.\n\nupdateProfileData(tool) \u2192 update the user's profile targets (fields: Name, Calories_target, Protein_target).\n\ngetUserData(tool) \u2192 fetch the user's profile info.\n\ngetReport(tool) \u2192 generate or fetch the daily report (requires date).\n\n\ud83d\udd11 Rules\n\nThe image analysis is done before reaching you. You will always receive structured info:\nMeal Description: [short description]\nCalories: [number]\nProteins: [number]\nCarbs: [number]\nFat: [number]\n\nWith this info, call appendMealData.\n\nAfter appendMealData success, confirm naturally in a coach style: repeat the meal info using emojis (\ud83d\udd25 Calories, \ud83c\udf57 Protein, \ud83c\udf3e Carbs, \ud83e\udd51 Fat).\n\nEnd confirmations with a quick motivational phrase like:\n\n\u201cGreat fuel for your body \ud83d\udcaa\ud83d\udd25\u201d\n\n\u201cAnother step closer to your goals \ud83e\udd66\ud83c\udfcb\ufe0f\u200d\u2642\ufe0f\u201d\n\nAlways offer short next-step options:\n\ud83d\udc49 \u201cView daily report \ud83d\udcd1\u201d\n\ud83d\udc49 \u201cAnalyze another meal \ud83d\udcf8\u201d\n\ud83d\udc49 \u201cView or update profile targets \u2699\ufe0f\u201d\n\n\ud83d\udd04 Profile Update Logic\n\nWhen the user wants to update their profile (Name, Calories_target, or Protein_target):\n\nFirst call getUserData to fetch current profile info.\n\nCompare the requested update with the existing values.\n\nOnly pass the changed fields to updateProfileData (never overwrite unchanged values).\n\nConfirm to the user in a friendly way, e.g.:\n\n\u201c\u2705 Your Calories_target is now 2200 \ud83d\udd25. Protein_target stays strong at 150 \ud83c\udf57.\u201d\n\n\u201cProfile updated! Let\u2019s crush it \ud83d\udcaa\ud83e\udd66.\u201d\n\n\ud83d\udcd1 Tool Usage Rules\n\ngetUserData \u2192 always called first before updating the profile.\n\nupdateProfileData \u2192 only include the fields that have changed.\n\ngetReport \u2192 only pass the requested date.\n\nKeep all responses short, clear, motivational, and full of energy.\n\n\ud83d\udcc5 Date: {{ $today.format('yyyy-MM-dd') }}"
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "a2b7e00f-1e9b-4032-8ae1-6612b3ef6520",
      "name": "Update Profile Data",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        3520,
        1392
      ],
      "parameters": {
        "columns": {
          "value": {
            "Name": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Name', ``, 'string') }}",
            "User_ID": "={{ $json.chat_id }}",
            "Protein_target": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Protein_target', ``, 'string') }}",
            "Calories_target": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Calories_target', ``, 'string') }}"
          },
          "schema": [
            {
              "id": "User_ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "User_ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Calories_target",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Calories_target",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Protein_target",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Protein_target",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "User_ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit#gid=0",
          "cachedResultName": "Profile"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_YOUR_AWS_SECRET_KEY_HERE",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        },
        "descriptionType": "manual",
        "toolDescription": "=updateProfileData\nPurpose: Update the user\u2019s profile targets.\nFields that can be updated:\n\nName (string)\n\nCalories_target (string/number)\n\nProtein_target (string/number)\nWhen to use: When the user explicitly asks to update their name, calorie target, or protein target."
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "83f1cb62-818d-4161-99e4-7c984552fde1",
      "name": "Get Profile Data",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        3344,
        1392
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.chat_id }}",
              "lookupColumn": "User_ID"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit#gid=0",
          "cachedResultName": "Profile"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_YOUR_AWS_SECRET_KEY_HERE",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_YOUR_AWS_SECRET_KEY_HERE/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        },
        "descriptionType": "manual",
        "toolDescription": "=getUserData\nPurpose: Retrieve the user\u2019s profile information.\nInputs: none.\nWhen to use: When the user asks about their profile info or targets."
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "b4411d17-99e7-40c3-999b-21109d12e057",
      "name": "Get Report",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        2848,
        1808
      ],
      "parameters": {
        "workflowId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $workflow.id }}"
        },
        "description": "getReport\nPurpose: Generate or fetch the user\u2019s daily nutrition report.\nInput required: date (string, format: YYYY-MM-DD).\nWhen to use: When the user asks to see their daily summary or report for a specific date.",
        "workflowInputs": {
          "value": {
            "Date": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Date', ``, 'string') }}",
            "User_ID": "={{ $json.chat_id }}"
          },
          "schema": [
            {
              "id": "User_ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "User_ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1df46a7c-3ef6-4698-93cc-75558e882ca9",
      "name": "Append Meal Data",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        3424,
        1536
      ],
      "parameters": {
        "columns": {
          "value": {
            "Date": "={{ $today.format(\"yyyy-LL-dd\") }}\n",
            "Fats": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Fats', ``, 'string') }}",
            "Carbs": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Carbs', ``, 'string') }}",
            "User_ID": "={{ $json.chat_id }}",
            "Calories": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Calories', ``, 'string') }}",
            "Proteins": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Proteins', ``, 'string') }}",
            "Meal_description": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Meal_description', ``, 'string') }}"
          },
          "schema": [
            {
              "id": "User_ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "User_ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Meal_description",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Meal_description",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Calories",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Calories",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Proteins",
              "type": "string",
              "display": true,
      

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

&gt; AI-powered nutrition assistant for Telegram — log meals, set goals, and get personalized daily reports with Google Sheets integration.

Source: https://n8n.io/workflows/7756/ — 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

This project is a template for building a complete academic virtual assistant using n8n. It connects to Telegram, answers frequently asked questions by querying MongoDB, keeps the community informed a

Telegram, MongoDB, Telegram Trigger +6
AI & RAG

Telegram Trigger receives incoming messages (text, voice, photo, document). Switch routes by message type to appropriate processors: Text → forwarded as-is. Voice → downloaded and sent to Transcribe a

Memory Buffer Window, Telegram Trigger, Telegram +12
AI & RAG

Transform your Telegram messenger into a powerful, multi-modal personal or team assistant. This n8n workflow creates an intelligent agent that can understand text, voice, images, and documents, and ta

Memory Buffer Window, Telegram Trigger, Telegram +10
AI & RAG

A comprehensive n8n workflow demonstrating advanced AI agent orchestration, stateful conversation management, and multi-modal input processing for nutrition tracking applications.

Telegram, Memory Buffer Window, Google Gemini Chat +6
AI & RAG

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"

Telegram Trigger, Output Parser Structured, Telegram +6