AutomationFlowsAI & RAG › Bot Roveeb

Bot Roveeb

Bot ROVEEb. Uses openAi, dataTable, telegram, spreadsheetFile. Webhook trigger; 31 nodes.

Webhook trigger★★★★★ complexityAI-powered31 nodesOpenAIData TableTelegramSpreadsheet FileTelegram Trigger
AI & RAG Trigger: Webhook Nodes: 31 Complexity: ★★★★★ AI nodes: yes Added:
Bot Roveeb — n8n workflow card showing OpenAI, Data Table, Telegram integration

This workflow follows the OpenAI → Telegram 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
{
  "name": "Bot ROVEEb",
  "nodes": [
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.message.text }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    },
                    "id": "0f04348c-a5af-4874-a8cd-8da747c5271f"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "text"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "a122a7ee-7f24-4d3b-aaf5-267688ed7176",
                    "leftValue": "={{ $json.message.voice.file_id }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "voice"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "507ebe51-f956-4fdd-bd2b-6589a7e3c206",
                    "leftValue": "={{ $json.message.document.file_id }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "csv"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        -2320,
        -672
      ],
      "id": "571cb7f5-a835-4bb7-8ea9-61792de2f361",
      "name": "Switch"
    },
    {
      "parameters": {
        "resource": "audio",
        "operation": "transcribe",
        "options": {
          "language": "pt"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        -1888,
        -656
      ],
      "id": "1dbe237c-c086-4c74-a292-1cab20d9e640",
      "name": "OpenAI",
      "retryOnFail": true,
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5-nano",
          "mode": "list",
          "cachedResultName": "GPT-5-NANO"
        },
        "responses": {
          "values": [
            {
              "content": "=Voc\u00ea \u00e9 um extrator de transa\u00e7\u00f5es financeiras em PT-BR no n8n. Sua tarefa \u00e9 LER o texto bruto e DEVOLVER apenas JSON v\u00e1lido seguindo o esquema abaixo.\n\nEntrada (texto bruto):\n{{$json[\"message\"]}}\n\nSa\u00edda (obrigat\u00f3ria, SOMENTE JSON v\u00e1lido):\n{\"transactions\":[ ... ]} \nSe nada for encontrado: {\"transactions\":[]}\n\nPara cada item em \"transactions\" use exatamente estas chaves:\n- categoria: UMA dentre {Alimenta\u00e7\u00e3o,Transporte,Moradia,Lazer,Sa\u00fade,Compras,Servi\u00e7os,Educa\u00e7\u00e3o,D\u00edvidas/Empr\u00e9stimos,Cart\u00e3o de Cr\u00e9dito,Viagem,Outros}. Se for ENTRADA, use \"Receita\".\n- descricao: resumo curto, normalizado e leg\u00edvel (ex.: \"Almo\u00e7o no X\", \"Gasolina no Y\"). N\u00c3O escreva \u201ccompra de\u201d, \u201crecebido de\u201d, \u201ccategoria\u2026\u201d.\n- valor: n\u00famero BRL com ponto decimal (ex.: 50.00). Converta valores por extenso (ex.: \"cinquenta reais\" \u2192 50.00).\n- data: AAAA-MM-DD. Inferir \u201choje/ontem/dia da semana\u201d considerando hoje = {{$now.setZone('America/Sao_Paulo').toFormat('yyyy-MM-dd HH:mm:ss')}}. Se ausente, usar a data de hoje.\n- tipo: \"entrada\" (receita/sal\u00e1rio/recebido) ou \"saida\" (despesa/compra/custo).\n\nREGRAS DE NORMALIZA\u00c7\u00c3O (aplique antes de classificar):\n1) Remova caracteres de ru\u00eddo em nomes/descri\u00e7\u00f5es: asterisco (*) , barras (/), sublinhado (_), h\u00edfen isolado (- quando usado como separador), pontos isolados (.) e m\u00faltiplos espa\u00e7os \u2192 substitua por um \u00fanico espa\u00e7o.\n2) Aparar espa\u00e7os no in\u00edcio/fim.\n3) \"Formato de frase\": \n   - Coloque iniciais de palavras em Mai\u00fasculas e demais min\u00fasculas (Title Case), mantendo min\u00fasculas para conectivos/curtas: {de, da, do, das, dos, e, em, no, na, nos, nas, com, para, por}.\n   - Preserve siglas comuns (2\u20134 letras) se vierem todas em mai\u00fasculas (ex.: \"USP\", \"DM\u201d, \"IFD\" permanecem mai\u00fasculas).\n4) Exemplos:\n   - \"PET LOVE*CLUBE\" \u2192 \"Pet Love Clube\"\n   - \"UBER * PENDING\" \u2192 \"Uber Pending\"\n   - \"FARMACIA SAO JOAO\" \u2192 \"Farmacia S\u00e3o Jo\u00e3o\" (n\u00e3o invente acentos se n\u00e3o souber)\n\nREGRAS DE VALOR:\n- Aceite formatos: \"R$ 3.032,81\", \"R$ -50,00\", \"50,00\", \"-12,06\", ou por extenso (\"cinquenta reais\").\n- Converter para n\u00famero com ponto decimal: 3032.81, -50.00, etc.\n- Se o valor for negativo, respeite o sinal no n\u00famero final.\n- Se houver mais de um valor no texto, escolha o da transa\u00e7\u00e3o em quest\u00e3o.\n\nREGRAS DE DATA:\n- Se houver \"hoje/ontem/anteontem\" ou \"segunda/ter\u00e7a/...\": infira a data relativa usando a refer\u00eancia de \"hoje\" acima (America/Sao_Paulo).\n- Se a data estiver ausente, use a data de hoje.\n- Se houver dia e m\u00eas sem ano, use o ano corrente.\n\nREGRAS DE TIPO:\n- \"entrada\" se o texto indicar cr\u00e9dito/recebimento: {recebido, cr\u00e9dito, creditado, estorno, reembolso, sal\u00e1rio, dep\u00f3sito, ajuste a cr\u00e9dito}.\n- Caso contr\u00e1rio, \"saida\".\n- Se o valor vier com sinal negativo mas o contexto indicar PAGAMENTO na fatura do cart\u00e3o (ajuste a cr\u00e9dito por exemplo) ou indicar estorno/reembolso, classifique como \"entrada\" e lance o valor positivo.\n\nREGRAS DE CATEGORIA (heur\u00edsticas, escolha UMA):\n- Viagem: hospedagem/hotel/pousada/airbnb/booking, tarifas de viagem, passagens.\n- Alimenta\u00e7\u00e3o: restaurante, caf\u00e9, iFood, padaria, mercado com itens de consumo imediato; \"almo\u00e7o\", \"jantar\", \"lanche\".\n- Transporte: Uber/99, combust\u00edvel (posto), ped\u00e1gio, estacionamento.\n- Sa\u00fade: farm\u00e1cia, cl\u00ednica, exames, plano de sa\u00fade.\n- Compras: varejo geral (Amazon, Shopee, Magazine Luiza, etc.).\n- Servi\u00e7os: assinaturas/servi\u00e7os digitais (Spotify, Netflix, Wasabi, Contabo, AWS), manuten\u00e7\u00e3o/servi\u00e7o recorrente.\n- Educa\u00e7\u00e3o: escola, curso, mensalidade educacional.\n- Moradia: aluguel, condom\u00ednio, luz, \u00e1gua, internet residencial.\n- Lazer: cinema, eventos, entretenimento n\u00e3o alimentar.\n- Cart\u00e3o de Cr\u00e9dito: tarifas e encargos expl\u00edcitos do cart\u00e3o (anuidade/IOF) quando n\u00e3o se encaixar melhor em outra.\n- D\u00edvidas/Empr\u00e9stimos: parcelas/financiamentos/empr\u00e9stimos banc\u00e1rios.\n- Receita: para entradas.\n- Outros: caso n\u00e3o se enquadre nas anteriores.\n\nREGRAS DE FILTRO:\n- Ignore linhas que sejam totais, rodap\u00e9s, cabe\u00e7alhos ou descri\u00e7\u00f5es gen\u00e9ricas sem transa\u00e7\u00e3o (ex.: \"Pagamentos Validos Normais\", \"Total da Fatura\").\n\nFORMATO FINAL:\n- Extraia TODAS as transa\u00e7\u00f5es que encontrar na entrada.\n- N\u00e3o inclua coment\u00e1rios, textos extras ou quebras indevidas \u2014 SOMENTE o JSON v\u00e1lido pedido.\n- Quando houver hospedagem na descri\u00e7\u00e3o, categorize como \"Viagem\".\n\nExemplo de sa\u00edda:\n{\"transactions\":[\n  {\"categoria\":\"Compras\",\"descricao\":\"Pet Love Clube\",\"valor\":12.06,\"data\":\"2025-11-04\",\"tipo\":\"saida\"},\n  {\"categoria\":\"Servi\u00e7os\",\"descricao\":\"Spotify\",\"valor\":40.90,\"data\":\"2025-11-04\",\"tipo\":\"saida\"}\n]}\n"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2,
      "position": [
        -1408,
        -624
      ],
      "id": "93d66d70-6ccd-4473-ac53-e3ab71275121",
      "name": "Message a model",
      "retryOnFail": true,
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ad1ddc47-fe87-4364-95ae-c9945a2dd4cf",
              "name": "message",
              "value": "={{ $json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1744,
        -656
      ],
      "id": "249efe3c-0b4d-4f59-9820-d1941062764b",
      "name": "Set Message from Audio"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "6e1e69d7-6962-4646-a064-665ed8ea089e",
              "name": "message",
              "value": "={{ $json.message.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1888,
        -880
      ],
      "id": "439990a1-fc59-4de4-a3a7-96dcf4e9e56b",
      "name": "Set Message from Text"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "3038539e-653b-40da-920b-69ea72ec3dc0",
              "name": "parsedJson",
              "value": "={{ JSON.parse($json.output[0].content[0].text) }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -928,
        -624
      ],
      "id": "571a82bb-62c7-4abc-96dc-a40f9515364f",
      "name": "Parse AI JSON"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "fb611a02-1d8d-4726-b701-075738cbe52a",
              "leftValue": "={{ $json.parsedJson.transactions.length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -768,
        -592
      ],
      "id": "124574b2-c1c3-4590-a255-0d003bba7222",
      "name": "Has transactions?"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "73c5a33d-8319-4b29-b377-cb0888c272ce",
              "name": "message",
              "value": "N\u00e3o consegui encontrar nenhuma transa\u00e7\u00e3o no seu \u00e1udio/texto. \ud83d\ude41",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        48,
        -480
      ],
      "id": "168511b6-07b0-44e4-9adc-554874508983",
      "name": "Fail message"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "73c5a33d-8319-4b29-b377-cb0888c272ce",
              "name": "message",
              "value": "=*{{ $json.user }}!* \ud83d\udc4b\n\nSeu lan\u00e7amento foi feito com sucesso:\n\n\u2705 *Tipo:* {{ $json.type }}\n\ud83d\uddd3\ufe0f *Data:* {{ DateTime.fromISO($('Edit Fields').item.json.date).toFormat('dd/MM/yyyy') }}\n\ud83e\uddfe *Descri\u00e7\u00e3o:* {{ $json.description }}\n\ud83d\udcb0 *Valor:* R$ {{ $json.value.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}\n\ud83c\udff7\ufe0f *Categoria:* {{ $json.category }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        80,
        -704
      ],
      "id": "5fd4b4c1-1b8c-4dc0-b025-5f0627e15d04",
      "name": "Sucess message"
    },
    {
      "parameters": {
        "dataTableId": {
          "__rl": true,
          "value": "rcz6CPiAZbvYcdSw",
          "mode": "list",
          "cachedResultName": "video",
          "cachedResultUrl": "/projects/Fk5PIHrixMIVGbVy/datatables/rcz6CPiAZbvYcdSw"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "value": "={{ $json.value }}",
            "date": "={{ $json.date }}",
            "type": "={{ $json.type }}",
            "category": "={{ $json.category }}",
            "description": "={{ $json.description }}",
            "user": "={{ $('Map Chat').item.json.user }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "value",
              "displayName": "value",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "date",
              "displayName": "date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "type",
              "displayName": "type",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "category",
              "displayName": "category",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "description",
              "displayName": "description",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "user",
              "displayName": "user",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1,
      "position": [
        -48,
        -704
      ],
      "id": "d119a50b-1760-4533-b506-974992f8b3ca",
      "name": "Save"
    },
    {
      "parameters": {
        "content": "## Receive message",
        "height": 272,
        "width": 368,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2816,
        -768
      ],
      "typeVersion": 1,
      "id": "e6344816-956a-493f-a33d-3f2aa83c7321",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## Extract informations",
        "height": 1008,
        "width": 1440,
        "color": 2
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2432,
        -1056
      ],
      "typeVersion": 1,
      "id": "7cb26b1b-2330-48ef-a995-4b658f2da0f6",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## Store and response to telegram",
        "height": 576,
        "width": 1536,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -976,
        -912
      ],
      "typeVersion": 1,
      "id": "1bae0ed3-36f1-43f9-85ac-befc9d150747",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "fieldToSplitOut": "parsedJson.transactions",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        -624,
        -752
      ],
      "id": "a078b6db-bb12-4c13-a0c2-07d2d9f9ef2f",
      "name": "Split Out"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a3ba42dc-84ea-4833-acd8-e77f4f7d4ddf",
              "name": "type",
              "value": "={{ $json.tipo }}",
              "type": "string"
            },
            {
              "id": "fe50a7bf-d43c-43b0-bb2d-c0d9ac9cd909",
              "name": "date",
              "value": "={{ $json.data }}",
              "type": "string"
            },
            {
              "id": "f9afcd79-07ca-47fb-88f9-11b358e58417",
              "name": "description",
              "value": "={{ $json.descricao }}",
              "type": "string"
            },
            {
              "id": "2b5aa2b9-7349-4f6d-91e0-ec34e2416be7",
              "name": "value",
              "value": "={{ $json.valor }}",
              "type": "string"
            },
            {
              "id": "3c5215ad-b557-4772-a5b2-cc1997ae142b",
              "name": "category",
              "value": "={{ $json.categoria }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -192,
        -704
      ],
      "id": "c31405d8-08a3-4812-a1ad-63448098577e",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -384,
        -768
      ],
      "id": "f6f84425-e963-4496-97eb-b7d6ab5ef12e",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "42bd45d0-768b-4c47-bb1b-fdc9f5480025",
              "leftValue": "={{ $json.message.document.mime_type }}",
              "rightValue": "csv",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "12f09a33-f1b2-435b-88bf-0f593d50159f",
              "leftValue": "={{ $json.message.document.file_name }}",
              "rightValue": "csv",
              "operator": {
                "type": "string",
                "operation": "endsWith"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -2160,
        -432
      ],
      "id": "da8111a7-6770-406b-9405-4c5f7c10b6d3",
      "name": "If"
    },
    {
      "parameters": {
        "resource": "file",
        "fileId": "={{ $('Telegram Trigger').item.json.message.document.file_id }}",
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -1952,
        -448
      ],
      "id": "dc9b8143-abf7-44ae-8b15-00c436900781",
      "name": "Get CSV",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "file",
        "fileId": "={{ $('Telegram Trigger').item.json.message.voice.file_id }}",
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -2032,
        -656
      ],
      "id": "76aa214a-c682-4a73-9a80-f7cfef756c63",
      "name": "Get audio",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "options": {
          "rawData": true,
          "readAsString": true
        }
      },
      "id": "1860907f-6abe-4fee-8935-8afd0a0ecc04",
      "name": "Convert To Spreadsheet",
      "type": "n8n-nodes-base.spreadsheetFile",
      "position": [
        -1792,
        -448
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "jsCode": "// ======= Configura\u00e7\u00e3o =======\nconst FIELD_CANDIDATES = {\n  desc:   ['Estabelecimento','estabelecimento','descricao','Descri\u00e7\u00e3o','title','Title','merchant','nome','estab'],\n  amount: ['Valor','valor','valor_total','amount','Amount','price'],\n  date:   ['Data','data','date','Date'] // n\u00e3o usado na frase (usamos \"hoje\"), mas deixado para futura expans\u00e3o\n};\n\nconst IGNORE_DESCRIPTIONS = [\n  'pagamentos validos normais',\n  'pagamento v\u00e1lido normal',\n  'pagamento recebido'\n];\n\n// ======= Helpers =======\nconst items = await $input.all();\n\nconst fmtHoje = new Intl.DateTimeFormat('pt-BR', { timeZone: 'America/Sao_Paulo' });\nconst hojeBR = fmtHoje.format(new Date());\n\n// remove BOM/acentos/caixa e normaliza espa\u00e7os\nfunction normalizeKey(k){\n  return String(k||'')\n    .replace(/\\uFEFF/g,'')\n    .normalize('NFD').replace(/[\\u0300-\\u036f]/g,'')\n    .toLowerCase().trim();\n}\nfunction normalizeText(s){\n  return String(s||'')\n    .replace(/\\uFEFF/g,'')\n    .replace(/\\*/g,' ')           // remove asteriscos\n    .replace(/\\s+/g,' ')          // compacta espa\u00e7os\n    .trim();\n}\nfunction normForCompare(s){\n  return normalizeText(s)\n    .normalize('NFD').replace(/[\\u0300-\\u036f]/g,'')\n    .toLowerCase();\n}\nfunction toTitleCase(s){\n  return normalizeText(s).toLowerCase().replace(/\\b([a-z\u00e0-\u00fa])([a-z\u00e0-\u00fa]*)/gi, (_,a,b)=> a.toUpperCase()+b);\n}\n\nfunction pick(obj, candidates){\n  // tenta direto\n  for (const c of candidates) if (obj[c] !== undefined) return obj[c];\n  // tenta por chaves normalizadas\n  const map = new Map(Object.keys(obj).map(k => [normalizeKey(k), k]));\n  for (const c of candidates){\n    const nk = normalizeKey(c);\n    if (map.has(nk)) return obj[map.get(nk)];\n  }\n  return undefined;\n}\n\n// \"R$ 3.032,81\" -> 3032.81 ; \"3032.81\" -> 3032.81 ; \"3.032,81\" -> 3032.81\nfunction parseAmountAny(s){\n  if (s == null) return 0;\n  const str = String(s).trim();\n  // se tem v\u00edrgula como decimal (pt-BR)\n  if (/,/.test(str) && !/^\\d+(\\.\\d+)?$/.test(str)){\n    const cleaned = str.replace(/[R$\\s]/g,'').replace(/\\./g,'').replace(',', '.');\n    const n = Number(cleaned);\n    return Number.isFinite(n) ? n : 0;\n  }\n  // caso \"Nubank\" j\u00e1 venha n\u00famero (ou string en-US)\n  const n = Number(str.replace(/[R$\\s]/g,''));\n  return Number.isFinite(n) ? n : 0;\n}\n\n// ======= Processamento =======\nconst lines = [];\n\nfor (const it of items){\n  const row = it.json ?? it;\n\n  const rawDesc   = pick(row, FIELD_CANDIDATES.desc);\n  const rawAmount = pick(row, FIELD_CANDIDATES.amount);\n\n  const desc = toTitleCase(rawDesc || '');\n  const descCmp = normForCompare(desc);\n\n  // ignorar linhas espec\u00edficas\n  if (IGNORE_DESCRIPTIONS.includes(descCmp)) continue;\n\n  const amount = parseAmountAny(rawAmount);\n  if (!desc || !Number.isFinite(amount)) continue;\n\n  const valorBR = Math.abs(amount).toLocaleString('pt-BR', { minimumFractionDigits:2, maximumFractionDigits:2 });\n  lines.push(`Compra de ${valorBR} no ${desc} na data de ${hojeBR}`);\n}\n\n// ======= Sa\u00edda preservando contexto =======\nconst baseBinary = items[0]?.binary ? { ...items[0].binary } : undefined;\n\nconst out = {\n  json: { message: lines.join('\\n') },\n  pairedItem: items.map((_, i) => ({ item: i }))\n};\nif (baseBinary) out.binary = baseBinary;\n\nreturn [out];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1616,
        -448
      ],
      "id": "a07ac806-c9f1-49ef-8785-7a51bd7cbff3",
      "name": "Code in JavaScript"
    },
    {
      "parameters": {
        "jsCode": "const items = await $input.all();\n\nfunction pickName(from) {\n  if (!from) return '';\n  // tenta username; se n\u00e3o houver, usa \"first_name last_name\" (quando houver)\n  const fname = from.first_name;\n  return fname;\n}\n\nreturn items.map((it, idx) => {\n  const j = it.json ?? it;\n\n  const message = j.message ?? j; // aceita quando j\u00e1 est\u00e1 \"achatado\"\n  const chat    = message.chat ?? j.chat ?? {};\n  const from    = message.from ?? j.from ?? {};\n\n  const chatId     = chat.id ?? null;\n  const chatType   = chat.type ?? null;\n  const chatTitle  = chat.title ?? null;\n  const fromId     = from.id ?? null;\n  const userLabel  = pickName(from);\n\n  return {\n    json: {\n      ...j,\n      chat_id: chatId,\n      chat_type: chatType,\n      chat_title: chatTitle,\n      from_id: fromId,\n      user: userLabel\n    },\n    binary: it.binary,\n    pairedItem: { item: idx }\n  };\n});\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2608,
        -704
      ],
      "id": "ec97b8f8-a3e6-49d5-b752-c3e699eb6eac",
      "name": "Map Chat"
    },
    {
      "parameters": {
        "chatId": "={{ $('Map Chat').item.json.chat_id }}",
        "text": "={{ $json.message }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        368,
        -720
      ],
      "id": "e1611863-a363-4687-9a74-5572bcdcddad",
      "name": "Send message",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const items = await $input.all();\n\n// Coleta todas as mensagens\nconst msgs = items.map(i => i.json?.message).filter(Boolean);\n\n// Junta respeitando limite do Telegram\nconst MAX = 3800;\nconst out = [];\nlet bucket = \"\";\n\nfor (const m of msgs) {\n  const piece = (bucket ? \"\\n\\n\" : \"\") + m;\n  if ((bucket.length + piece.length) > MAX) {\n    out.push({ json: { message: bucket } });\n    bucket = m;\n  } else {\n    bucket += piece;\n  }\n}\nif (bucket) out.push({ json: { message: bucket } });\n\nreturn out.map(o => ({\n  ...o,\n  pairedItem: [{ item: 0 }]\n}));\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -192,
        -864
      ],
      "id": "a8020ed1-f874-4d59-b864-8d5a31034456",
      "name": "Aggregate"
    },
    {
      "parameters": {
        "content": "## Retrive data and generate HTML view",
        "height": 416,
        "width": 1376,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2832,
        64
      ],
      "typeVersion": 1,
      "id": "34cb849c-8277-4799-9da0-4ddf0971a108",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "path": "finances-video",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -2800,
        224
      ],
      "id": "ba31b11d-b969-4736-b21c-3b3004006057",
      "name": "Webhook"
    },
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "value": "rcz6CPiAZbvYcdSw",
          "mode": "list",
          "cachedResultName": "video",
          "cachedResultUrl": "/projects/Fk5PIHrixMIVGbVy/datatables/rcz6CPiAZbvYcdSw"
        },
        "filters": {
          "conditions": [
            {
              "keyName": "date",
              "condition": "gte",
              "keyValue": "={{ $json.start }}"
            },
            {
              "keyName": "date",
              "condition": "lte",
              "keyValue": "={{ $json.end }}"
            }
          ]
        },
        "returnAll": true
      },
      "name": "List Finance (Data Table)",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1,
      "position": [
        -2288,
        224
      ],
      "id": "e5fd6190-dc90-47eb-a766-78e48c510dde",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "// ================= helpers =================\nfunction isEntrada(t){ const v=(t||'').toLowerCase(); return ['entrada','receita','salario','sal\u00e1rio','recebido','recebidos'].some(x=>v.includes(x)); }\nfunction isSaida(t){ const v=(t||'').toLowerCase(); return ['saida','sa\u00edda','despesa','despesas','compra','custo'].some(x=>v.includes(x)); }\nfunction brl(x){ return (Number(x)||0).toLocaleString('pt-BR',{minimumFractionDigits:2, maximumFractionDigits:2}); }\nfunction normDate(s){ if(!s) return null; if(/^\\d{4}-\\d{2}$/.test(s)) return s + '-01'; return s; }\n\n// N\u00c3O CONVERTE PARA Date: usa s\u00f3 a string YYYY-MM-DD\nfunction ymdToBR(ymd){\n  const s = (ymd||'').slice(0,10);\n  const [y,m,d] = s.split('-');\n  return (y && m && d) ? `${d}/${m}/${y}` : '';\n}\n\nfunction ptMonthYear(ym){\n  const [y, m] = (ym||'').split('-').map(Number);\n  // Representa\u00e7\u00e3o est\u00e1tica sem usar Date: m\u00eas por nome em pt-BR\n  const meses = ['janeiro','fevereiro','mar\u00e7o','abril','maio','junho','julho','agosto','setembro','outubro','novembro','dezembro'];\n  if (!y || !m) return ym || '';\n  return `${meses[m-1]} de ${y}`;\n}\n\n// ================ inputs =====================\nconst datesItem = $items('Set Infos', 0, 0)[0]?.json || {};\nconst fromStrIn = datesItem.start;\nconst toStrIn   = datesItem.end;\nconst initialBalance = Number(datesItem.initialBalance ?? datesItem.ib ?? 0) || 0;\n\nconst from = normDate(fromStrIn);\nconst to   = normDate(toStrIn) || new Date().toISOString().slice(0,10);\n\n// Limites em STRING (sem timezone)\nconst fromY = (from || '0000-00-00').slice(0,10);\nconst toY   = (to   || '9999-12-31').slice(0,10);\n\nif (!fromY || fromY === '0000-00-00') {\n  return [{ json: { html_content: `<!doctype html><meta charset=\"utf-8\"><meta name=\"color-scheme\" content=\"light\"><body style=\"font:14px system-ui;background:#fff;color:#111\"><h3>Par\u00e2metro ?start=AAAA-MM-DD (ou AAAA-MM) \u00e9 obrigat\u00f3rio</h3></body>` } }];\n}\n\n// rows de entrada (Data Table -> Code)\nconst rows = (await $input.all()).map(i => i.json);\n\n// normaliza (sem parse de data)\nconst normalized = rows.map(r=>({\n  date: r.date,                         // mant\u00e9m original\n  ymd: (r.date||'').slice(0,10),        // YYYY-MM-DD (base para tudo)\n  ym:  (r.date||'').slice(0,7),         // YYYY-MM\n  value: Number(r.value)||0,\n  type: r.type||'',\n  description: r.description||'',\n  category: r.category||'',\n  user: r.user || ''\n}));\n\n// filtra por string (evita timezone)\nconst inRange = normalized.filter(r => r.ymd && r.ymd >= fromY && r.ymd <= toY);\n\n// meses presentes\nconst months = Array.from(new Set(inRange.map(x=>x.ym))).filter(Boolean).sort();\n\n// agrega\u00e7\u00e3o mensal\nconst perMonth = months.map(m=>{\n  const xs = inRange.filter(x=>x.ym===m);\n  const recebidos = xs.filter(x=>isEntrada(x.type)).reduce((a,b)=>a+b.value,0);\n  const despesas  = xs.filter(x=>isSaida(x.type)).reduce((a,b)=>a+b.value,0);\n  return { month:m, recebidos, despesas, saldo: recebidos - despesas };\n});\n\n// totais do per\u00edodo\nconst totalRecebidos = inRange.filter(x=>isEntrada(x.type)).reduce((a,b)=>a+b.value,0);\nconst totalDespesas  = inRange.filter(x=>isSaida(x.type)).reduce((a,b)=>a+b.value,0);\nconst saldoPeriodo   = totalRecebidos - totalDespesas;\n\n// \u2014\u2014 saldo acumulado (sem converter datas) \u2014\u2014\nconst timeline = inRange\n  .map(x => ({ ...x, delta: isEntrada(x.type) ? Number(x.value||0) : -Number(x.value||0) }))\n  .sort((a,b) => a.ymd.localeCompare(b.ymd) || a.description.localeCompare(b.description));\n\nlet running = initialBalance;\nconst saldoAcumSeries = timeline.map(t => {\n  running += t.delta;\n  return { ymd: t.ymd, label: ymdToBR(t.ymd), balance: Number(running.toFixed(2)) };\n});\nconst saldoInicial = initialBalance;\nconst saldoFinal   = Number((initialBalance + saldoPeriodo).toFixed(2));\n\n// ---- s\u00e9ries linha (recebidos\u00d7despesas) ----\nlet lineLabels = [];\nlet serieRecebidos = [];\nlet serieDespesas  = [];\nconst isSingleMonth = months.length === 1;\n\nif (isSingleMonth) {\n  const days = Array.from(new Set(inRange.map(x=>x.ymd))).sort(); // YYYY-MM-DD\n  lineLabels = days.map(d => ymdToBR(d));\n  const recMap = new Map(), desMap = new Map();\n  for (const d of days) { recMap.set(d, 0); desMap.set(d, 0); }\n  for (const it of inRange) {\n    if (isEntrada(it.type)) recMap.set(it.ymd, (recMap.get(it.ymd) || 0) + it.value);\n    else if (isSaida(it.type)) desMap.set(it.ymd, (desMap.get(it.ymd) || 0) + it.value);\n  }\n  serieRecebidos = days.map(d => Number((recMap.get(d)||0).toFixed(2)));\n  serieDespesas  = days.map(d => Number((desMap.get(d)||0).toFixed(2)));\n} else {\n  lineLabels     = perMonth.map(x=> ptMonthYear(x.month));\n  serieRecebidos = perMonth.map(x=> Number(x.recebidos.toFixed(2)));\n  serieDespesas  = perMonth.map(x=> Number(x.despesas.toFixed(2)));\n}\n\n// ---- gr\u00e1fico da direita (din\u00e2mico) ----\nlet rightLabels, rightData, rightTitle;\nif (isSingleMonth) {\n  rightLabels    = ['Receitas', 'Despesas'];\n  rightData      = [Number(totalRecebidos.toFixed(2)), Number(totalDespesas.toFixed(2))];\n  rightTitle     = 'Receitas \u00d7 Despesas';\n} else {\n  rightLabels    = perMonth.map(x => ptMonthYear(x.month));\n  rightData      = perMonth.map(x => Number(x.saldo.toFixed(2)));\n  rightTitle     = 'Saldo por m\u00eas';\n}\n\n// \u2014\u2014 despesas por categoria (barras) \u2014\u2014\nconst catMap = new Map();\nfor (const r of inRange) {\n  if (isSaida(r.type)) {\n    const k = r.category || 'Sem categoria';\n    catMap.set(k, (catMap.get(k) || 0) + (Number(r.value) || 0));\n  }\n}\nconst catLabels = Array.from(catMap.keys());\nconst catValues = Array.from(catMap.values());\nconst catTitle  = `Despesas por categoria ${isSingleMonth ? '(do m\u00eas)' : '(no per\u00edodo)'}`;\n\n// tabela saldos por m\u00eas\nconst saldoRows = perMonth.map(x =>\n  `<tr>\n     <td>${ptMonthYear(x.month)}</td>\n     <td class=\"td-right\">R$ ${brl(x.recebidos)}</td>\n     <td class=\"td-right\">R$ ${brl(x.despesas)}</td>\n     <td class=\"td-right\" style=\"font-weight:600;color:${x.saldo>=0?'#1a7f37':'#c62828'}\">R$ ${brl(x.saldo)}</td>\n   </tr>`\n).join('');\n\n// lan\u00e7amentos (ordenados por data ASC)\nconst lancamentos = [...inRange].sort((a,b)=> a.ymd.localeCompare(b.ymd) || a.description.localeCompare(b.description));\nconst lancRows = lancamentos.map(x=>{\n  const isIn = isEntrada(x.type);\n  const color = isIn ? '#1a7f37' : '#c62828';\n  const sign = isIn ? '+' : '-';\n  return `<tr>\n    <td>${ymdToBR(x.ymd)}</td>\n    <td>${x.user || ''}</td>\n    <td>${x.description || ''}</td>\n    <td>${x.category || ''}</td>\n    <td class=\"td-right\" style=\"color:${color};font-weight:600\">${sign} R$ ${brl(x.value)}</td>\n  </tr>`;\n}).join('');\n\n// =================== HTML =====================\nconst html = `<!doctype html>\n<html lang=\"pt-BR\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<meta name=\"color-scheme\" content=\"light\">\n<title>Relat\u00f3rio Financeiro (${fromY} a ${toY})</title>\n<style>\n  :root{\n    --bg:#ffffff; --card:#ffffff; --text:#0b1220; --muted:#5f6b7a; --border:#e6eaf2;\n    --pos:#1a7f37; --neg:#c62828; --shadow:0 8px 20px rgba(0,0,0,.06); --radius:14px; --wrap:1080px;\n  }\n  *{box-sizing:border-box}\n  html,body{margin:0; background:#ffffff; color:var(--text); font:14px/1.45 ui-sans-serif,system-ui,Inter,Roboto,Arial;}\n  .wrap{max-width:var(--wrap); margin:32px auto; padding:0 20px;}\n  header{display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:18px;}\n  h1{margin:0; font-weight:700; font-size:26px; letter-spacing:.2px;}\n  .badge{display:inline-flex; gap:8px; align-items:center; padding:8px 12px; border:1px solid var(--border); border-radius:999px; background:#f3f4f6; color:#374151; font-weight:700;}\n  .cards{display:grid;grid-template-columns:repeat(12,1fr); gap:16px; margin:18px 0;}\n  .card{grid-column:span 12; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); padding:16px;}\n  .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:14px;}\n  .kpi{background:var(--card); border:1px solid var(--border); border-radius:12px; padding:14px;}\n  .kpi .label{color:var(--muted); font-size:12px;}\n  .kpi .value{margin-top:6px; font-weight:800; font-size:22px;}\n  .chart-2col{display:grid; grid-template-columns: 1fr 1fr; gap:16px;}\n  .chart-box{border:1px solid var(--border); border-radius:12px; padding:12px; background:var(--card);}\n  .chart-title{margin:0 0 10px 0; font-weight:700; font-size:16px;}\n  @media (max-width: 900px){ .chart-2col{grid-template-columns:1fr} }\n\n  .table-wrap{overflow:auto; border:1px solid var(--border); border-radius:12px; background:var(--card);}\n  table{width:100%; border-collapse:separate; border-spacing:0; min-width:720px;}\n  thead th{position:sticky; top:0; background:#ffffff; color:var(--text); text-align:left; z-index:1; border-bottom:1px solid var(--border);}\n  th, td{padding:10px 12px;}\n  tbody tr:nth-child(odd){background-color:#00000005;}\n  tbody tr:hover{background:#00000008;}\n  .td-right{text-align:right;}\n</style>\n</head>\n<body>\n  <div class=\"wrap\">\n    <header>\n      <h1>Relat\u00f3rio Financeiro</h1>\n      <span class=\"badge\">Per\u00edodo: ${fromY} \u2192 ${toY}</span>\n    </header>\n\n    <section class=\"cards\">\n      <div class=\"card\">\n        <div class=\"kpis\">\n          <div class=\"kpi\"><div class=\"label\">Saldo Inicial</div><div class=\"value\">R$ ${brl(saldoInicial)}</div></div>\n          <div class=\"kpi\"><div class=\"label\">Total Recebidos</div><div class=\"value\" style=\"color:var(--pos)\">R$ ${brl(totalRecebidos)}</div></div>\n          <div class=\"kpi\"><div class=\"label\">Total Despesas</div><div class=\"value\" style=\"color:var(--neg)\">R$ ${brl(totalDespesas)}</div></div>\n          <div class=\"kpi\"><div class=\"label\">Saldo no Per\u00edodo</div><div class=\"value\" style=\"color:${saldoPeriodo>=0?'var(--pos)':'var(--neg)'}\">R$ ${brl(saldoPeriodo)}</div></div>\n          <div class=\"kpi\"><div class=\"label\">Saldo Final</div><div class=\"value\" style=\"color:${saldoFinal>=0?'var(--pos)':'var(--neg)'}\">R$ ${brl(saldoFinal)}</div></div>\n        </div>\n      </div>\n\n      <div class=\"card\">\n        <div class=\"chart-2col\">\n          <div class=\"chart-box\">\n            <h3 class=\"chart-title\">${isSingleMonth ? 'Recebidos \u00d7 Despesas por dia' : 'Recebidos \u00d7 Despesas por m\u00eas'}</h3>\n            <div style=\"height:260px\"><canvas id=\"chartLine\"></canvas></div>\n          </div>\n          <div class=\"chart-box\">\n            <h3 class=\"chart-title\">${isSingleMonth ? 'Receitas \u00d7 Despesas' : 'Saldo por m\u00eas'}</h3>\n            <div style=\"height:260px\"><canvas id=\"chartRight\"></canvas></div>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"card\">\n        <h3 class=\"chart-title\">${catTitle}</h3>\n        ${\n          catLabels.length\n            ? `<div style=\"height:260px\"><canvas id=\"chartCat\"></canvas></div>`\n            : `<div style=\"padding:8px;color:#5f6b7a\">Sem despesas no per\u00edodo.</div>`\n        }\n      </div>\n\n      <div class=\"card\">\n        <h3 class=\"chart-title\">Saldo por m\u00eas</h3>\n        <div class=\"table-wrap\">\n          <table>\n            <thead><tr><th>M\u00eas</th><th class=\"td-right\">Recebidos</th><th class=\"td-right\">Despesas</th><th class=\"td-right\">Saldo</th></tr></thead>\n            <tbody>${perMonth.length ? saldoRows : '<tr><td colspan=\"4\" style=\"padding:16px;color:#5f6b7a\">Sem dados no per\u00edodo.</td></tr>'}</tbody>\n          </table>\n        </div>\n      </div>\n\n      <div class=\"card\">\n        <h3 class=\"chart-title\">Lan\u00e7amentos</h3>\n        <div class=\"table-wrap\">\n          <table>\n            <thead><tr><th>Data</th><th>Quem</th><th>Descri\u00e7\u00e3o</th><th>Categoria</th><th class=\"td-right\">Valor</th></tr></thead>\n            <tbody>${lancRows || '<tr><td colspan=\"5\" style=\"padding:16px;color:#5f6b7a\">Sem lan\u00e7amentos.</td></tr>'}</tbody>\n          </table>\n        </div>\n      </div>\n    </section>\n  </div>\n\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"><\\/script>\n<script>\n(() => {\n  if (!window.Chart) return;\n\n  Chart.defaults.color = '#0b1220';\n  const grid = '#e6eaf2';\n  const tick = '#5f6b7a';\n  Chart.defaults.scales = Chart.defaults.scales || {};\n  Chart.defaults.scales.x = { grid: { color: grid }, ticks: { color: tick } };\n  Chart.defaults.scales.y = { grid: { color: grid }, ticks: { color: tick }, beginAtZero:true };\n\n  // --- line (Recebidos \u00d7 Despesas) ---\n  const lineCtx = document.getElementById('chartLine').getContext('2d');\n  new Chart(lineCtx, {\n    type: 'line',\n    data: {\n      labels: ${JSON.stringify(lineLabels)},\n      datasets: [\n        { label: 'Recebidos', data: ${JSON.stringify(serieRecebidos)}, borderWidth: 2, tension: 0.25, pointRadius: 3 },\n        { label: 'Despesas',  data: ${JSON.stringify(serieDespesas)},  borderWidth: 2, tension: 0.25, pointRadius: 3 }\n      ]\n    },\n    options: {\n      responsive: true,\n      maintainAspectRatio: false,\n      plugins: { legend: { position: 'bottom', labels: { color: '#0b1220' } } },\n      scales: {\n        x:{ grid:{ color: grid }, ticks:{ color: tick } },\n        y:{ grid:{ color: grid }, ticks:{ color: tick }, beginAtZero:true }\n      }\n    }\n  });\n\n  // --- right (pizza 1 m\u00eas; barras empilhadas >1 m\u00eas) ---\n  const rightCtx = document.getElementById('chartRight').getContext('2d');\n  const isSingle = ${JSON.stringify(isSingleMonth)};\n  const labelsRight = ${JSON.stringify(rightLabels)};\n  const dataArrRight = ${JSON.stringify(rightData)};\n\n  const baseOptions = {\n    responsive: true,\n    maintainAspectRatio: false,\n    plugins: { legend: { position: 'bottom', labels: { color: '#0b1220' } } }\n  };\n\n  if (isSingle) {\n    new Chart(rightCtx, {\n      type: 'pie',\n      data: { labels: labelsRight, datasets: [{ data: dataArrRight }] },\n      options: baseOptions\n    });\n  } else {\n    const positivos = dataArrRight.map(v => v > 0 ? v : 0);\n    const negativos = dataArrRight.map(v => v < 0 ? v : 0);\n\n    new Chart(rightCtx, {\n      type: 'bar',\n      data: {\n        labels: labelsRight,\n        datasets: [\n          { label: 'Positivo', data: positivos, backgroundColor: 'rgba(26,127,55,0.25)', borderColor: '#1a7f37', borderWidth: 1, stack: 'saldo' },\n          { label: 'Negativo', data: negativos, backgroundColor: 'rgba(198,40,40,0.20)', borderColor: '#c62828', borderWidth: 1, stack: 'saldo' }\n        ]\n      },\n      options: {\n        ...baseOptions,\n        scales: {\n          x: { stacked: true, grid: { color: grid }, ticks: { color: tick } },\n          y: { stacked: true, grid: { color: grid }, ticks: { color: tick } }\n        }\n      }\n    });\n  }\n\n  // --- despesas por categoria (barras) ---\n  const catLabels = ${JSON.stringify(catLabels)};\n  const catValues = ${JSON.stringify(catValues)};\n  if (catLabels.length) {\n    const catCtx = document.getElementById('chartCat').getContext('2d');\n    new Chart(catCtx, {\n      type: 'bar',\n      data: {\n        labels: catLabels,\n        datasets: [\n          { label: 'Despesas', data: catValues, borderWidth: 1 }\n        ]\n      },\n      options: {\n        responsive: true,\n        maintainAspectRatio: false,\n        plugins: { legend: { position: 'bottom', labels: { color: '#0b1220' } } },\n        scales: {\n          x: { grid: { color: grid }, ticks: { color: tick } },\n          y: { grid: { color: grid }, ticks: { color: tick }, beginAtZero: true }\n        }\n      }\n    });\n  }\n})();\n<\\/script>\n</body>\n</html>`;\n\nreturn [{ json: { html_content: html } }];\n"
      },
      "name": "Generate HTML",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2032,
        224
      ],
      "id": "6434bb32-7f20-4e61-bc44-c6874c0219f5"
    },
    {
      "parameters": {
        "keepOnlySet": true,
        "values": {
          "string": [
            {
              "name": "start",
              "value": "={{ $json.query.start ?? '2024-01-01' }}"
            },
            {
              "name": "end",
              "value": "={{ $json.query.end ?? (new Date().toISOString().slice(0,10)) }}"
            }
          ],
          "number": [
            {
              "name": "initialBalance",
              "value": "={{ Number($json.query.ib) ?? 0 }}"
            }
          ]
        },
        "options": {}
      },
      "name": "Set Infos",
      "type": "n8n-nodes-base.set",
      "typeVersion": 2,
      "position": [
        -2544,
        224
      ],
      "id": "0f5bcd37-894d-476a-a148-44f3d21ebc86"
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "={{ $json[\"html_content\"] }}",
        "options": {}
      },
      "name": "Response with HTML",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        -1728,
        224
      ],
      "id": "4c7bbd6b-aeb2-44af-a910-e7018e822551"
    },
    {
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.2,
      "position": [
        -2768,
        -704
      ],
      "id": "2730c018-093d-4d18-8332-600dda1f0354",
      "name": "Telegram Trigger",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Switch": {
      "main": [
        [
          {
            "node": "Set Message from Text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get audio",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI": {
      "main": [
        [
          {
            "node": "Set Message from Audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Parse AI JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Message from Audio": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Message from Text": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI JSON": {
      "main": [
        [
          {
            "node": "Has transactions?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has transactions?": {
      "main": [
        [
          {
            "node": "Split Out",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fail message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fail message": {
      "main": [
        [
          {
            "node": "Send message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sucess message": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save": {
      "main": [
        [
          {
            "node": "Sucess message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Save",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If": {
      "main": [
        [
          {
            "node": "Get CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get CSV": {
      "main": [
        [
          {
            "node": "Convert To Spreadsheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get audio": {
      "main": [
        [
          {
            "node": "OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert To Spreadsheet": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map Chat": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Send message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Set Infos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Finance (Data Table)": {
      "main": [
        [
          {
            "node": "Generate HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate HTML": {
      "main": [
        [
          {
            "node": "Response with HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Infos": {
      "main": [
        [
          {
            "node": "List Finance (Data Table)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Map Chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "3f5ea76e-6c0d-4c4c-856b-b776c00eae32",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "N1cHhvnFycILn5LP",
  "tags": []
}

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

Bot ROVEEb. Uses openAi, dataTable, telegram, spreadsheetFile. Webhook trigger; 31 nodes.

Source: https://gist.github.com/mateusrovedaa/9a81c2ea328011684568aae89771c5d5 — 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

Monitor and manage Docker containers from Telegram with AI log analysis

OpenAI, Telegram Trigger, Telegram +1
AI & RAG

Ask questions like “How much did I spend on food last month?” and get instant answers from your financial data — directly in Telegram.

Telegram Trigger, OpenAI, Google Sheets +2
AI & RAG

Build a Telegram bot that helps users find AliExpress products using natural language requests. The bot uses OpenAI to optimize search queries, Decodo to scrape product listings, and AI analysis to se

Telegram Trigger, OpenAI, Telegram +3
AI & RAG

Voice Note -> Veo 3 AD. Uses telegramTrigger, telegram, openAi, httpRequest. Event-driven trigger; 49 nodes.

Telegram Trigger, Telegram, OpenAI +3
AI & RAG

Most expense tracker apps (like Money Lover, Spendee, or Wallet) have a common friction point: Data Entry. You have to unlock your phone, find the app, wait for it to load, navigate menus, and manuall

Google Sheets, Google Gemini, Telegram +2