AutomationFlowsAI & RAG › Foodsnap - Unified (food & Coach)

Foodsnap - Unified (food & Coach)

FoodSnap - Unified (Food & Coach). Uses postgres, n8n-nodes-evolution-api, googleGemini. Webhook trigger; 22 nodes.

Webhook trigger★★★★☆ complexityAI-powered22 nodesPostgresN8N Nodes Evolution ApiGoogle Gemini
AI & RAG Trigger: Webhook Nodes: 22 Complexity: ★★★★☆ AI nodes: yes Added:

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
{
  "name": "FoodSnap - Unified (Food & Coach)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "wa/inbound",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -680,
        -940
      ],
      "id": "webhook-unified",
      "name": "Webhook (Whatsapp)"
    },
    {
      "parameters": {
        "jsCode": "const body = $json.body ?? $json;\nconst data = body.data ?? {};\n\n// =========================\n// RemoteJid (prioridade s.whatsapp.net)\n// =========================\nconst remoteJid =\n  data?.key?.remoteJid?.includes('@s.whatsapp.net')\n    ? data.key.remoteJid\n    : data?.key?.remoteJidAlt || '';\n\n// n\u00famero limpo (E.164 sem +)\nconst number = remoteJid.replace(/\\D/g, '');\n\n// =========================\n// Message ID\n// =========================\nconst message_id = data?.key?.id || '';\n\n// =========================\n// Texto e Caption\n// =========================\n// Verifica conversation, extendedTextMessage (text) e imageMessage (caption)\nconst text =\n  data?.message?.conversation ||\n  data?.message?.extendedTextMessage?.text ||\n  data?.message?.imageMessage?.caption ||\n  '';\n\n// =========================\n// Imagem\n// =========================\nconst imageMessage =\n  data?.message?.imageMessage ||\n  data?.message?.extendedTextMessage?.contextInfo?.quotedMessage?.imageMessage ||\n  null;\n\n// =========================\n// Return normalizado\n// =========================\nreturn [\n  {\n    number,\n    remoteJid,\n    message_id,\n    text,\n    hasImage: !!imageMessage,\n    imageMessage,\n    pushName: data?.pushName || 'Atleta',\n    timestamp: new Date().toISOString(),\n    raw: body\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -460,
        -940
      ],
      "id": "normalize-inbound",
      "name": "Normalizar Dados"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "check-coach",
              "leftValue": "={{ $json.text }}",
              "rightValue": "coach,treino,shape,biotipo,fisico,musculo",
              "operator": {
                "type": "string",
                "operation": "contains",
                "singleValue": true
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -240,
        -940
      ],
      "id": "router-intent",
      "name": "\u00c9 Coach?"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "with u as (\n  select id\n  from public.profiles\n  where phone_e164 = cast({{ $('Normalizar Dados').item.json.number }} as text)\n  limit 1\n),\nent as (\n  select ue.is_active, ue.entitlement_code, ue.valid_until\n  from public.user_entitlements ue\n  where ue.user_id = (select id from u)\n  order by ue.valid_until desc nulls last\n  limit 1\n),\nusage as (\n  select count(*) as used_count\n  from public.coach_analyses fa\n  where fa.user_id = (select id from u)\n  and fa.used_free_quota = true\n)\nselect\n  (select id from u) as user_id,\n  (select id from u) is not null as exists,\n  coalesce((select used_count from usage), 0)::int as free_used,\n  greatest(0, 3 - coalesce((select used_count from usage), 0))::int as free_remaining,\n  coalesce((select is_active from ent), false) as plan_active,\n  (coalesce((select is_active from ent), false) = true OR greatest(0, 3 - coalesce((select used_count from usage), 0)) > 0) as can_process",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        20,
        -1160
      ],
      "id": "validate-coach",
      "name": "Valida\u00e7\u00e3o Coach",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "with u as (\n  select id\n  from public.profiles\n  where phone_e164 = cast({{ $('Normalizar Dados').item.json.number }} as text)\n  limit 1\n),\nent as (\n  select\n    ue.user_id,\n    ue.is_active,\n    ue.entitlement_code\n  from public.user_entitlements ue\n  where ue.user_id = (select id from u)\n  order by ue.valid_until desc nulls last\n  limit 1\n),\nusage as (\n  select count(*) filter (where fa.used_free_quota = true) as free_used\n  from public.food_analyses fa\n  where fa.user_id = (select id from u)\n)\nselect\n  (select id from u) is not null as exists,\n  (select id from u) as user_id,\n  coalesce((select free_used from usage), 0)::int as free_used,\n  greatest(0, 5 - coalesce((select free_used from usage), 0))::int as free_remaining,\n  coalesce((select is_active from ent), false) as plan_active,\n  (\n    (select id from u) is not null\n    and (\n      coalesce((select is_active from ent), false) = true\n      or greatest(0, 5 - coalesce((select free_used from usage), 0)) > 0\n    )\n  ) as can_process;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        20,
        -740
      ],
      "id": "validate-food",
      "name": "Valida\u00e7\u00e3o Food",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "check-process",
              "leftValue": "={{ $json.can_process }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        260,
        -1160
      ],
      "id": "if-coach-quota",
      "name": "Coach OK?"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "check-process-food",
              "leftValue": "={{ $json.can_process }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        260,
        -740
      ],
      "id": "if-food-quota",
      "name": "Food OK?"
    },
    {
      "parameters": {
        "resource": "messages-api",
        "instanceName": "FoodSnap",
        "remoteJid": "={{ $('Normalizar Dados').item.json.number }}",
        "messageText": "\ud83e\uddd0 *Coach AI*: Analisando seu bi\u00f3tipo e gerando seu treino... Aguarde!",
        "options_message": {}
      },
      "type": "n8n-nodes-evolution-api.evolutionApi",
      "typeVersion": 1,
      "position": [
        500,
        -1260
      ],
      "id": "msg-ack-coach",
      "name": "Ack Coach",
      "credentials": {
        "evolutionApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "chat-api",
        "operation": "get-media-base64",
        "instanceName": "FoodSnap",
        "messageId": "={{ $('Normalizar Dados').item.json.message_id }}"
      },
      "type": "n8n-nodes-evolution-api.evolutionApi",
      "typeVersion": 1,
      "position": [
        720,
        -1260
      ],
      "id": "get-image-coach",
      "name": "Baixar IMG Coach",
      "credentials": {
        "evolutionApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "toBinary",
        "sourceProperty": "data.base64",
        "options": {}
      },
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        940,
        -1260
      ],
      "id": "convert-binary-coach",
      "name": "Bin\u00e1rio Coach"
    },
    {
      "parameters": {
        "resource": "image",
        "operation": "analyze",
        "modelId": {
          "__rl": true,
          "value": "models/gemini-1.5-flash",
          "mode": "list",
          "cachedResultName": "models/gemini-1.5-flash"
        },
        "text": "=Voc\u00ea \u00e9 um Treinador F\u00edsico de Elite e Nutricionista Esportivo.\nAnalise a imagem fornecida (foto de corpo inteiro/f\u00edsico).\n\n1. Verifique se \u00e9 uma foto de corpo humano v\u00e1lida para an\u00e1lise fitness. Se n\u00e3o, retorne \"valid_body\": false.\n2. Se for v\u00e1lida, estime:\n   - Bi\u00f3tipo Predominante (Ectomorfo, Mesomorfo, Endomorfo)\n   - Percentual de Gordura (BF% aproximado)\n   - N\u00edvel de Massa Muscular (Baixo, M\u00e9dio, Alto)\n3. Sugira:\n   - Objetivo Principal (Cutting, Bulking, Manuten\u00e7\u00e3o)\n   - Divis\u00e3o de Treino Recomendada (ex: ABC, ABCDE, Upper/Lower)\n   - Foco Diet\u00e9tico (ex: Carb Cycling, Alto Carbo, Keto)\n   - Dica de Ouro (uma frase curta de impacto)\n\nResponda APENAS em JSON estrito (sem markdown):\n{\n  \"valid_body\": true,\n  \"biotype\": \"...\",\n  \"estimated_body_fat\": 15,\n  \"muscle_mass\": \"M\u00e9dio\",\n  \"goal\": \"Bulking\",\n  \"workout\": \"...\",\n  \"diet\": \"...\",\n  \"tip\": \"...\"\n}",
        "inputType": "binary",
        "simplify": false,
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "typeVersion": 1,
      "position": [
        1160,
        -1260
      ],
      "id": "gemini-coach",
      "name": "Gemini Coach",
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const raw = $json?.candidates?.[0]?.content?.parts?.[0]?.text || '{}';\nconst clean = raw.replace(/```json/g, '').replace(/```/g, '').trim();\nlet data = {};\ntry { \n  data = JSON.parse(clean); \n} catch(e) {\n  data = { valid_body: false, error: 'Failed to parse AI' };\n}\n\nreturn [{ ...data }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1380,
        -1260
      ],
      "id": "parse-coach-json",
      "name": "Parse Coach JSON"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "insert into public.coach_analyses\n(\n  user_id,\n  source,\n  image_url,\n  ai_raw_response,\n  ai_structured,\n  biotype,\n  estimated_body_fat,\n  goal_suggestion,\n  muscle_mass_level,\n  used_free_quota\n)\nvalues\n(\n  cast('{{ $(\"Valida\u00e7\u00e3o Coach\").item.json.user_id }}' as uuid),\n  'whatsapp',\n  null,\n  '{{ JSON.stringify($json) }}',\n  '{{ JSON.stringify($json) }}',\n  '{{ $json.biotype }}',\n  cast({{ $json.estimated_body_fat || 0 }} as numeric),\n  '{{ $json.goal }}',\n  '{{ $json.muscle_mass }}',\n  CASE\n    WHEN {{ $(\"Valida\u00e7\u00e3o Coach\").item.json.plan_active }} = true THEN false\n    ELSE true\n  END\n)\nreturning id as analysis_id;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1600,
        -1260
      ],
      "id": "save-coach-db",
      "name": "Salvar Coach DB",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "messages-api",
        "instanceName": "FoodSnap",
        "remoteJid": "={{ $('Normalizar Dados').item.json.number }}",
        "messageText": "=\u26a1 *Coach AI Report*\n\n\ud83e\uddec *Bi\u00f3tipo*: {{$json.biotype}}\n\u2696\ufe0f *BF Estimado*: ~{{$json.estimated_body_fat}}%\n\ud83d\udcaa *Massa Muscular*: {{$json.muscle_mass}}\n\n\ud83c\udfaf *Foco*: {{$json.goal}}\n\ud83c\udfcb\ufe0f *Treino*: {{$json.workout}}\n\ud83e\udd57 *Dieta*: {{$json.diet}}\n\n\ud83d\udca1 *Dica*: {{$json.tip}}\n\n_Acesse o App para ver a ficha completa!_",
        "options_message": {}
      },
      "type": "n8n-nodes-evolution-api.evolutionApi",
      "typeVersion": 1,
      "position": [
        1820,
        -1260
      ],
      "id": "reply-coach",
      "name": "Responder Coach",
      "credentials": {
        "evolutionApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "messages-api",
        "instanceName": "FoodSnap",
        "remoteJid": "={{ $('Normalizar Dados').item.json.number }}",
        "messageText": "\ud83d\udcf8 Recebi sua foto! Analisando o prato... \u23f3",
        "options_message": {}
      },
      "type": "n8n-nodes-evolution-api.evolutionApi",
      "typeVersion": 1,
      "position": [
        500,
        -740
      ],
      "id": "ack-food",
      "name": "Ack Food",
      "credentials": {
        "evolutionApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "chat-api",
        "operation": "get-media-base64",
        "instanceName": "FoodSnap",
        "messageId": "={{ $('Normalizar Dados').item.json.message_id }}"
      },
      "type": "n8n-nodes-evolution-api.evolutionApi",
      "typeVersion": 1,
      "position": [
        720,
        -740
      ],
      "id": "get-image-food",
      "name": "Baixar IMG Food",
      "credentials": {
        "evolutionApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "toBinary",
        "sourceProperty": "data.base64",
        "options": {}
      },
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        940,
        -740
      ],
      "id": "convert-binary-food",
      "name": "Bin\u00e1rio Food"
    },
    {
      "parameters": {
        "resource": "image",
        "operation": "analyze",
        "modelId": {
          "__rl": true,
          "value": "models/gemini-1.5-flash",
          "mode": "list",
          "cachedResultName": "models/gemini-1.5-flash"
        },
        "text": "=Voc\u00ea \u00e9 um assistente nutricional... (Prompt Original de Comida)... Retorne SOMENTE JSON.",
        "inputType": "binary",
        "simplify": false,
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "typeVersion": 1,
      "position": [
        1160,
        -740
      ],
      "id": "gemini-food",
      "name": "Gemini Food",
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Limpeza de JSON da Comida (Original)\nconst raw = $json?.candidates?.[0]?.content?.parts?.[0]?.text || '{}';\n// ... l\u00f3gica existente de parse do FoodSnap ...\nreturn [{\n  items: [],\n  total: { calories: 500, protein: 30 },\n  tip: { text: \"Exemplo de an\u00e1lise de comida\" }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1380,
        -740
      ],
      "id": "parse-food",
      "name": "Parse Food",
      "notes": "L\u00f3gica completa de parse de comida aqui (resumida para o arquivo)"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "insert into public.food_analyses ... (SQL Original)",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1600,
        -740
      ],
      "id": "save-food-db",
      "name": "Salvar Food DB",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "messages-api",
        "instanceName": "FoodSnap",
        "remoteJid": "={{ $('Normalizar Dados').item.json.number }}",
        "messageText": "=\ud83e\udd57 *FoodSnap*: Calorias: {{$json.total.calories}} ... (Formato Original)",
        "options_message": {}
      },
      "type": "n8n-nodes-evolution-api.evolutionApi",
      "typeVersion": 1,
      "position": [
        1820,
        -740
      ],
      "id": "reply-food",
      "name": "Responder Food",
      "credentials": {
        "evolutionApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "messages-api",
        "instanceName": "FoodSnap",
        "remoteJid": "={{ $('Normalizar Dados').item.json.number }}",
        "messageText": "\u26a0\ufe0f Por favor, envie uma *imagem* para an\u00e1lise.",
        "options_message": {}
      },
      "type": "n8n-nodes-evolution-api.evolutionApi",
      "typeVersion": 1,
      "position": [
        260,
        -500
      ],
      "id": "msg-no-image",
      "name": "Sem Imagem",
      "credentials": {
        "evolutionApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Webhook (Whatsapp)": {
      "main": [
        [
          {
            "node": "Normalizar Dados",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalizar Dados": {
      "main": [
        [
          {
            "node": "\u00c9 Coach?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u00c9 Coach?": {
      "main": [
        [
          {
            "node": "Valida\u00e7\u00e3o Coach",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Valida\u00e7\u00e3o Food",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Valida\u00e7\u00e3o Coach": {
      "main": [
        [
          {
            "node": "Coach OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Valida\u00e7\u00e3o Food": {
      "main": [
        [
          {
            "node": "Food OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Coach OK?": {
      "main": [
        [
          {
            "node": "Ack Coach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Food OK?": {
      "main": [
        [
          {
            "node": "Ack Food",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ack Coach": {
      "main": [
        [
          {
            "node": "Baixar IMG Coach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ack Food": {
      "main": [
        [
          {
            "node": "Baixar IMG Food",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Baixar IMG Coach": {
      "main": [
        [
          {
            "node": "Bin\u00e1rio Coach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Baixar IMG Food": {
      "main": [
        [
          {
            "node": "Bin\u00e1rio Food",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bin\u00e1rio Coach": {
      "main": [
        [
          {
            "node": "Gemini Coach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bin\u00e1rio Food": {
      "main": [
        [
          {
            "node": "Gemini Food",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini Coach": {
      "main": [
        [
          {
            "node": "Parse Coach JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini Food": {
      "main": [
        [
          {
            "node": "Parse Food",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Coach JSON": {
      "main": [
        [
          {
            "node": "Salvar Coach DB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Food": {
      "main": [
        [
          {
            "node": "Salvar Food DB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Salvar Coach DB": {
      "main": [
        [
          {
            "node": "Responder Coach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Salvar Food DB": {
      "main": [
        [
          {
            "node": "Responder Food",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

FoodSnap - Unified (Food & Coach). Uses postgres, n8n-nodes-evolution-api, googleGemini. Webhook trigger; 22 nodes.

Source: https://github.com/marciobever/foodsnap/blob/bc763b0f807c2f9d31b45d7c90aa572573736249/src/n8n-foodsnap-unified.json — 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

Livelo - disparo. Uses googleSheets, n8n-nodes-evolution-api, googleGemini. Webhook trigger; 7 nodes.

Google Sheets, N8N Nodes Evolution Api, Google Gemini
AI & RAG

Eu Clara – Funil Kiwify Completo. Uses postgres, openAi, httpRequest, gmail. Webhook trigger; 70 nodes.

Postgres, OpenAI, HTTP Request +1
AI & RAG

Lua Nova - Sistema Completo. Uses postgres, httpRequest, openAi. Webhook trigger; 55 nodes.

Postgres, HTTP Request, OpenAI
AI & RAG

User Signup & Verification: The workflow starts when a user signs up. It generates a verification code and sends it via SMS using Twilio. Code Validation: The user replies with the code. The workflow

Postgres, HTTP Request, OpenAI +2
AI & RAG

This workflow automates the process of generating stylized product photos for e-commerce by combining real product shots with creative templates. It enables the creation of a complete set of images fo

Airtable, HTTP Request, Google Gemini