AutomationFlowsEmail & Gmail › Shiny Gmail Lightweight Telegram Rules Manager V4.0 Lean

Shiny Gmail Lightweight Telegram Rules Manager V4.0 Lean

Shiny Gmail Lightweight Telegram Rules Manager V4.0 Lean. Uses telegramTrigger, postgres, telegram. Event-driven trigger; 16 nodes.

Event trigger★★★★☆ complexity16 nodesTelegram TriggerPostgresTelegram
Email & Gmail Trigger: Event Nodes: 16 Complexity: ★★★★☆ Added:

This workflow follows the Postgres → 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": "Shiny Gmail Lightweight Telegram Rules Manager V4.0 Lean",
  "nodes": [
    {
      "parameters": {
        "updates": [
          "message"
        ]
      },
      "id": "785807e3-d13f-4f66-85f8-106fd7217819",
      "name": "Telegram Trigger",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const update=$input.first()?.json||{};const message=update.message||update;const chat=message.chat||{};const text=String(message.text||'').trim();\nconst allowedChatId=String($env.TELEGRAM_CHAT_ID||'').trim();const chatId=String(chat.id||'').trim();const authorized=Boolean(allowedChatId&&chatId===allowedChatId);\nfunction normPattern(s){return String(s||'').trim().toLowerCase()}function isEmail(s){return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(s)}function isDomain(s){return /^[a-z0-9.-]+\\.[a-z]{2,}$/i.test(s)&&!s.includes('@')}function extractDomain(s){return String(s||'').toLowerCase().split('@').pop()}function categoryForRuleType(t){t=String(t||'').toLowerCase();if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown'}\nconst parts=text.split(/\\s+/).filter(Boolean);const cmd=(parts[0]||'').toLowerCase().replace(/@\\S+$/,'');const arg=normPattern(parts[1]||'');\nlet action='fallback',ruleType=null,matchType='exact',senderPattern=arg,valid=true,validationError=null,recentCategory=null,recentCount=5,statsDays=7,recentFilter=null,recentSenderPattern=null,recentIsDomain=false,rulesFilter=null;\nconst validCats=new Set(['priority','finances','accounts-subscriptions','account-security','unimportant','needs-review']);\nconst addMap={'/whitelist':'whitelist','/blacklist':'blacklist','/receipt':'force_finance','/review':'force_review','/whitelistdomain':'whitelist','/blacklistdomain':'blacklist','/receiptdomain':'force_finance','/reviewdomain':'force_review'};\nconst removeMap={'/unwhitelist':'whitelist','/unblacklist':'blacklist','/unreceipt':'force_finance','/unreview':'force_review','/unwhitelistdomain':'whitelist','/unblacklistdomain':'blacklist','/unreceiptdomain':'force_finance','/unreviewdomain':'force_review'};\nif(!authorized)action='unauthorized';\nelse if(cmd==='/help'||cmd==='/start')action='help';\nelse if(cmd==='/rules'){action='rules';rulesFilter=arg||null;}\nelse if(cmd==='/stats'){action='stats';const n=Number(arg||7);statsDays=Number.isFinite(n)?Math.min(Math.max(Math.trunc(n),1),90):7;}\nelse if(cmd==='/recent'){\n action='recent';\n if(arg==='failed'){recentFilter='status';recentCount=Number.isFinite(Number(parts[2]))?Math.min(Math.max(Math.trunc(Number(parts[2])),1),20):5;}\n else if(arg==='warnings'||arg==='warned'){recentFilter='warnings';recentCount=Number.isFinite(Number(parts[2]))?Math.min(Math.max(Math.trunc(Number(parts[2])),1),20):5;}\n else if(validCats.has(arg)){recentCategory=arg;const n=Number(normPattern(parts[2]||'5'));recentCount=Number.isFinite(n)?Math.min(Math.max(Math.trunc(n),1),20):5;}\n else if(isEmail(arg)||isDomain(arg)){recentFilter='sender';recentSenderPattern=arg;recentIsDomain=isDomain(arg)&&!isEmail(arg);const n=Number(normPattern(parts[2]||'5'));recentCount=Number.isFinite(n)?Math.min(Math.max(Math.trunc(n),1),20):5;}\n else if(arg&&!Number.isNaN(Number(arg))){recentCount=Math.min(Math.max(Math.trunc(Number(arg)),1),20);}else if(arg){valid=false;validationError='For /recent, use /recent, /recent failed, /recent warnings, /recent 10, /recent needs-review, /recent sender@example.com, or /recent example.com 10.';}\n}\nelse if(cmd==='/explain'){action='explain';if(!arg){valid=false;validationError='Please provide an email or domain.'}else if(!(isEmail(arg)||isDomain(arg))){valid=false;validationError='Please provide a valid email or domain.'}}\nelse if(addMap[cmd]){action='add_rule';ruleType=addMap[cmd];matchType=cmd.endsWith('domain')?'domain':'exact';if(matchType==='exact'&&!isEmail(arg)){valid=false;validationError='Please provide a valid email address.'}if(matchType==='domain'&&!isDomain(arg)){valid=false;validationError='Please provide a valid domain, for example example.com.'}}\nelse if(removeMap[cmd]){action='remove_rule';ruleType=removeMap[cmd];matchType=cmd.endsWith('domain')?'domain':'exact';if(matchType==='exact'&&!isEmail(arg)){valid=false;validationError='Please provide a valid email address.'}if(matchType==='domain'&&!isDomain(arg)){valid=false;validationError='Please provide a valid domain, for example example.com.'}}\nconst explainDomain=isEmail(arg)?extractDomain(arg):arg;return[{json:{authorized,chatId,text,cmd,arg,action,valid,validationError,account:$env.GMAIL_ACCOUNT||'primary',senderPattern,matchType,ruleType,categoryLabel:categoryForRuleType(ruleType),explainInput:arg,explainDomain,explainInputType:isEmail(arg)?'email':(isDomain(arg)?'domain':'unknown'),recentCategory,recentCount,recentFilter,recentSenderPattern,recentIsDomain,statsDays,rulesFilter}}];"
      },
      "id": "11d872bb-5d26-460f-95c3-6d967c248ab6",
      "name": "Parse And Authorize Command",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "help",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "help",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "unauthorized",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "unauthorized",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "invalid",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ !$json.valid }}",
                    "rightValue": true,
                    "operator": {
                      "type": "boolean",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "add_rule",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "add_rule",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "remove_rule",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "remove_rule",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "rules",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "rules",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "stats",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "stats",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "recent",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "recent",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "explain",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "explain",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "fallbackOutputName": "unknown_command"
        }
      },
      "id": "b6ab98eb-6a8b-443a-959d-64a7a1f29a36",
      "name": "Route Command",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const j=$input.first().json;let text='';\nfunction dist(a,b){const d=Array.from({length:a.length+1},(_,i)=>[i]);for(let j=1;j<=b.length;j++)d[0][j]=j;for(let i=1;i<=a.length;i++)for(let j=1;j<=b.length;j++)d[i][j]=Math.min(d[i-1][j]+1,d[i][j-1]+1,d[i-1][j-1]+(a[i-1]===b[j-1]?0:1));return d[a.length][b.length];}\nconst known=['/help','/rules','/stats','/recent','/explain','/whitelist','/blacklist','/receipt','/review','/whitelistdomain','/blacklistdomain','/receiptdomain','/reviewdomain','/unwhitelist','/unblacklist','/unreceipt','/unreview','/unwhitelistdomain','/unblacklistdomain','/unreceiptdomain','/unreviewdomain'];\nif(j.action==='unauthorized')text='Command unavailable.';else if(!j.valid)text=`\u26a0\ufe0f ${h(j.validationError)}\\n\\nSend /help for commands.`;else if(j.action==='help')text=`\ud83e\uddf9 <b>Shiny Gmail Manager V4.0</b>\\n\\n<b>Labels</b>\\n\ud83d\udd34 priority = real human conversations\\n\ud83d\udcb0 finances = money, receipts, billing, loans, shopping\\n\ud83d\udd14 accounts-subscriptions = services/tools/automation notices\\n\ud83d\udd10 account-security = OTPs, login, password reset, account safety\\n\ud83d\uddd1 unimportant = clutter\\n\ud83d\udd0e needs-review = automation uncertainty\\n\\n<b>Add sender rules</b>\\n<code>/whitelist boss@example.com</code> \u2192 priority\\n<code>/blacklist promo@example.com</code> \u2192 unimportant\\n<code>/receipt billing@example.com</code> \u2192 finances\\n<code>/review unknown@example.com</code> \u2192 needs-review\\n\\n<b>Add domain rules</b>\\n<code>/whitelistdomain company.com</code>\\n<code>/blacklistdomain spamdomain.com</code>\\n<code>/receiptdomain billingvendor.com</code>\\n<code>/reviewdomain ambiguousvendor.com</code>\\n\\n<b>Remove rules</b>\\n<code>/unwhitelist boss@example.com</code>\\n<code>/unblacklist promo@example.com</code>\\n<code>/unreceipt billing@example.com</code>\\n<code>/unreview unknown@example.com</code>\\n<code>/unwhitelistdomain company.com</code>\\n<code>/unblacklistdomain spamdomain.com</code>\\n<code>/unreceiptdomain billingvendor.com</code>\\n<code>/unreviewdomain ambiguousvendor.com</code>\\n\\n<b>Inspect</b>\\n<code>/rules [finances|priority|domain|exact|disabled|all|pattern]</code>\\n<code>/stats [days]</code>\\n<code>/recent</code>\\n<code>/recent failed</code>\\n<code>/recent warnings</code>\\n<code>/recent needs-review 10</code>\\n<code>/recent sender@example.com 5</code>\\n<code>/recent example.com 10</code>\\n<code>/explain sender@example.com</code>`;else{const typed=j.cmd||'';const best=known.map(c=>[c,dist(typed,c)]).sort((a,b)=>a[1]-b[1])[0];text=best&&best[1]<=3?`Unknown command: ${h(typed)}\\n\\nDid you mean: ${h(best[0])}?\\n\\nSend /help for all commands.`:'Send /help for commands.';}return[{json:{chatId:j.chatId||$env.TELEGRAM_CHAT_ID,text:clampTelegram(text)}}];"
      },
      "id": "f28bd1e7-b0bd-4ed8-97a8-c85964453957",
      "name": "Format Simple Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        700,
        -260
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{(()=>{const account=String($json.account).replace(/'/g,\"''\"),pattern=String($json.senderPattern).replace(/'/g,\"''\"),matchType=String($json.matchType).replace(/'/g,\"''\"),ruleType=String($json.ruleType).replace(/'/g,\"''\");return \"WITH disabled AS (UPDATE gmail_sender_rules SET enabled=false, updated_at=NOW() WHERE account='\"+account+\"' AND sender_pattern='\"+pattern+\"' AND match_type='\"+matchType+\"' AND rule_type <> '\"+ruleType+\"' AND enabled=true RETURNING rule_type AS superseded_rule_type), upserted AS (INSERT INTO gmail_sender_rules (account, sender_pattern, match_type, rule_type, priority, enabled, created_at, updated_at) VALUES ('\"+account+\"','\"+pattern+\"','\"+matchType+\"','\"+ruleType+\"',10,true,NOW(),NOW()) ON CONFLICT (account, sender_pattern, match_type, rule_type) DO UPDATE SET enabled=true, updated_at=NOW() RETURNING sender_pattern, match_type, rule_type) SELECT u.sender_pattern, u.match_type, u.rule_type, (SELECT json_agg(disabled.superseded_rule_type) FROM disabled) AS superseded_rule_types FROM upserted u;\";})()}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "33376884-286c-4fca-b603-2e539b12a719",
      "name": "Upsert Rule",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        -20
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{ \"UPDATE gmail_sender_rules SET enabled=false, updated_at=NOW() WHERE account='\"+String($json.account).replace(/'/g,\"''\")+\"' AND sender_pattern='\"+String($json.senderPattern).replace(/'/g,\"''\")+\"' AND match_type='\"+String($json.matchType).replace(/'/g,\"''\")+\"' AND rule_type='\"+String($json.ruleType).replace(/'/g,\"''\")+\"' RETURNING sender_pattern, match_type, rule_type;\" }}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "b0a1202b-6621-4d6c-a975-ec20d3ccb0eb",
      "name": "Disable Rule",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        120
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const original=$('Parse And Authorize Command').first().json;const row=$input.first()?.json||{};if(row.error)return[{json:{chatId:original.chatId,text:clampTelegram(`\u26a0\ufe0f Rule change was not saved.\\n\\nDatabase error. No rule was changed.\\n${String(row.error.message||'Try again later.').slice(0,200)}`)}}];const changed=Boolean(row.sender_pattern);const label=original.categoryLabel||mapRule(row.rule_type);let text=original.action==='add_rule'?`\u2705 Rule saved\\n\\n${original.senderPattern} (${original.matchType}) \u2192 ${emojiCat(label)}`:changed?`\ud83d\uddd1\ufe0f Rule removed\\n\\n${original.senderPattern} (${original.matchType}) \u2192 ${emojiCat(label)}`:`\u2139\ufe0f Rule not found\\n\\n${original.senderPattern} (${original.matchType}) \u2192 ${emojiCat(label)}`;const superseded=row.superseded_rule_types;if(original.action==='add_rule'&&Array.isArray(superseded)&&superseded.length)text+=`\\nSuperseded previous rule(s): ${superseded.map(t=>emojiCat(mapRule(t))).join(', ')}`;return[{json:{chatId:original.chatId,text:clampTelegram(text)}}];"
      },
      "id": "3a0615c4-8d36-4133-a144-ee8cbd00e6ab",
      "name": "Format Rule Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        40
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{(()=>{const account=String($json.account).replace(/'/g,\"''\"),f=String($json.rulesFilter||'').replace(/'/g,\"''\");const like=f.replace(/[\\\\%_]/g,m=>'\\\\'+m);const catMap={priority:[\"whitelist\"],unimportant:[\"blacklist\"],finances:[\"force_finance\",\"finance\",\"receipt\",\"force_receipts\"],\"needs-review\":[\"force_review\",\"review\"]};let where=\" WHERE account='\"+account+\"'\";if(f==='disabled')where+=\" AND enabled=false\";else if(f==='all')where+=\"\";else where+=\" AND enabled=true\";if(f&&f!=='disabled'&&f!=='all'){if(f==='domain'||f==='exact')where+=\" AND match_type='\"+f+\"'\";else if(catMap[f])where+=\" AND rule_type IN (\"+catMap[f].map(x=>\"'\"+x+\"'\").join(',')+\")\";else where+=\" AND sender_pattern ILIKE '%\"+like+\"%' ESCAPE '\\\\'\";}return \"SELECT sender_pattern, match_type, rule_type, priority, enabled FROM gmail_sender_rules\"+where+\" ORDER BY enabled DESC, priority ASC, CASE WHEN match_type='exact' THEN 0 ELSE 1 END, created_at DESC, sender_pattern LIMIT 100;\";})()}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "ffb2cfd2-ea44-421c-ac95-c1c7cbd44b6b",
      "name": "Query Rules",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        260
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const original=$('Parse And Authorize Command').first().json;const first=$input.first()?.json||{};if(first.error)return[{json:{chatId:original.chatId,text:clampTelegram(`\u26a0\ufe0f Database unavailable.\\n\\nCould not load rules safely. Try again later.\\n${String(first.error.message||'').slice(0,200)}`)}}];const rows=$input.all().map(i=>i.json).filter(r=>r.sender_pattern);const exact=rows.filter(r=>r.match_type==='exact'),domains=rows.filter(r=>r.match_type==='domain');const typeCounts={};for(const r of rows){const cat=mapRule(r.rule_type);typeCounts[cat]=(typeCounts[cat]||0)+1;}const comp=Object.entries(typeCounts).map(([cat,n])=>`${emojiCat(cat)}: ${n}`).join(' \u00b7 ');function line(r){const disabled=r.enabled===false||r.enabled==='false';return`\u2022 ${disabled?'\u23f8\ufe0f ':''}${h(r.sender_pattern)} \u2192 ${emojiCat(mapRule(r.rule_type))} \u00b7 p${h(r.priority??10)}${disabled?' (disabled)':''}`;}let text=`\ud83d\udccb <b>Your Gmail Rules</b>${original.rulesFilter?` \u2014 ${h(original.rulesFilter)}`:''}\\nExact senders: ${exact.length} \u00b7 Domains: ${domains.length}`;if(comp)text+=`\\n${h(comp)}`;if(!rows.length)text+='\\n\\nNo rules found for this filter. Send /help for examples.';else{if(exact.length)text+='\\n\\n<b>Exact senders</b>\\n'+exact.map(line).join('\\n');if(domains.length)text+='\\n\\n<b>Domains</b>\\n'+domains.map(line).join('\\n');text+='\\n\\n<i>Rule order: lower priority wins; exact beats domain at same priority; ties use newest first.</i>';}return[{json:{chatId:original.chatId,text:clampTelegram(text)}}];"
      },
      "id": "51ca239b-c2b7-41b3-b0ae-26ab741fd3f9",
      "name": "Format Rules Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        260
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{(()=>{const account=String($json.account).replace(/'/g,\"''\"),days=Math.min(Math.max(Number($json.statsDays||7),1),90);return \"WITH recent AS (SELECT category, source, sender, updated_at, confidence FROM gmail_message_ledger WHERE account='\"+account+\"' AND status='completed' AND updated_at >= NOW() - INTERVAL '\"+days+\" days'), totals AS (SELECT COUNT(*) AS total FROM recent), cats AS (SELECT category, COUNT(*) AS count FROM recent GROUP BY category), sources AS (SELECT source, COUNT(*) AS count FROM recent GROUP BY source), senders AS (SELECT sender, COUNT(*) AS count FROM recent WHERE sender <> '' GROUP BY sender ORDER BY count DESC LIMIT 5), rule_hits AS (SELECT COUNT(*) FILTER (WHERE source NOT IN ('ai','ai_low_confidence','ai_failed')) AS rule_count, COUNT(*) AS total FROM recent), ai_cats AS (SELECT category, COUNT(*) AS count FROM recent WHERE source IN ('ai','ai_low_confidence','ai_failed') GROUP BY category ORDER BY count DESC LIMIT 5), ai_confidence AS (SELECT COALESCE(ROUND(AVG(confidence)::numeric,2),0) AS avg_conf, COALESCE(ROUND(MIN(confidence)::numeric,2),0) AS min_conf, COUNT(*) AS ai_count FROM recent WHERE source IN ('ai','ai_low_confidence')), daily_avg AS (SELECT COALESCE(ROUND(COUNT(*)::numeric / GREATEST(EXTRACT(EPOCH FROM (MAX(updated_at) - MIN(updated_at))) / 86400.0, 1), 1),0) AS avg_per_day, COALESCE(GREATEST(CEIL(EXTRACT(EPOCH FROM (MAX(updated_at) - MIN(updated_at))) / 86400.0), 1)::int,1) AS actual_days FROM recent), review_senders AS (SELECT sender, COUNT(*) AS count FROM recent WHERE category='needs-review' AND sender <> '' GROUP BY sender HAVING COUNT(*) >= 2 ORDER BY count DESC LIMIT 3) SELECT json_build_object('days',\"+days+\",'total',(SELECT total FROM totals),'avg_per_day',(SELECT avg_per_day FROM daily_avg),'actual_days',(SELECT actual_days FROM daily_avg),'categories',(SELECT COALESCE(json_agg(cats),'[]'::json) FROM cats),'sources',(SELECT COALESCE(json_agg(sources),'[]'::json) FROM sources),'top_senders',(SELECT COALESCE(json_agg(senders),'[]'::json) FROM senders),'rule_hits',(SELECT row_to_json(rule_hits) FROM rule_hits),'ai_categories',(SELECT COALESCE(json_agg(ai_cats),'[]'::json) FROM ai_cats),'ai_confidence',(SELECT row_to_json(ai_confidence) FROM ai_confidence),'review_senders',(SELECT COALESCE(json_agg(review_senders),'[]'::json) FROM review_senders)) AS stats;\";})()}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "42654a75-7d9e-44a8-9637-a440c96e66d6",
      "name": "Query Stats",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        420
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const original=$('Parse And Authorize Command').first().json;const first=$input.first()?.json||{};if(first.error)return[{json:{chatId:original.chatId,text:clampTelegram(`\u26a0\ufe0f Database unavailable.\\n\\nCould not load stats safely. Try again later.\\n${String(first.error.message||'').slice(0,200)}`)}}];const data=first.stats||{};function list(arr,key='category',emoji=false){return Array.isArray(arr)&&arr.length?arr.map(x=>`${emoji?emojiCat(x[key]):(x[key]||x.source||x.sender)}: ${x.count}`).join(' | '):'none';}const total=Number(data.total||0),rh=data.rule_hits||{},ruleCount=Number(rh.rule_count||0),pct=total?Math.round(ruleCount/total*100):0,ai=data.ai_confidence||{};let text=`\ud83d\udcca <b>Shiny Gmail Stats</b> \u2014 last ${data.days||original.statsDays||7} day(s)\\n\\nTotal processed: ${total}\\nDaily average: ${data.avg_per_day||0} emails/day (over ${data.actual_days||1} actual day(s))\\nCategories: ${list(data.categories||[],'category',true)}\\nSources: ${list(data.sources||[],'source',false)}\\nAI bypass rate: ${pct}% (${ruleCount}/${total} bypassed AI)\\n\ud83e\udd16 AI confidence: avg ${ai.avg_conf??0} \u00b7 min ${ai.min_conf??0} \u00b7 across ${ai.ai_count||0} classification(s)\\nTop AI-classified: ${list(data.ai_categories||[],'category',true)}\\nTop senders: ${list(data.top_senders||[],'sender',false)}`;const rs=data.review_senders||[];if(Array.isArray(rs)&&rs.length)text+=`\\n\\n\ud83d\udca1 <b>Rule candidates</b> (frequent needs-review)\\n`+rs.map(s=>`\u2022 ${h(s.sender)} (${s.count}\u00d7 \u2192 /whitelist, /receipt, or /blacklist)`).join('\\n');return[{json:{chatId:original.chatId,text:clampTelegram(text)}}];"
      },
      "id": "03ae3625-29e7-4a96-9057-d623d06d5083",
      "name": "Format Stats Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        420
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{(()=>{const account=String($json.account).replace(/'/g,\"''\"),limit=Math.min(Math.max(Number($json.recentCount||5),1),20),cat=$json.recentCategory?String($json.recentCategory).replace(/'/g,\"''\"):null,senderPattern=$json.recentSenderPattern?String($json.recentSenderPattern).replace(/'/g,\"''\"):null,isDomain=Boolean($json.recentIsDomain),failed=$json.recentFilter==='status',warned=$json.recentFilter==='warnings';let where=\" WHERE account='\"+account+\"'\";if(warned)where+=\" AND status LIKE 'completed%' AND error_message IS NOT NULL\";else where+=\" AND status='\"+(failed?'failed':'completed')+\"'\";if(cat)where+=\" AND category='\"+cat+\"'\";if(senderPattern)where+=isDomain?\" AND (LOWER(split_part(sender,'@',2))='\"+senderPattern+\"' OR LOWER(split_part(sender,'@',2)) LIKE '%.\"+senderPattern+\"')\":\" AND sender='\"+senderPattern+\"'\";return \"SELECT gmail_message_id, sender, subject, category, source, confidence, reasoning, status, error_message, updated_at FROM gmail_message_ledger\"+where+\" ORDER BY updated_at DESC LIMIT \"+limit+\";\";})()}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "88541d85-c0e3-473e-994b-ff2dc78383b8",
      "name": "Query Recent",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        580
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const original=$('Parse And Authorize Command').first().json;const first=$input.first()?.json||{};if(first.error)return[{json:{chatId:original.chatId,text:clampTelegram(`\u26a0\ufe0f Database unavailable.\\n\\nCould not load recent classifications safely. Try again later.\\n${String(first.error.message||'').slice(0,200)}`)}}];const rows=$input.all().map(i=>i.json).filter(r=>r.gmail_message_id);const failed=original.recentFilter==='status';const warned=original.recentFilter==='warnings';const filter=warned?' \u2014 \u26a0\ufe0f warnings':(failed?' \u2014 \ud83d\udea8 failed':(original.recentCategory?` \u2014 ${emojiCat(original.recentCategory)}`:(original.recentSenderPattern?` \u2014 ${h(original.recentSenderPattern)}`:'')));function gmailLink(id){return id&&!String(id).startsWith('input_failure_')?`https://mail.google.com/mail/u/0/#all/${encodeURIComponent(id)}`:null;}let text=`\ud83d\udcec <b>Recent Classifications${filter}</b>\\n`;if(!rows.length)text+='\\nNo matching emails found yet.';else text+='\\n'+rows.map((r,i)=>{const link=gmailLink(r.gmail_message_id);const subj=h(r.subject||'(no subject)');const subject=link?`<a href=\"${link}\">${subj}</a>`:subj;if(warned)return`${i+1}. <b>\u26a0\ufe0f warning</b> \u2014 ${h(r.sender||'unknown')} \u00b7 ${relativeTime(r.updated_at)}\\n${subject}\\n${h(r.source||'completed_with_warning')} \u2014 ${h(r.error_message||'Completed with warning.')}`;if(failed)return`${i+1}. <b>\ud83d\udea8 failed</b> \u2014 ${h(r.sender||'unknown')} \u00b7 ${relativeTime(r.updated_at)}\\n${subject}\\n${h(r.source||'failed')} \u2014 ${h(r.error_message||r.reasoning||'No error message.')}\\nNext: rerun tomorrow, or inspect Gmail label state if this repeats.`;return`${i+1}. <b>${emojiCat(r.category)}</b> \u2014 ${h(r.sender||'unknown')} \u00b7 ${relativeTime(r.updated_at)}\\n${subject}\\n${h(r.source||'unknown')} \u00b7 confidence ${h(r.confidence??0)}\\n${h(r.reasoning||'')}`}).join('\\n\\n');return[{json:{chatId:original.chatId,text:clampTelegram(text)}}];"
      },
      "id": "e211964c-3db9-4faa-aa10-0b1a0d3bb71c",
      "name": "Format Recent Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        580
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{(()=>{const account=String($json.account).replace(/'/g,\"''\"),input=String($json.explainInput).replace(/'/g,\"''\"),domain=String($json.explainDomain).replace(/'/g,\"''\");return \"WITH rule_matches AS (SELECT sender_pattern, match_type, rule_type, priority, created_at, CASE WHEN sender_pattern='\"+input+\"' THEN 'exact sender match' WHEN match_type='domain' AND '\"+domain+\"'=sender_pattern THEN 'root domain match' WHEN match_type='domain' AND '\"+domain+\"' LIKE '%.' || sender_pattern THEN 'subdomain match' ELSE 'matched' END AS match_reason, CASE WHEN match_type='exact' AND sender_pattern='\"+input+\"' THEN 0 WHEN match_type='domain' AND '\"+domain+\"'=sender_pattern THEN 1 WHEN match_type='domain' AND '\"+domain+\"' LIKE '%.' || sender_pattern THEN 2 ELSE 9 END AS match_strength, NULL::timestamptz AS last_seen, 0 AS hist_count, 0 AS row_type FROM gmail_sender_rules WHERE account='\"+account+\"' AND enabled=true AND (sender_pattern='\"+input+\"' OR (match_type='domain' AND ('\"+domain+\"'=sender_pattern OR '\"+domain+\"' LIKE '%.' || sender_pattern))) ORDER BY priority ASC, match_strength ASC, created_at DESC LIMIT 5), history AS (SELECT NULL::text AS sender_pattern, NULL::text AS match_type, category AS rule_type, NULL::integer AS priority, MAX(updated_at) AS created_at, ('history: ' || source || ' \u00d7 ' || COUNT(*) || ', conf ' || ROUND(AVG(confidence)::numeric, 2)::text) AS match_reason, 99 AS match_strength, MAX(updated_at) AS last_seen, COUNT(*)::int AS hist_count, 1 AS row_type FROM gmail_message_ledger WHERE account='\"+account+\"' AND status='completed' AND (sender='\"+input+\"' OR LOWER(split_part(sender,'@',2))='\"+domain+\"' OR LOWER(split_part(sender,'@',2)) LIKE '%.\"+domain+\"') GROUP BY category, source ORDER BY COUNT(*) DESC LIMIT 3) SELECT * FROM rule_matches UNION ALL SELECT * FROM history ORDER BY row_type ASC, priority ASC NULLS LAST, match_strength ASC, hist_count DESC, created_at DESC NULLS LAST;\";})()}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "bf53fdcb-44ee-473b-8a0c-45056bcc40dc",
      "name": "Query Explain",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        740
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const original=$('Parse And Authorize Command').first().json;const first=$input.first()?.json||{};if(first.error)return[{json:{chatId:original.chatId,text:clampTelegram(`\u26a0\ufe0f Database unavailable.\\n\\nCould not preview rule matching safely. Try again later.\\n${String(first.error.message||'').slice(0,200)}`)}}];const rows=$input.all().map(i=>i.json);const ruleRows=rows.filter(r=>r.row_type===0&&r.sender_pattern);const historyRows=rows.filter(r=>r.row_type===1);let decision='Gemini will classify using metadata';let text=`\ud83d\udd0e <b>Classification Preview</b>\\n\\nInput: ${h(original.explainInput)}\\n`;if(ruleRows.length){const w=ruleRows[0];decision=`${emojiCat(mapRule(w.rule_type))} via ${w.match_type==='domain'?'domain rule':'exact sender rule'}`;}text+=`<b>Decision today:</b> ${h(decision)}\\n<i>Winner selected by: priority ASC; exact beats domain at same priority; newest rule wins remaining ties.</i>\\n`;if(!ruleRows.length)text+='\\nNo sender rule matches. Gemini will classify using metadata.';else{const w=ruleRows[0];text+=`\\n<b>Winner</b>\\n${h(w.sender_pattern)} (${h(w.match_type)})\\nMatch: ${h(w.match_reason||'matched')}\\nPriority: p${h(w.priority??10)}\\nRule type: ${h(w.rule_type)}\\nFuture category: ${emojiCat(mapRule(w.rule_type))}`;if(ruleRows.length>1){const runner=ruleRows[1];let reason='More recently created rule at same priority';if(Number(w.priority)!==Number(runner.priority))reason=`Lower priority value (p${w.priority} before p${runner.priority})`;else if(w.match_type!==runner.match_type)reason='Exact match beats domain match at same priority';else if(Number(w.match_strength)!==Number(runner.match_strength))reason='More specific domain match wins before broader subdomain/root matches';text+=`\\n\\n<b>Also matched</b>\\n`+ruleRows.slice(1).map((r,i)=>`${i+2}. ${h(r.sender_pattern)} (${h(r.match_type)}) \u00b7 ${h(r.match_reason||'matched')} \u00b7 p${h(r.priority??10)} \u2192 ${emojiCat(mapRule(r.rule_type))}`).join('\\n')+`\\n\u26a1 Resolution: ${h(reason)}`;}}\nif(historyRows.length){text+=`\\n\\n<b>Classification history</b>\\n`+historyRows.map(r=>`${emojiCat(r.rule_type)} \u00b7 ${h(r.match_reason)} \u00b7 last ${relativeTime(r.last_seen)}`).join('\\n');if(ruleRows.length){const future=mapRule(ruleRows[0].rule_type);const dominant=historyRows[0].rule_type;if(dominant&&future&&dominant!==future)text+=`\\n\\n\u26a0\ufe0f <b>Drift note</b>\\nCurrent winning rule classifies future mail as ${emojiCat(future)}, but recent history is mostly ${emojiCat(dominant)}.`;}}else text+='\\n\\nNo classification history found for this sender/domain.';return[{json:{chatId:original.chatId,text:clampTelegram(text)}}];"
      },
      "id": "4062fd69-0c08-4b74-a250-a3bd738c1267",
      "name": "Format Explain Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        740
      ]
    },
    {
      "parameters": {
        "operation": "sendMessage",
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.text }}",
        "additionalFields": {
          "parse_mode": "HTML",
          "disable_web_page_preview": true
        }
      },
      "id": "a63972b3-09d3-42ee-8b6b-284347167e05",
      "name": "Send Telegram Reply",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1220,
        260
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000
    }
  ],
  "connections": {
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Parse And Authorize Command",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse And Authorize Command": {
      "main": [
        [
          {
            "node": "Route Command",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Command": {
      "main": [
        [
          {
            "node": "Format Simple Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Simple Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Simple Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Upsert Rule",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Disable Rule",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Query Rules",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Query Stats",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Query Recent",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Query Explain",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Simple Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Rule": {
      "main": [
        [
          {
            "node": "Format Rule Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Disable Rule": {
      "main": [
        [
          {
            "node": "Format Rule Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Rule Result": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Rules": {
      "main": [
        [
          {
            "node": "Format Rules Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Rules Reply": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Stats": {
      "main": [
        [
          {
            "node": "Format Stats Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Stats Reply": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Recent": {
      "main": [
        [
          {
            "node": "Format Recent Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Recent Reply": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Explain": {
      "main": [
        [
          {
            "node": "Format Explain Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Explain Reply": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Simple Reply": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "saveExecutionProgress": false,
    "executionTimeout": 300
  },
  "staticData": null,
  "tags": [],
  "triggerCount": 1,
  "versionId": "v4.0-lean-generated"
}
Pro

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

About this workflow

Shiny Gmail Lightweight Telegram Rules Manager V4.0 Lean. Uses telegramTrigger, postgres, telegram. Event-driven trigger; 16 nodes.

Source: https://github.com/chanrylejay/shiny-gmail-automation/blob/main/workflows/workflow_c_rules_manager.json — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

For makers, founders, and productivity nerds who want to listen to their inbox instead of reading it. No servers, no hosting — all done with n8n, a Telegram bot, and AI/ML API (LLM + TTS).

HTTP Request, N8N Nodes Aimlapi, Data Table +3
Email & Gmail

This workflow runs every Monday at 8 AM and automatically monitors your Jira project, measures progress against the active sprint, and delivers a structured report to stakeholders — with zero manual e

Postgres, Jira, N8N Nodes Htmlcsstopdf +1
Email & Gmail

This template is ideal for educators, HR professionals, and anyone looking to automate testing and collect responses through Telegram, while storing results in a Postgres database.

Telegram, Postgres, Telegram Trigger
Email & Gmail

This workflow accepts a suspected scam URL via an n8n form, enriches it with RDAP, certificate transparency, DNS/IP hosting data, urlscan.io results, and HTML fingerprints, then correlates findings ag

Form Trigger, HTTP Request, Data Table +2
Email & Gmail

Signup Intake → CRM triage. Uses formTrigger, googleSheets, telegram, telegramTrigger. Webhook trigger; 28 nodes.

Form Trigger, Google Sheets, Telegram +3