This workflow follows the HTTP Request → 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 →
{
"name": "Telegram Query Bot",
"nodes": [
{
"id": "tg-trigger-id",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1,
"position": [
100,
300
],
"parameters": {
"updates": [
"message",
"callback_query"
],
"additionalFields": {}
}
},
{
"id": "parse-command-id",
"name": "Parse Command",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
280,
300
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const msg = $json.message;\nconst callback = $json.callback_query;\n\nlet command, args, chatId, msgId, callbackQueryId;\n\nif (callback) {\n const data = callback.data || '';\n const colonIdx = data.indexOf(':');\n command = colonIdx >= 0 ? data.slice(0, colonIdx) : data;\n args = colonIdx >= 0 ? data.slice(colonIdx + 1) : '';\n chatId = callback.message?.chat?.id?.toString() || '';\n msgId = callback.message?.message_id;\n callbackQueryId = callback.id;\n} else {\n const text = (msg?.text || '').trim();\n chatId = msg?.chat?.id?.toString() || '';\n msgId = msg?.message_id;\n callbackQueryId = null;\n\n if (text.startsWith('/search')) {\n command = 'search';\n args = text.replace(/^\\/search\\s*/i, '').trim();\n } else if (text.startsWith('/latest')) {\n command = 'latest';\n const match = text.match(/(\\d+)/);\n args = match ? match[1] : '5';\n } else if (text.startsWith('/stats')) {\n command = 'stats';\n args = '';\n } else if (text.startsWith('/help')) {\n command = 'help';\n args = '';\n } else {\n command = 'unknown';\n args = '';\n }\n}\n\nreturn { json: { command, args, chatId, msgId, callbackQueryId } };\n"
}
},
{
"id": "route-command-id",
"name": "Route Command",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
460,
300
],
"parameters": {
"mode": "rules",
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.command }}",
"rightValue": "search",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "0"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.command }}",
"rightValue": "latest",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "1"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.command }}",
"rightValue": "help",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "2"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.command }}",
"rightValue": "stats",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "3"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.command }}",
"rightValue": "expand",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "4"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.command }}",
"rightValue": "skip",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "5"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.command }}",
"rightValue": "save",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "6"
}
]
},
"fallbackOutput": "extra"
}
},
{
"id": "call-search-id",
"name": "Call Search Webhook",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
660,
160
],
"parameters": {
"method": "POST",
"url": "=https://{{ $vars.N8N_DOMAIN }}/webhook/search",
"sendBody": true,
"contentType": "json",
"body": {
"query": "={{ $json.args }}",
"top_k": 5
},
"options": {
"timeout": 60000
}
}
},
{
"id": "format-search-reply-id",
"name": "Format Search Reply",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
860,
160
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const chatId = $('Parse Command').first().json.chatId;\nconst data = $json;\n\nconst sourcesText = (data.sources || []).slice(0, 5).map((s, i) =>\n `${i + 1}. [${s.title}](${s.url})\\n _${s.relevance_to_query || ''}_`\n).join('\\n\\n');\n\nconst confidence = data.confidence ? `\\n\\n\ud83d\udcca *Confidence:* ${data.confidence}` : '';\n\nconst text = [\n `\ud83d\udd0d *Query:* ${data.query}`,\n '',\n data.answer || '_No answer generated._',\n confidence,\n sourcesText ? `\\n\\n\ud83d\udcc4 *Sources:*\\n${sourcesText}` : '\\n\\n_No relevant papers found._'\n].join('\\n');\n\nreturn { json: { chatId, text } };\n"
}
},
{
"id": "fetch-latest-id",
"name": "Fetch Latest Papers",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
300
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const limit = Math.min(parseInt($json.args) || 5, 10);\nconst chatId = $json.chatId;\n\n// Scroll the arxiv_papers collection ordered by score (proxy for recency)\n// Qdrant scroll returns points in insertion order by default\nconst response = await fetch('http://qdrant:6333/collections/arxiv_papers/points/scroll', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'api-key': $vars.QDRANT_API_KEY || ''\n },\n body: JSON.stringify({\n limit,\n with_payload: true,\n order_by: { key: 'metadata.ingested_at', direction: 'desc' }\n })\n});\n\nif (!response.ok) {\n const err = await response.text();\n throw new Error(`Qdrant scroll failed: ${err}`);\n}\n\nconst data = await response.json();\nconst points = (data.result?.points || []).map(p => ({\n title: p.payload?.metadata?.title || p.payload?.title || 'Untitled',\n url: p.payload?.metadata?.url || p.payload?.url || '',\n relevance: p.payload?.metadata?.relevance || p.payload?.relevance || '',\n tags: p.payload?.metadata?.tags || p.payload?.tags || []\n}));\n\nreturn { json: { chatId, papers: points } };\n"
}
},
{
"id": "format-latest-reply-id",
"name": "Format Latest Reply",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
860,
300
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const { chatId, papers } = $json;\n\nif (!papers || papers.length === 0) {\n return { json: { chatId, text: '\ud83d\udced No papers found in the database yet.' } };\n}\n\nconst lines = papers.map((p, i) => {\n const tags = Array.isArray(p.tags) ? p.tags.join(', ') : p.tags || '';\n const rel = p.relevance ? ` \u00b7 ${p.relevance}` : '';\n return `${i + 1}. [${p.title}](${p.url})\\n \ud83c\udff7\ufe0f ${tags}${rel}`;\n});\n\nconst text = `\ud83d\udcda *Latest papers:*\\n\\n${lines.join('\\n\\n')}`;\nreturn { json: { chatId, text } };\n"
}
},
{
"id": "help-reply-id",
"name": "Help Reply",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
440
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const chatId = $json.chatId;\nconst text = [\n '\ud83e\udd16 *ResearchFlow AI \u2014 Commands*',\n '',\n '`/search <question>` \u2014 Ask a question and get a cited answer from stored papers',\n '_Example: /search How do mixture of experts models scale?_',\n '',\n '`/latest [n]` \u2014 Show the n most recently ingested papers (default 5, max 10)',\n '_Example: /latest 3_',\n '',\n '`/stats` \u2014 Show a 7-day run summary (papers indexed, relevance breakdown, fallback rate)',\n '',\n '`/help` \u2014 Show this message'\n].join('\\n');\nreturn { json: { chatId, text } };\n"
}
},
{
"id": "unknown-reply-id",
"name": "Unknown Command Reply",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
580
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const chatId = $json.chatId;\nconst text = '\u2753 Unknown command. Send /help to see available commands.';\nreturn { json: { chatId, text } };\n"
}
},
{
"id": "send-reply-id",
"name": "Send Reply",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1,
"position": [
1060,
300
],
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.text }}",
"additionalFields": {
"parse_mode": "Markdown",
"disable_web_page_preview": true
}
}
},
{
"id": "answer-callback-id",
"name": "Answer Callback Query",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
860,
720
],
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $vars.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"contentType": "json",
"body": {
"callback_query_id": "={{ $json.callbackQueryId }}",
"text": "={{ $json.command === 'expand' ? '\ud83d\udd0d Fetching deep summary...' : $json.command === 'skip' ? '\u23ed Marked as skipped' : '\ud83d\udd16 Saved to your library' }}"
},
"options": {}
}
},
{
"id": "handle-expand-id",
"name": "Handle Expand",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1260,
580
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Fetch the paper from Qdrant by URL, then call OpenAI\n// for a deeper summary including methodology and limitations.\nconst paperUrl = $json.args;\nconst chatId = $json.chatId;\n\n// 1. Search Qdrant for the paper by URL metadata field\nconst searchResp = await fetch('http://qdrant:6333/collections/arxiv_papers/points/scroll', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'api-key': $vars.QDRANT_API_KEY || ''\n },\n body: JSON.stringify({\n filter: {\n must: [{ key: 'metadata.url', match: { value: paperUrl } }]\n },\n limit: 1,\n with_payload: true\n })\n});\n\nconst searchData = await searchResp.json();\nconst point = searchData.result?.points?.[0];\n\nif (!point) {\n return { json: { chatId, text: '\u274c Paper not found in the database.' } };\n}\n\nconst meta = point.payload?.metadata || point.payload || {};\nconst title = meta.title || 'Unknown title';\nconst stored = point.payload?.document || meta.abstract || '';\n\n// 2. Call GPT-4o for a deeper analysis\nconst gptResp = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${$vars.OPENAI_API_KEY}`\n },\n body: JSON.stringify({\n model: 'gpt-4o',\n max_tokens: 800,\n messages: [\n {\n role: 'system',\n content: 'You are a research assistant. Given a paper summary, produce a deeper analysis covering: (1) Methodology in detail \u2014 architecture, training procedure, key equations if any; (2) Limitations and open questions; (3) Practical applications; (4) Related work it builds on. Be technical but concise. Use plain text, no markdown headers.'\n },\n {\n role: 'user',\n content: `Title: ${title}\\n\\nSummary:\\n${stored}`\n }\n ]\n })\n});\n\nconst gptData = await gptResp.json();\nconst analysis = gptData.choices?.[0]?.message?.content || 'Could not generate analysis.';\n\nconst text = [\n `\ud83d\udd0d *Deep analysis: ${title}*`,\n '',\n analysis,\n '',\n `\ud83d\udd17 ${paperUrl}`\n].join('\\n');\n\nreturn { json: { chatId, text } };\n"
}
},
{
"id": "handle-skip-id",
"name": "Handle Skip",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1260,
720
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Find the Qdrant point for this paper and set user_rating: -1\nconst paperUrl = $json.args;\nconst chatId = $json.chatId;\n\n// Scroll to find the point ID\nconst scrollResp = await fetch('http://qdrant:6333/collections/arxiv_papers/points/scroll', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'api-key': $vars.QDRANT_API_KEY || ''\n },\n body: JSON.stringify({\n filter: { must: [{ key: 'metadata.url', match: { value: paperUrl } }] },\n limit: 1,\n with_payload: false\n })\n});\n\nconst scrollData = await scrollResp.json();\nconst pointId = scrollData.result?.points?.[0]?.id;\n\nif (!pointId) {\n return { json: { chatId, text: '\u274c Paper not found \u2014 cannot update rating.' } };\n}\n\n// PATCH the payload\nconst patchResp = await fetch(`http://qdrant:6333/collections/arxiv_papers/points/payload`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'api-key': $vars.QDRANT_API_KEY || ''\n },\n body: JSON.stringify({\n payload: { 'metadata.user_rating': -1, 'metadata.user_rated_at': new Date().toISOString() },\n points: [pointId]\n })\n});\n\nif (!patchResp.ok) {\n const err = await patchResp.text();\n throw new Error(`Qdrant PATCH failed: ${err}`);\n}\n\nreturn { json: { chatId, text: '\u23ed Got it \u2014 this paper will be deprioritised in future searches.' } };\n"
}
},
{
"id": "handle-save-id",
"name": "Handle Save",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1260,
860
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Find the Qdrant point for this paper and set user_rating: 1\nconst paperUrl = $json.args;\nconst chatId = $json.chatId;\n\nconst scrollResp = await fetch('http://qdrant:6333/collections/arxiv_papers/points/scroll', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'api-key': $vars.QDRANT_API_KEY || ''\n },\n body: JSON.stringify({\n filter: { must: [{ key: 'metadata.url', match: { value: paperUrl } }] },\n limit: 1,\n with_payload: false\n })\n});\n\nconst scrollData = await scrollResp.json();\nconst pointId = scrollData.result?.points?.[0]?.id;\n\nif (!pointId) {\n return { json: { chatId, text: '\u274c Paper not found \u2014 cannot update rating.' } };\n}\n\nconst patchResp = await fetch(`http://qdrant:6333/collections/arxiv_papers/points/payload`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'api-key': $vars.QDRANT_API_KEY || ''\n },\n body: JSON.stringify({\n payload: { 'metadata.user_rating': 1, 'metadata.user_rated_at': new Date().toISOString() },\n points: [pointId]\n })\n});\n\nif (!patchResp.ok) {\n const err = await patchResp.text();\n throw new Error(`Qdrant PATCH failed: ${err}`);\n}\n\nreturn { json: { chatId, text: '\ud83d\udd16 Saved! This paper is now in your library. It will be used as a quality signal for future prompt tuning.' } };\n"
}
},
{
"id": "route-feedback-id",
"name": "Route Feedback",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
1060,
720
],
"parameters": {
"mode": "rules",
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.command }}",
"rightValue": "expand",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "0"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.command }}",
"rightValue": "skip",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "1"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.command }}",
"rightValue": "save",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "2"
}
]
},
"fallbackOutput": "none"
}
},
{
"id": "fetch-stats-id",
"name": "Fetch Stats",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
720
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const chatId = $json.chatId;\n\n// Scroll the run_logs collection \u2014 small collection, no pagination needed\nconst resp = await fetch('http://qdrant:6333/collections/run_logs/points/scroll', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'api-key': $vars.QDRANT_API_KEY || ''\n },\n body: JSON.stringify({ limit: 100, with_payload: true })\n});\n\nif (!resp.ok) {\n const err = await resp.text();\n return { json: { chatId, text: `\u274c Could not fetch stats: ${err}` } };\n}\n\nconst data = await resp.json();\nconst points = data.result?.points || [];\n\nif (points.length === 0) {\n return { json: { chatId, text: '\ud83d\udcca No runs logged yet. Stats will appear after the first pipeline run.' } };\n}\n\n// Filter to last 7 days\nconst since7d = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\nconst recentRuns = points\n .map(p => p.payload)\n .filter(p => p?.timestamp && new Date(p.timestamp) >= since7d)\n .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));\n\nconst allRuns = points.map(p => p.payload).filter(Boolean);\n\nif (recentRuns.length === 0) {\n return { json: { chatId, text: '\ud83d\udcca No runs in the last 7 days.' } };\n}\n\n// Aggregate\nconst sum = (arr, key) => arr.reduce((a, r) => a + (r[key] || 0), 0);\nconst totalFetched = sum(recentRuns, 'papers_fetched');\nconst totalNew = sum(recentRuns, 'processed');\nconst totalSeen = sum(recentRuns, 'already_seen');\nconst totalHigh = sum(recentRuns, 'high');\nconst totalMedium = sum(recentRuns, 'medium');\nconst totalLow = sum(recentRuns, 'low');\nconst totalFallbacks = sum(recentRuns, 'pdf_fallbacks');\nconst avgDuration = recentRuns.reduce((a, r) => a + (r.duration_ms || 0), 0) / recentRuns.length;\nconst fallbackRate = totalNew > 0 ? ((totalFallbacks / totalNew) * 100).toFixed(1) : '0.0';\n\n// Last 7 runs table\nconst runRows = recentRuns.slice(0, 7).map(r => {\n const date = new Date(r.timestamp).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' });\n return ` ${date}: ${r.processed ?? 0} new (H:${r.high ?? 0} M:${r.medium ?? 0} L:${r.low ?? 0})`;\n}).join('\\n');\n\nconst text = [\n `\ud83d\udcca *ResearchFlow \u2014 7-day stats* (${recentRuns.length} runs)`,\n '',\n `\ud83d\udce5 Fetched: ${totalFetched} \u00b7 Already seen: ${totalSeen} \u00b7 New: ${totalNew}`,\n `\u2705 High: ${totalHigh} \u00b7 \u26a1 Medium: ${totalMedium} \u00b7 \ud83d\udd07 Low: ${totalLow}`,\n `\u26a0\ufe0f PDF fallback rate: ${fallbackRate}%`,\n `\u23f1 Avg run time: ${(avgDuration / 1000).toFixed(1)}s`,\n '',\n '*Recent runs:*',\n runRows,\n '',\n `\ud83d\udce6 Total runs logged: ${allRuns.length}`\n].join('\\n');\n\nreturn { json: { chatId, text } };\n"
}
}
],
"connections": {
"Telegram Trigger": {
"main": [
[
{
"node": "Parse Command",
"type": "main",
"index": 0
}
]
]
},
"Parse Command": {
"main": [
[
{
"node": "Route Command",
"type": "main",
"index": 0
}
]
]
},
"Route Command": {
"main": [
[
{
"node": "Call Search Webhook",
"type": "main",
"index": 0
}
],
[
{
"node": "Fetch Latest Papers",
"type": "main",
"index": 0
}
],
[
{
"node": "Help Reply",
"type": "main",
"index": 0
}
],
[
{
"node": "Fetch Stats",
"type": "main",
"index": 0
}
],
[],
[
{
"node": "Answer Callback Query",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Callback Query",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Callback Query",
"type": "main",
"index": 0
}
]
]
},
"Call Search Webhook": {
"main": [
[
{
"node": "Format Search Reply",
"type": "main",
"index": 0
}
]
]
},
"Format Search Reply": {
"main": [
[
{
"node": "Send Reply",
"type": "main",
"index": 0
}
]
]
},
"Fetch Latest Papers": {
"main": [
[
{
"node": "Format Latest Reply",
"type": "main",
"index": 0
}
]
]
},
"Format Latest Reply": {
"main": [
[
{
"node": "Send Reply",
"type": "main",
"index": 0
}
]
]
},
"Help Reply": {
"main": [
[
{
"node": "Send Reply",
"type": "main",
"index": 0
}
]
]
},
"Unknown Command Reply": {
"main": [
[
{
"node": "Send Reply",
"type": "main",
"index": 0
}
]
]
},
"Answer Callback Query": {
"main": [
[
{
"node": "Route Feedback",
"type": "main",
"index": 0
}
]
]
},
"Route Feedback": {
"main": [
[
{
"node": "Handle Expand",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Skip",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Save",
"type": "main",
"index": 0
}
]
]
},
"Handle Expand": {
"main": [
[
{
"node": "Send Reply",
"type": "main",
"index": 0
}
]
]
},
"Handle Skip": {
"main": [
[
{
"node": "Send Reply",
"type": "main",
"index": 0
}
]
]
},
"Handle Save": {
"main": [
[
{
"node": "Send Reply",
"type": "main",
"index": 0
}
]
]
},
"Fetch Stats": {
"main": [
[
{
"node": "Send Reply",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"callerPolicy": "workflowsFromSameOwner",
"errorWorkflow": ""
},
"tags": [
{
"name": "search"
},
{
"name": "telegram"
}
]
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Telegram Query Bot. Uses telegramTrigger, httpRequest, telegram. Event-driven trigger; 16 nodes.
Source: https://github.com/keila-moral/researchflow-ai/blob/main/workflows/Telegram_Query_Bot.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
N8N Complete Final. Uses telegramTrigger, dataTable, telegram, mqtt. Event-driven trigger; 58 nodes.
Pede Ai. Uses httpRequest, telegram, postgres, telegramTrigger. Event-driven trigger; 57 nodes.
TextMain. Uses telegramTrigger, stopAndError, telegram, httpRequest. Event-driven trigger; 56 nodes.
Pede Ai. Uses httpRequest, telegram, postgres, telegramTrigger. Event-driven trigger; 53 nodes.
📄 Documentation: Notion Guide