This workflow follows the Google Sheets → HTTP Request 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": "02b \u2014 Article callback",
"nodes": [
{
"parameters": {
"updates": [
"message",
"callback_query"
],
"additionalFields": {}
},
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.2,
"position": [
0,
0
],
"id": "11111111-2222-2222-2222-111111111111",
"name": "Telegram Trigger",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Parse a Telegram update into a normalized command object.\n//\n// Supported text commands (in marketing chat only):\n// /approve <exec_id> \u2014 fire publish-article GH Action for a draft\n// /reject <exec_id> \u2014 drop the draft, no GH dispatch\n// /redispatch <exec_id> \u2014 re-fire GH dispatch even if state isn't awaiting_review\n// /list or /queue \u2014 show all drafts awaiting review with inline buttons\n// /topic <free text> \u2014 append a custom topic to content-plan, status=approved\n// /edit <exec_id> <field>=<value> \u2014 single-field edit (title|category|excerpt|read)\n// @puffypet_bot <text> \u2014 open a chat thread with Claude (mention anywhere in the message)\n//\n// Replies to any bot message (when text doesn't match a command above) also\n// route to the chat handler \u2014 they continue the existing thread.\n//\n// Inline button callbacks (from /list output) carry callback_data of shape:\n// act:<approve|reject|edit|list>:<exec_id>\n//\n// Anything else is silently dropped.\nconst MARKETING_CHAT_ID = -5184755657;\n\nconst payload = $json;\n\n// Branch 1 \u2014 callback_query from inline button click\nif (payload.callback_query) {\n const cq = payload.callback_query;\n const chatId = cq.message?.chat?.id;\n if (chatId !== MARKETING_CHAT_ID) return [];\n\n const data = (cq.data || '').trim();\n // Format: act:<command>:<exec_id?>\n const m = data.match(/^act:([a-z]+)(?::(\\S+))?$/);\n if (!m) return [];\n\n const command = m[1].toLowerCase();\n const exec_id = m[2] || null;\n\n return [{\n json: {\n command,\n exec_id,\n source: 'callback_query',\n callback_query_id: cq.id,\n reply_to_message_id: cq.message?.message_id || null,\n chat_id: chatId,\n },\n }];\n}\n\n// Branch 2 \u2014 text message\nconst msg = payload.message || payload;\nconst chatId = msg.chat?.id;\nconst text = (msg.text || '').trim();\n\nif (chatId !== MARKETING_CHAT_ID) return [];\nif (!text) return [];\n\n// /list and /queue (no args)\nif (/^\\/(list|queue)(?:@\\w+)?\\s*$/i.test(text)) {\n return [{\n json: {\n command: 'list',\n exec_id: null,\n source: 'text',\n reply_to_message_id: msg.message_id,\n chat_id: chatId,\n },\n }];\n}\n\n// /approve | /reject | /redispatch <exec_id>\nconst m1 = text.match(/^\\/(approve|reject|redispatch)(?:@\\w+)?\\s+(\\S+)\\s*$/i);\nif (m1) {\n return [{\n json: {\n command: m1[1].toLowerCase(),\n exec_id: m1[2],\n source: 'text',\n reply_to_message_id: msg.message_id,\n chat_id: chatId,\n },\n }];\n}\n\n// /topic <free text>\nconst m2 = text.match(/^\\/topic(?:@\\w+)?\\s+(.+)$/is);\nif (m2) {\n return [{\n json: {\n command: 'topic',\n topic_text: m2[1].trim(),\n exec_id: null,\n source: 'text',\n reply_to_message_id: msg.message_id,\n chat_id: chatId,\n },\n }];\n}\n\n// /edit <exec_id> <field>=<value>\n// Value can be unquoted (single token) or double-quoted (any text).\nconst m3 = text.match(/^\\/edit(?:@\\w+)?\\s+(\\S+)\\s+(\\w+)\\s*=\\s*(\"[^\"]*\"|.+?)\\s*$/i);\nif (m3) {\n let value = m3[3];\n if (value.startsWith('\"') && value.endsWith('\"')) {\n value = value.slice(1, -1);\n }\n return [{\n json: {\n command: 'edit',\n exec_id: m3[1],\n edit_field: m3[2].toLowerCase(),\n edit_value: value,\n source: 'text',\n reply_to_message_id: msg.message_id,\n chat_id: chatId,\n },\n }];\n}\n\n// @puffypet_bot <text> \u2014 open a chat thread with Claude.\n// The mention can appear anywhere in the message; the rest (with the mention\n// stripped) becomes the prompt. Bot username is hardcoded \u2014 change here if the\n// bot is renamed via @BotFather.\nif (/@puffypet_bot\\b/i.test(text)) {\n const stripped = text.replace(/@puffypet_bot\\b/gi, ' ').replace(/\\s+/g, ' ').trim();\n if (stripped) {\n return [{\n json: {\n command: 'chat',\n prompt_text: stripped,\n source_message_id: msg.message_id,\n parent_bot_message_id: '',\n username: msg.from?.username || msg.from?.first_name || '',\n source: 'text',\n reply_to_message_id: msg.message_id,\n chat_id: chatId,\n },\n }];\n }\n}\n\n// Reply to a bot message that didn't match a command \u2014 continue the chat thread.\n// Claude resolves whether this is a known thread (lookup by parent message_id)\n// or a new thread (e.g. reply to an article preview from 02a).\nif (msg.reply_to_message && msg.reply_to_message.from && msg.reply_to_message.from.is_bot) {\n return [{\n json: {\n command: 'chat',\n prompt_text: text,\n source_message_id: msg.message_id,\n parent_bot_message_id: String(msg.reply_to_message.message_id),\n username: msg.from?.username || msg.from?.first_name || '',\n source: 'text',\n reply_to_message_id: msg.message_id,\n chat_id: chatId,\n },\n }];\n}\n\n// Drop everything else\nreturn [];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
0
],
"id": "22222222-3333-3333-3333-222222222222",
"name": "Parse Command"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-class-draft-action",
"leftValue": "={{ $json.command }}",
"rightValue": "approve|reject|redispatch",
"operator": {
"type": "string",
"operation": "regex"
}
}
],
"combinator": "and"
},
"outputKey": "draft-action"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-class-list",
"leftValue": "={{ $json.command }}",
"rightValue": "list",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"outputKey": "list"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-class-topic",
"leftValue": "={{ $json.command }}",
"rightValue": "topic",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"outputKey": "topic"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-class-edit",
"leftValue": "={{ $json.command }}",
"rightValue": "edit",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"outputKey": "edit"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-class-chat",
"leftValue": "={{ $json.command }}",
"rightValue": "chat",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"outputKey": "chat"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
480,
0
],
"id": "30000000-3000-3000-3000-300000000000",
"name": "Route Class"
},
{
"parameters": {
"authentication": "serviceAccount",
"documentId": {
"__rl": true,
"value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "drafts",
"mode": "name"
},
"filtersUI": {
"values": [
{
"lookupColumn": "exec_id",
"lookupValue": "={{ $json.exec_id }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
720,
0
],
"id": "33333333-4444-4444-4444-333333333333",
"name": "Get Draft Row",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Merge command context with the draft row, validate, build branch input.\n// For /redispatch, skip the idempotency state check \u2014 the command exists\n// specifically to re-fire GH dispatch after a failure (state will be 'dispatched'\n// from the prior failed attempt; allowing retry is the whole point).\nconst cmd = $('Parse Command').first().json;\nconst draft = $input.first().json;\n\nif (!draft || !draft.exec_id) {\n throw new Error(`Draft not found for exec_id=${cmd.exec_id}`);\n}\n\nconst skipIdempotency = cmd.command === 'redispatch';\nif (!skipIdempotency && draft.state !== 'awaiting_review') {\n return [{\n json: {\n ...cmd,\n draft,\n already_processed: true,\n message: `Already ${draft.state}, skipping`,\n },\n }];\n}\n\nlet article;\ntry {\n article = JSON.parse(draft.article_json);\n} catch (e) {\n throw new Error(`Could not parse article_json: ${e.message}`);\n}\n\nreturn [{\n json: {\n ...cmd,\n draft,\n article,\n already_processed: false,\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
0
],
"id": "44444444-5555-5555-5555-444444444444",
"name": "Hydrate"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-already-processed",
"leftValue": "={{ $json.already_processed }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"outputKey": "Already processed"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-approve",
"leftValue": "={{ $json.command }}",
"rightValue": "approve",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"outputKey": "Approve"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-reject",
"leftValue": "={{ $json.command }}",
"rightValue": "reject",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"outputKey": "Reject"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-redispatch",
"leftValue": "={{ $json.command }}",
"rightValue": "redispatch",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"outputKey": "Redispatch"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1200,
0
],
"id": "55555555-6666-6666-6666-555555555555",
"name": "Route Action"
},
{
"parameters": {
"chatId": "={{ $json.chat_id }}",
"text": "=\u26a0\ufe0f \u042d\u0442\u0443 \u0441\u0442\u0430\u0442\u044c\u044e \u0443\u0436\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438 \u0440\u0430\u043d\u0435\u0435 ({{ $json.draft.state }}). \u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u0434\u0435\u043b\u0430\u044e.\n<i>exec_id: <code>{{ $json.exec_id }}</code></i>",
"replyToMessageId": "={{ $json.reply_to_message_id }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1440,
-300
],
"id": "66666666-7777-7777-7777-666666666666",
"name": "Telegram: Already Processed",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Build the GitHub workflow_dispatch payload that publish-article.yml expects.\n// publish-article.mjs validates excerpt to be 1\u2013200 chars, so we trim the lede\n// to its first sentence (then hard-clamp at 200).\nconst { article, exec_id } = $input.first().json;\n\nconst readMatch = (article.read || '').match(/(\\d+)/);\nconst read_minutes = readMatch ? readMatch[1] : '6';\n\nconst now = new Date();\nconst pad = (n) => String(n).padStart(2, '0');\nconst date = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}`;\n\nconst monthNames = ['January','February','March','April','May','June','July','August','September','October','November','December'];\nconst date_label = `${monthNames[now.getUTCMonth()]} ${now.getUTCFullYear()}`;\n\nconst lede = (article.body || []).find(b => b.type === 'lede');\nconst ledeText = lede ? lede.text : '';\nconst sentenceMatch = ledeText.match(/^[^.!?]+[.!?]/);\nconst firstSentence = sentenceMatch ? sentenceMatch[0].trim() : ledeText;\nconst excerpt = firstSentence.length > 200\n ? firstSentence.slice(0, 197).trim() + '...'\n : firstSentence;\n\nconst inputs = {\n id: article.id,\n category: article.category,\n title: article.title,\n excerpt,\n body_json: JSON.stringify(article.body),\n date,\n date_label,\n read_minutes,\n featured: 'false',\n};\n\nif (article.hero_svg) {\n inputs.hero_svg = article.hero_svg;\n inputs.hero_alt = article.hero_alt || '';\n inputs.hero_caption = article.hero_caption || '';\n}\n\nreturn [{\n json: {\n body: { ref: 'main', inputs },\n exec_id,\n article_id: article.id,\n article_title: article.title,\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1440,
0
],
"id": "77777777-8888-8888-8888-777777777777",
"name": "Build Dispatch Payload"
},
{
"parameters": {
"method": "POST",
"url": "https://api.github.com/repos/puffy-pet/puffy-site/actions/workflows/publish-article.yml/dispatches",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/vnd.github+json"
},
{
"name": "X-GitHub-Api-Version",
"value": "2022-11-28"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.body) }}",
"options": {
"redirect": {
"redirect": {}
},
"response": {
"response": {
"fullResponse": true
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1680,
0
],
"id": "88888888-9999-9999-9999-888888888888",
"name": "GitHub: Dispatch Action",
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"authentication": "serviceAccount",
"operation": "update",
"documentId": {
"__rl": true,
"value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "drafts",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"exec_id": "={{ $('Parse Command').first().json.exec_id }}",
"state": "dispatched"
},
"matchingColumns": [
"exec_id"
],
"schema": [
{
"id": "exec_id",
"displayName": "exec_id",
"type": "string",
"display": true,
"canBeUsedToMatch": true,
"defaultMatch": true,
"required": false,
"removed": false
},
{
"id": "state",
"displayName": "state",
"type": "string",
"display": true,
"canBeUsedToMatch": true,
"defaultMatch": false,
"required": false,
"removed": false
}
]
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1920,
0
],
"id": "99999999-aaaa-aaaa-aaaa-999999999999",
"name": "Mark Draft Dispatched",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"authentication": "serviceAccount",
"operation": "update",
"documentId": {
"__rl": true,
"value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "content-plan"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"id": "={{ $('Hydrate').first().json.draft.topic_id }}",
"published_url": "=dispatched-{{ $('Parse Command').first().json.exec_id }}"
},
"matchingColumns": [
"id"
],
"schema": [
{
"id": "id",
"displayName": "id",
"type": "string",
"display": true,
"canBeUsedToMatch": true,
"defaultMatch": true,
"required": false,
"removed": false
},
{
"id": "published_url",
"displayName": "published_url",
"type": "string",
"display": true,
"canBeUsedToMatch": false,
"required": false,
"removed": false
}
]
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
2160,
0
],
"id": "99999999-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"name": "Mark Topic Published",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Command').first().json.chat_id }}",
"text": "=\ud83d\ude80 <b>\u041f\u0440\u0438\u043d\u044f\u0442\u043e \u2014 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u044e \u0432 \u0431\u043b\u043e\u0433</b>\n\n<b>{{ $('Build Dispatch Payload').first().json.article_title }}</b>\n\n\u0427\u0435\u0440\u0435\u0437 ~1 \u043c\u0438\u043d\u0443\u0442\u0443 \u0432 \u0440\u0435\u043f\u043e <a href=\"https://github.com/puffy-pet/puffy-site/pulls\">puffy-site</a> \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f PR <code>auto/publish-{{ $('Build Dispatch Payload').first().json.article_id }}</code>. \u041f\u043e\u0441\u043c\u043e\u0442\u0440\u0438 \u0447\u0442\u043e \u0432\u044b\u0448\u043b\u043e, \u043f\u043e\u043f\u0440\u0430\u0432\u044c \u0435\u0441\u043b\u0438 \u0447\u0442\u043e \u2014 \u0438 \u043d\u0430\u0436\u043c\u0438 Merge. \u0427\u0435\u0440\u0435\u0437 \u043c\u0438\u043d\u0443\u0442\u0443 \u043f\u043e\u0441\u043b\u0435 merge \u0441\u0442\u0430\u0442\u044c\u044f \u0431\u0443\u0434\u0435\u0442 live.\n\n<i>exec_id: <code>{{ $('Parse Command').first().json.exec_id }}</code></i>",
"replyToMessageId": "={{ $('Parse Command').first().json.reply_to_message_id }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML",
"disable_web_page_preview": true
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2400,
0
],
"id": "aaaaaaaa-bbbb-bbbb-bbbb-aaaaaaaaaaaa",
"name": "Telegram: Approved",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"authentication": "serviceAccount",
"operation": "update",
"documentId": {
"__rl": true,
"value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "drafts",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"exec_id": "={{ $('Parse Command').first().json.exec_id }}",
"state": "rejected"
},
"matchingColumns": [
"exec_id"
],
"schema": [
{
"id": "exec_id",
"displayName": "exec_id",
"type": "string",
"display": true,
"canBeUsedToMatch": true,
"defaultMatch": true,
"required": false,
"removed": false
},
{
"id": "state",
"displayName": "state",
"type": "string",
"display": true,
"canBeUsedToMatch": true,
"defaultMatch": false,
"required": false,
"removed": false
}
]
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1440,
200
],
"id": "bbbbbbbb-cccc-cccc-cccc-bbbbbbbbbbbb",
"name": "Mark Draft Rejected",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Command').first().json.chat_id }}",
"text": "=\u2717 <b>\u041e\u0442\u043a\u043b\u043e\u043d\u0438\u043b</b> \u2014 \u0441\u0442\u0430\u0442\u044c\u044f \u043d\u0435 \u043f\u043e\u0439\u0434\u0451\u0442 \u0432 \u0431\u043b\u043e\u0433. \u0422\u0435\u043c\u0430 \u043e\u0441\u0442\u0430\u043b\u0430\u0441\u044c \u0432 content-plan, \u043f\u0440\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u0437\u0430\u043f\u0443\u0441\u043a\u0435 Claude \u043d\u0430\u043f\u0438\u0448\u0435\u0442 \u0434\u0440\u0443\u0433\u043e\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442.\n\n<i>exec_id: <code>{{ $('Parse Command').first().json.exec_id }}</code></i>",
"replyToMessageId": "={{ $('Parse Command').first().json.reply_to_message_id }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1680,
200
],
"id": "cccccccc-dddd-dddd-dddd-cccccccccccc",
"name": "Telegram: Rejected",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"authentication": "serviceAccount",
"documentId": {
"__rl": true,
"value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "drafts",
"mode": "name"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
720,
400
],
"id": "11ee0001-1111-1111-1111-111111111111",
"name": "Read All Drafts",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Build a formatted message + inline-keyboard for /list (or /queue).\n// Shows all drafts with state=awaiting_review. Each draft gets [\u2705 Publish]\n// [\u270f Edit] [\u2717 Reject] inline buttons (callback_data: act:<command>:<exec_id>).\n// Fall back to copyable exec_ids + text commands when queue is empty.\nconst rows = $input.all().map(r => r.json);\nconst awaiting = rows.filter(r => r.state === 'awaiting_review');\nconst cmd = $('Parse Command').first().json;\n\nfunction escapeHtml(s) {\n return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\nif (awaiting.length === 0) {\n return [{\n json: {\n chat_id: cmd.chat_id,\n reply_to_message_id: cmd.reply_to_message_id,\n text: '\ud83d\udced <b>\u041e\u0447\u0435\u0440\u0435\u0434\u044c \u043f\u0443\u0441\u0442\u0430.</b>\\n\\n\u041d\u0435\u0442 \u0434\u0440\u0430\u0444\u0442\u043e\u0432, \u0436\u0434\u0443\u0449\u0438\u0445 \u0440\u0435\u0432\u044c\u044e. 02a \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0441\u0442\u0430\u0442\u044c\u044e \u0432 08:00 \u041c\u0421\u041a (cron), \u043b\u0438\u0431\u043e \u043d\u0430\u0436\u043c\u0438 Execute workflow \u043d\u0430 02a \u0432 n8n, \u043b\u0438\u0431\u043e \u043e\u0442\u043f\u0440\u0430\u0432\u044c <code>/topic <\u0442\u0435\u043c\u0430></code>, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043a\u0430\u0437\u0430\u0442\u044c \u0442\u0435\u043c\u0443 \u0441\u0435\u0439\u0447\u0430\u0441.',\n keyboard_rows: [],\n },\n }];\n}\n\nconst MAX_CARDS = 8;\nconst cards = awaiting.slice(0, MAX_CARDS);\n\nconst sections = cards.map((d, i) => {\n let title = '?';\n let excerpt = '';\n try {\n const article = JSON.parse(d.article_json);\n title = article.title || '?';\n const lede = (article.body || []).find(b => b.type === 'lede');\n if (lede) {\n const sm = (lede.text || '').match(/^[^.!?]+[.!?]/);\n excerpt = sm ? sm[0].trim() : lede.text;\n if (excerpt.length > 200) excerpt = excerpt.slice(0, 197) + '...';\n }\n } catch (e) {}\n\n return `<b>${i + 1}. ${escapeHtml(title)}</b>\\n<i>${escapeHtml(excerpt)}</i>\\n<code>${d.exec_id}</code>`;\n});\n\nlet text = `\ud83d\udfe1 <b>\u041e\u0447\u0435\u0440\u0435\u0434\u044c \u043d\u0430 \u0440\u0435\u0432\u044c\u044e (${awaiting.length})</b>\\n\\n${sections.join('\\n\\n')}`;\nif (awaiting.length > MAX_CARDS) {\n text += `\\n\\n<i>\u2026 \u0438 \u0435\u0449\u0451 ${awaiting.length - MAX_CARDS}. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0439 exec_id \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e.</i>`;\n}\ntext += `\\n\\n\u0416\u043c\u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043f\u043e\u0434 \u043d\u0443\u0436\u043d\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0451\u0439, \u0438\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439 <code>/edit <exec_id> <field>=<value></code>, <code>/topic <\u0442\u0435\u043c\u0430></code>.`;\n\n// Build keyboard_rows in n8n's nested fixedCollection shape:\n// [{ row: { buttons: [{ text, additionalFields: { callback_data } }] } }, ...]\nconst keyboard_rows = cards.map((d, i) => ({\n row: {\n buttons: [\n { text: `${i + 1}. \u2705 Publish`, additionalFields: { callback_data: `act:approve:${d.exec_id}` } },\n { text: '\u270f Edit', additionalFields: { callback_data: `act:edit:${d.exec_id}` } },\n { text: '\u2717 Reject', additionalFields: { callback_data: `act:reject:${d.exec_id}` } },\n ],\n },\n}));\n\nreturn [{\n json: {\n chat_id: cmd.chat_id,\n reply_to_message_id: cmd.reply_to_message_id,\n text,\n keyboard_rows,\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
400
],
"id": "11ee0002-2222-2222-2222-222222222222",
"name": "Format List"
},
{
"parameters": {
"chatId": "={{ $json.chat_id }}",
"text": "={{ $json.text }}",
"replyToMessageId": "={{ $json.reply_to_message_id }}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": "={{ $json.keyboard_rows }}"
},
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML",
"disable_web_page_preview": true
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1200,
400
],
"id": "11ee0003-3333-3333-3333-333333333333",
"name": "Send List",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// /topic <free text> \u2014 build a content-plan row for an ad-hoc topic.\n// Output keys must EXACTLY match content-plan column headers \u2014 autoMapInputData\n// in the next node maps by name. No extra keys (chat_id etc) \u2014 Telegram nodes\n// downstream pull those from $('Parse Command') directly.\nconst cmd = $('Parse Command').first().json;\nconst raw = (cmd.topic_text || '').trim();\n\nif (!raw) {\n throw new Error('Empty topic text');\n}\n\nconst slug = raw\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 40);\n\nconst now = new Date();\nconst pad = (n) => String(n).padStart(2, '0');\nconst dateSuffix = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}`;\nconst id = `${slug || 'manual-topic'}-${dateSuffix}`;\n\nconst week_start = (() => {\n const d = new Date(now);\n const day = d.getUTCDay();\n const diff = day === 0 ? -6 : 1 - day;\n d.setUTCDate(d.getUTCDate() + diff);\n return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;\n})();\n\nreturn [{\n json: {\n id,\n week_start,\n title: raw,\n cluster: 'Manual',\n category: 'Guides',\n target_keyword: raw,\n search_intent: 'Manual entry from /topic command',\n channels: 'blog,instagram,tiktok,x',\n length: '10 min',\n seasonal: 'FALSE',\n rationale: `Manual /topic from marketing chat at ${now.toISOString()}`,\n status: 'approved',\n created_at: now.toISOString(),\n published_url: '',\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
600
],
"id": "11ee0004-4444-4444-4444-444444444444",
"name": "Build Topic Row"
},
{
"parameters": {
"authentication": "serviceAccount",
"operation": "append",
"documentId": {
"__rl": true,
"value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "content-plan"
},
"columns": {
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"schema": []
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
960,
600
],
"id": "11ee0005-5555-5555-5555-555555555555",
"name": "Append Topic to Content Plan",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://n8n-production-5692.up.railway.app/webhook/02a-manual-topic",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($('Build Topic Row').first().json) }}",
"options": {
"redirect": {
"redirect": {}
},
"timeout": 5000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1200,
600
],
"id": "11ee0011-1111-1111-1111-111111111111",
"name": "Trigger 02a Webhook",
"alwaysOutputData": true,
"onError": "continueRegularOutput"
},
{
"parameters": {
"chatId": "={{ $('Parse Command').first().json.chat_id }}",
"text": "=\ud83d\udcdd <b>\u0422\u0435\u043c\u0430 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430 02a.</b>\n\n<b>{{ $('Build Topic Row').first().json.title }}</b>\n<code>{{ $('Build Topic Row').first().json.id }}</code>\n\n\u0413\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u2014 \u043f\u0440\u0435\u0432\u044c\u044e \u043f\u0440\u0438\u043b\u0435\u0442\u0438\u0442 \u0432 \u0447\u0430\u0442 \u0447\u0435\u0440\u0435\u0437 ~1.5\u20132 \u043c\u0438\u043d\u0443\u0442\u044b (Wikipedia \u2192 draft \u2192 polish \u2192 hero SVG, ~$0.06).\n\n<i>\u0415\u0441\u043b\u0438 02a \u043d\u0435 \u043e\u0442\u0432\u0435\u0442\u0438\u0442 \u0437\u0430 3 \u043c\u0438\u043d \u2014 \u043e\u0442\u043a\u0440\u043e\u0439 /list, \u0438\u043b\u0438 \u0437\u0430\u0439\u0434\u0438 \u0432 n8n \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044c Executions \u0443 02a.</i>",
"replyToMessageId": "={{ $('Parse Command').first().json.reply_to_message_id }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1440,
600
],
"id": "11ee0006-6666-6666-6666-666666666666",
"name": "Telegram: Topic Added",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"authentication": "serviceAccount",
"documentId": {
"__rl": true,
"value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "drafts",
"mode": "name"
},
"filtersUI": {
"values": [
{
"lookupColumn": "exec_id",
"lookupValue": "={{ $json.exec_id }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
720,
800
],
"id": "11ee0007-7777-7777-7777-777777777777",
"name": "Get Draft for Edit",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Apply a single-field edit to a draft's article_json.\n// Allowed fields: title, category, excerpt (rewrites first lede block), read.\n// Anything else is rejected with a friendly Telegram error.\nconst cmd = $('Parse Command').first().json;\nconst draft = $input.first().json;\n\nif (!draft || !draft.exec_id) {\n return [{\n json: {\n ...cmd,\n ok: false,\n error: `Draft <code>${cmd.exec_id}</code> not found.`,\n },\n }];\n}\n\nconst field = (cmd.edit_field || '').toLowerCase();\nconst value = cmd.edit_value || '';\n\nconst ALLOWED = ['title', 'category', 'excerpt', 'read'];\nif (!ALLOWED.includes(field)) {\n return [{\n json: {\n ...cmd,\n ok: false,\n error: `\u041f\u043e\u043b\u0435 <code>${field}</code> \u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f. \u0420\u0430\u0437\u0440\u0435\u0448\u0451\u043d\u043d\u044b\u0435: ${ALLOWED.join(', ')}.`,\n },\n }];\n}\n\nlet article;\ntry {\n article = JSON.parse(draft.article_json);\n} catch (e) {\n return [{\n json: {\n ...cmd,\n ok: false,\n error: `\u041d\u0435 \u0441\u043c\u043e\u0433 \u0440\u0430\u0441\u043f\u0430\u0440\u0441\u0438\u0442\u044c article_json: ${e.message}`,\n },\n }];\n}\n\nlet oldValue;\nif (field === 'excerpt') {\n // \"excerpt\" maps to the lede block text\n const lede = (article.body || []).find(b => b.type === 'lede');\n oldValue = lede ? lede.text : '';\n if (lede) {\n lede.text = value;\n } else {\n (article.body = article.body || []).unshift({ type: 'lede', text: value });\n }\n} else {\n oldValue = article[field];\n article[field] = value;\n}\n\nreturn [{\n json: {\n ...cmd,\n ok: true,\n article,\n article_json: JSON.stringify(article),\n field,\n old_value: String(oldValue || ''),\n new_value: value,\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
800
],
"id": "11ee0008-8888-8888-8888-888888888888",
"name": "Apply Edit"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-edit-ok",
"leftValue": "={{ $json.ok }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"outputKey": "ok"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-edit-failed",
"leftValue": "={{ $json.ok }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
}
}
],
"combinator": "and"
},
"outputKey": "failed"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1200,
800
],
"id": "11ee0009-9999-9999-9999-999999999999",
"name": "Edit OK?"
},
{
"parameters": {
"authentication": "serviceAccount",
"operation": "update",
"documentId": {
"__rl": true,
"value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "drafts",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"exec_id": "={{ $json.exec_id }}",
"article_json": "={{ $json.article_json }}"
},
"matchingColumns": [
"exec_id"
],
"schema": [
{
"id": "exec_id",
"displayName": "exec_id",
"type": "string",
"display": true,
"canBeUsedToMatch": true,
"defaultMatch": true,
"required": false,
"removed": false
},
{
"id": "article_json",
"displayName": "article_json",
"type": "string",
"display": true,
"canBeUsedToMatch": false,
"required": false,
"removed": false
}
]
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1440,
800
],
"id": "11ee000a-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"name": "Update Draft Article",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Command').first().json.chat_id }}",
"text": "=\u270f <b>\u041f\u043e\u043b\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043e</b>\n\n<b>{{ $('Apply Edit').first().json.field }}</b>:\n<i>\u0431\u044b\u043b\u043e</i>: {{ $('Apply Edit').first().json.old_value }}\n<i>\u0441\u0442\u0430\u043b\u043e</i>: {{ $('Apply Edit').first().json.new_value }}\n\n<code>{{ $('Parse Command').first().json.exec_id }}</code>\n\n\u041f\u0440\u043e\u0432\u0435\u0440\u044c \u0438 \u0436\u043c\u0438 <code>/approve {{ $('Parse Command').first().json.exec_id }}</code> (\u0438\u043b\u0438 \u2705 Publish \u0438\u0437 <code>/list</code>).",
"replyToMessageId": "={{ $('Parse Command').first().json.reply_to_message_id }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1680,
800
],
"id": "11ee000b-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"name": "Telegram: Edit Done",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Command').first().json.chat_id }}",
"text": "=\u26a0\ufe0f <b>\u041d\u0435 \u0441\u043c\u043e\u0433 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c</b>\n\n{{ $json.error }}\n\n\u0424\u043e\u0440\u043c\u0430\u0442: <code>/edit <exec_id> <field>=<value></code>\n\u041f\u0440\u0438\u043c\u0435\u0440: <code>/edit abc123 title=\"\u041d\u043e\u0432\u043e\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\"</code>\n\n\u041f\u043e\u043b\u044f: <code>title</code>, <code>category</code>, <code>excerpt</code>, <code>read</code>.",
"replyToMessageId": "={{ $('Parse Command').first().json.reply_to_message_id }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1440,
1000
],
"id": "11ee000c-cccc-cccc-cccc-cccccccccccc",
"name": "Telegram: Edit Failed",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "rule-is-callback",
"leftValue": "={{ $json.source }}",
"rightValue": "callback_query",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"outputKey": "callback"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
240,
200
],
"id": "11ee000d-dddd-dddd-dddd-dddddddddddd",
"name": "Is Callback?"
},
{
"parameters": {
"resource": "callback",
"operation": "answerQuery",
"queryId": "={{ $json.callback_query_id }}",
"additionalFields": {}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
480,
200
],
"id": "11ee000e-eeee-eeee-eeee-eeeeeeeeeeee",
"name": "Answer Callback Query",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "02d-claude-chat-puffy",
"mode": "id"
},
"options": {}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
720,
1200
],
"id": "11ee0010-0000-0000-0000-000000000010",
"name": "Call 02d Chat"
}
],
"connections": {
"Telegram Trigger": {
"main": [
[
{
"node": "Parse Command",
"type": "main",
"index": 0
}
]
]
},
"Parse Command": {
"main": [
[
{
"node": "Route Class",
"type": "main",
"index": 0
},
{
"node": "Is Callback?",
"type": "main",
"index": 0
}
]
]
},
"Is Callback?": {
"main": [
[
{
"node": "Answer Callback Query",
"type": "main",
"index": 0
}
]
]
},
"Route Class": {
"main": [
[
{
"node": "Get Draft Row",
"type": "main",
"index": 0
}
],
[
{
"node": "Read All Drafts",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Topic Row",
"type": "main",
"index": 0
}
],
[
{
"node": "Get Draft for Edit",
"type": "main",
"index": 0
}
],
[
{
"node": "Call 02d Chat",
"type": "main",
"index": 0
}
]
]
},
"Get Draft Row": {
"main": [
[
{
"node": "Hydrate",
"type": "main",
"index": 0
}
]
]
},
"Hydrate": {
"main": [
[
{
"node": "Route Action",
"type": "main",
"index": 0
}
]
]
},
"Route Action": {
"main": [
[
{
"node": "Telegram: Already Processed",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Dispatch Payload",
"type": "main",
"index": 0
}
],
[
{
"node": "Mark Draft Rejected",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Dispatch Payload",
"type": "main",
"index": 0
}
]
]
},
"Build Dispatch Payload": {
"main": [
[
{
"node": "GitHub: Dispatch Action",
"type": "main",
"index": 0
}
]
]
},
"GitHub: Dispatch Action": {
"main": [
[
{
"node": "Mark Draft Dispatched",
"type": "main",
"index": 0
}
]
]
},
"Mark Draft Dispatched": {
"main": [
[
{
"node": "Mark Topic Published",
"type": "main",
"index": 0
}
]
]
},
"Mark Topic Published": {
"main": [
[
{
"node": "Telegram: Approved",
"type": "main",
"index": 0
}
]
]
},
"Mark Draft Rejected": {
"main": [
[
{
"node": "Telegram: Rejected",
"type": "main",
"index": 0
}
]
]
},
"Read All Drafts": {
"main": [
[
{
"node": "Format List",
"type": "main",
"index": 0
}
]
]
},
"Format List": {
"main": [
[
{
"node": "Send List",
"type": "main",
"index": 0
}
]
]
},
"Build Topic Row": {
"main": [
[
{
"node": "Append Topic to Content Plan",
"type": "main",
"index": 0
}
]
]
},
"Append Topic to Content Plan": {
"main": [
[
{
"node": "Trigger 02a Webhook",
"type": "main",
"index": 0
}
]
]
},
"Trigger 02a Webhook": {
"main": [
[
{
"node": "Telegram: Topic Added",
"type": "main",
"index": 0
}
]
]
},
"Get Draft for Edit": {
"main": [
[
{
"node": "Apply Edit",
"type": "main",
"index": 0
}
]
]
},
"Apply Edit": {
"main": [
[
{
"node": "Edit OK?",
"type": "main",
"index": 0
}
]
]
},
"Edit OK?": {
"main": [
[
{
"node": "Update Draft Article",
"type": "main",
"index": 0
}
],
[
{
"node": "Telegram: Edit Failed",
"type": "main",
"index": 0
}
]
]
},
"Update Draft Article": {
"main": [
[
{
"node": "Telegram: Edit Done",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"versionId": "00000000-0000-0000-0000-000000000020",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "02b-article-callback-puffy",
"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.
googleApihttpHeaderAuthtelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
02b — Article callback. Uses telegramTrigger, googleSheets, telegram, httpRequest. Event-driven trigger; 30 nodes.
Source: https://github.com/puffy-pet/puffy-automation/blob/93987b4006c730c43fec3389026825d63e59e3c3/workflows/02b-article-callback.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.
This workflow provides a complete solution for handling Telegram Stars payments, invoicing and refunds using n8n. It automates the process of sending invoices, managing pre-checkout approvals, recordi
clients kept booking meetings during my prayer times. i'd either miss a prayer or scramble to reschedule. the problem wasn't the clients — it was that my calendar had no blocked windows for salah. i n
Generate 360° product videos from a single photo using Google Veo 3 and Telegram
Automates LinkedIn job searches across multiple countries and categories, filters results with AI, stores data in Google Sheets, and sends weekly Telegram notifications. Perfect for professionals seek
Telegram-Command-Interface.N8N. Uses telegramTrigger, httpRequest, googleSheets, telegram. Event-driven trigger; 25 nodes.