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
This n8n workflow allows users to generate AI-generated images by sending messages to a Telegram bot. Each request is logged in Google Sheets and limited by a daily quota per user. Image prompts are e