AutomationFlowsAI & RAG › Generate and Approve Twitter News Tweets with Groq, Google Sheets and Telegram

Generate and Approve Twitter News Tweets with Groq, Google Sheets and Telegram

ByShreya Bhingarkar @shreya-bhingarkar on n8n.io

This n8n template automates the creation and approval of Twitter (X) content using real-time news data, AI-generated text, and a human approval workflow.

Event trigger★★★★☆ complexityAI-powered24 nodesTelegram TriggerGoogle SheetsTelegramAgentHTTP RequestGroq Chat
AI & RAG Trigger: Event Nodes: 24 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #14842 — we link there as the canonical source.

This workflow follows the Agent → Google Sheets recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "nodes": [
    {
      "id": "d4e41eb2-473a-4496-a54f-7893da5185ae",
      "name": "When Telegram Callback Triggered",
      "type": "n8n-nodes-base.telegramTrigger",
      "position": [
        128,
        4064
      ],
      "parameters": {
        "updates": [
          "callback_query"
        ],
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "62e8e538-4ad3-4cb1-91be-df8983e9e4b1",
      "name": "Extract Telegram Callback",
      "type": "n8n-nodes-base.code",
      "position": [
        352,
        4064
      ],
      "parameters": {
        "jsCode": "const cb = $json.callback_query;\nif (!cb) throw new Error('No callback_query');\n\nconst data = cb.data || '';\n\n// EXPECT: approve::rowId OR reject::rowId\nconst [action, rowId] = data.split('::');\n\nreturn [{\n  json: {\n    action: action,         // approve / reject\n    rowId: rowId,           // actual row id\n    chatId: String(cb.message.chat.id),\n    messageId: cb.message.message_id,\n    callbackQueryId: cb.id\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "e3eac23d-756f-4a6e-abd7-ae3f50887c04",
      "name": "Update Approval in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        800,
        3968
      ],
      "parameters": {
        "columns": {
          "value": {
            "Row ID": "={{ $json.rowId }}",
            "Status": "Approved",
            "row_number": 0
          },
          "schema": [
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Topic",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Topic",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Style",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Style",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tweet",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Tweet",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Hashtags",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Hashtags",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Scheduled Time",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Scheduled Time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Created At",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Created At",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Row ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Row ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Row ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "YOUR_GOOGLE_NAME"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "4901d347-fa14-4593-a365-80464c8e5825",
      "name": "Update Rejection in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        800,
        4160
      ],
      "parameters": {
        "columns": {
          "value": {
            "Row ID": "={{ $('Extract Telegram Callback').item.json.rowId }}",
            "Status": "Rejected",
            "row_number": 0
          },
          "schema": [
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Topic",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Topic",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Style",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Style",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tweet",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Tweet",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Hashtags",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Hashtags",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Scheduled Time",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Scheduled Time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Created At",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Created At",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Row ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Row ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Row ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "YOUR_GOOGLE_NAME"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "edd53ab8-1bd8-403a-810e-e7104f1c8612",
      "name": "Send Approval Confirmation",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1024,
        3968
      ],
      "parameters": {
        "text": "=\u2705 Tweet approved! Status updated in sheet.",
        "chatId": "={{ $('Extract Telegram Callback').first().json.chatId }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "7e55529d-cccf-4a3a-ad4f-f111c0dea1bb",
      "name": "Send Rejection Confirmation",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1024,
        4160
      ],
      "parameters": {
        "text": "=\u274c Tweet rejected. Status updated in sheet.",
        "chatId": "={{ $('Extract Telegram Callback').first().json.chatId }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "884e576f-bf5c-4360-b44f-ef834318e50f",
      "name": "When Scheduled at 8 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        128,
        3488
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "f1a2eece-cf08-4e43-84a8-873e295ab488",
      "name": "Process Article Extraction",
      "type": "n8n-nodes-base.code",
      "position": [
        784,
        3488
      ],
      "parameters": {
        "jsCode": "if ($input.all().length > 1 && $input.itemIndex > 0) {\n  return [];\n}\n// SAFE RSS ACCESS\nconst rssItems = $('Fetch Google News RSS').all();\n\nif (!rssItems.length) {\n  throw new Error('RSS data missing');\n}\n\nconst xml = rssItems[0].json.data;\n\n\n// GET USED ARTICLES SAFELY (WORKS EVEN IF SHEET EMPTY)\nlet usedTitles = [];\n\ntry {\n  const usedItems = $('Read Used Articles').all();\n\n  usedTitles = usedItems\n    .map(item => item.json[\"Title\"] || item.json[\"Title \"] || \"\")\n    .filter(Boolean)\n    .map(t => t.toLowerCase().trim());\n\n} catch (e) {\n  usedTitles = [];\n}\n\n\n// EXTRACT RSS ITEMS\nconst itemRegex = /<item>([\\s\\S]*?)<\\/item>/g;\nconst items = [];\nlet match;\n\nwhile ((match = itemRegex.exec(xml)) !== null) {\n  items.push(match[1]);\n}\n\n\n// PARSE ARTICLES\nconst articles = items.slice(0, 10).map(item => {\n  const title = (item.match(/<title>([\\s\\S]*?)<\\/title>/) || [])[1] || '';\n  const description = (item.match(/<description>([\\s\\S]*?)<\\/description>/) || [])[1] || '';\n  const pubDate = (item.match(/<pubDate>([\\s\\S]*?)<\\/pubDate>/) || [])[1] || '';\n  const link = (item.match(/<link>([\\s\\S]*?)<\\/link>/) || [])[1] || '';\n\n  return {\n    title: title\n      .replace(/<!\\[CDATA\\[|\\]\\]>/g, '')\n      .replace(/\\s*-\\s*[\\w.]+\\.(com|in|org|net|co\\.in)$/i, '')\n      .trim(),\n\n    description: description\n      .replace(/<!\\[CDATA\\[|\\]\\]>/g, '')\n      .replace(/<[^>]+>/g, '')\n      .trim(),\n\n    pubDate: pubDate.trim(),\n    link: link.trim()\n  };\n});\n\n\n// REMOVE EMPTY TITLES\nconst validArticles = articles.filter(a => a.title.length > 5);\n\n\n// REMOVE ALREADY USED ARTICLES\nconst unused = validArticles.filter(a =>\n  !usedTitles.includes(a.title.toLowerCase().trim())\n);\n\n\n// HANDLE FULLY USED CASE\nconst pool = unused.length > 0 ? unused : validArticles;\n\nif (pool.length === 0) {\n  throw new Error('No valid articles found in RSS feed');\n}\n\n\n// RANDOM PICK\nconst randomIndex = Math.floor(Math.random() * pool.length);\nconst top = pool[randomIndex];\n\n\n// EXTRA SAFETY CHECK\nconst isAlreadyUsed = usedTitles.includes(top.title.toLowerCase().trim());\n\nif (isAlreadyUsed && unused.length > 0) {\n  const fallback = unused[0];\n\n  return [{\n    json: {\n      topic: fallback.title,\n      description: fallback.description,\n      pubDate: fallback.pubDate,\n      link: fallback.link,\n      unusedCount: unused.length,\n      totalArticles: validArticles.length,\n      usedCount: usedTitles.length,\n      note: 'fallback used'\n    }\n  }];\n}\n\n\n// FINAL OUTPUT\nreturn [{\n  json: {\n    topic: top.title,\n    description: top.description,\n    pubDate: top.pubDate,\n    link: top.link,\n    unusedCount: unused.length,\n    totalArticles: validArticles.length,\n    usedCount: usedTitles.length,\n    note: 'random pick'\n  }\n}];"
      },
      "executeOnce": false,
      "typeVersion": 2
    },
    {
      "id": "f24c22d5-98c0-4c1a-a3f9-abfdd3a02e5d",
      "name": "AI Tweet Generation Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1024,
        3456
      ],
      "parameters": {
        "text": "=You are a professional Twitter/X content strategist.\n\nBased on this trending news article:\nTopic: {{ $json.topic }}\nSummary: {{ $json.description }}\nPublished: {{ $json.pubDate }}\n\nGenerate exactly 5 tweets, each in a different style:\n1. Informational\n2. Hot take\n3. Question\n4. Quote style (NO quotation marks)\n5. Thread hook (ends with 1/5)\n\nSTRICT RULES:\n- Return ONLY valid JSON\n- DO NOT include markdown, backticks, explanation, or extra text\n- Use ONLY standard double quotes (\")\n- Escape any quotes inside text\n- Max 260 characters per tweet\n- Max 3 hashtags per tweet\n\nFORMAT:\n{\n  \"tweets\": [\n    {\"style\": \"informational\", \"text\": \"...\", \"hashtags\": \"...\"},\n    {\"style\": \"hot take\", \"text\": \"...\", \"hashtags\": \"...\"},\n    {\"style\": \"question\", \"text\": \"...\", \"hashtags\": \"...\"},\n    {\"style\": \"quote\", \"text\": \"...\", \"hashtags\": \"...\"},\n    {\"style\": \"thread hook\", \"text\": \"...\", \"hashtags\": \"...\"}\n  ]\n}",
        "options": {
          "systemMessage": "You are a professional Twitter/X content strategist. Always return raw JSON only. Never add markdown, backticks, or explanation outside the JSON object."
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "c2db3023-fb97-4b87-a6fb-3984236f3df2",
      "name": "Parse Tweet Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1408,
        3488
      ],
      "parameters": {
        "jsCode": "const raw = $json.output || $json.text || JSON.stringify($json);\n\n// Nuclear clean all problematic characters\nlet clean = raw\n  .replace(/[\\u201C\\u201D\\u201E\\u201F\\u2033\\u2036]/g, '\"')\n  .replace(/[\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035]/g, \"'\")\n  .replace(/\\\\n/g, ' ')\n  .replace(/\\\\t/g, ' ')\n  .replace(/\\\\r/g, ' ')\n  .replace(/```json/g, '')\n  .replace(/```/g, '')\n  .trim();\n\n// Extract JSON object\nconst match = clean.match(/\\{[\\s\\S]*\\}/);\nif (!match) {\n  return [{\n    json: {\n      error: 'AI output invalid',\n      rawOutput: raw\n    }\n  }];\n}\n\n// Fix any remaining smart quotes inside JSON strings\nclean = clean.replace(/\"\\s*:\\s*\"([^\"]*?)\"/g, (m, p1) => {\n  return m.replace(/[\\u201C\\u201D\\u201E\\u201F]/g, '\\\\\"');\n});\n\nlet parsed;\ntry {\n  parsed = JSON.parse(clean);\n} catch(e) {\n  // Last resort \u2014 manually extract tweets using regex\n  const tweetRegex = /\"text\"\\s*:\\s*\"(.*?)(?<!\\\\)\"/g;\n  const hashRegex = /\"hashtags\"\\s*:\\s*\"(.*?)(?<!\\\\)\"/g;\n  const styleRegex = /\"style\"\\s*:\\s*\"(.*?)(?<!\\\\)\"/g;\n\n  const texts = [...clean.matchAll(/\"text\"\\s*:\\s*\"([^\"]+)\"/g)].map(m => m[1]);\n  const hashes = [...clean.matchAll(/\"hashtags\"\\s*:\\s*\"([^\"]+)\"/g)].map(m => m[1]);\n  const styles = [...clean.matchAll(/\"style\"\\s*:\\s*\"([^\"]+)\"/g)].map(m => m[1]);\n\n  if (texts.length === 0) throw new Error('Could not parse tweets: ' + e.message);\n\n  parsed = {\n    tweets: texts.map((text, i) => ({\n      style: styles[i] || 'unknown',\n      text: text,\n      hashtags: hashes[i] || ''\n    }))\n  };\n}\n\nconst topic = $('Process Article Extraction').first().json.topic;\nconst description = $('Process Article Extraction').first().json.description;\n\nconst times = ['08:00 IST', '10:00 IST', '12:00 IST', '14:00 IST', '16:00 IST'];\n\nconst now = new Date();\nconst istOffset = 5.5 * 60 * 60000;\nconst istNow = new Date(now.getTime() + istOffset);\nconst today = istNow.toISOString().split('T')[0];\nconst createdAtIST = istNow.toISOString().replace('Z', '+05:30');\n\nconst rows = parsed.tweets.map((tweet, index) => {\n  const fullPost = tweet.text + ' ' + tweet.hashtags;\n  const rowId = tweet.style + '_' + today + '_' + Date.now();\n\n  const blocks = [\n    {\n      type: \"section\",\n      text: {\n        type: \"mrkdwn\",\n        text: \"*Tweet ready for approval*\\n\\n*Style:* \" + tweet.style + \"\\n*Scheduled:* \" + times[index] + \"\\n*Topic:* \" + topic + \"\\n\\n*Tweet:*\\n\" + tweet.text + \"\\n\\n\" + tweet.hashtags\n      }\n    },\n    {\n      type: \"actions\",\n      block_id: rowId,\n      elements: [\n        {\n          type: \"button\",\n          text: { type: \"plain_text\", text: \"Approve\" },\n          style: \"primary\",\n          value: \"approved\",\n          action_id: \"approve_tweet\"\n        },\n        {\n          type: \"button\",\n          text: { type: \"plain_text\", text: \"Reject\" },\n          style: \"danger\",\n          value: \"rejected\",\n          action_id: \"reject_tweet\"\n        }\n      ]\n    }\n  ];\n\n  return {\n    json: {\n      topic: topic,\n      description: description,\n      style: tweet.style,\n      tweet: tweet.text,\n      hashtags: tweet.hashtags,\n      fullPost: fullPost,\n      charCount: fullPost.length,\n      scheduledTime: times[index],\n      scheduledDate: today,\n      status: 'Pending',\n      createdAt: createdAtIST,\n      rowId: rowId,\n      slackBlocks: JSON.stringify(blocks)\n    }\n  };\n});\n\nreturn rows;"
      },
      "typeVersion": 2
    },
    {
      "id": "a2fd206b-ce48-456a-a44b-3b8f0f785019",
      "name": "Log Tweets in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1856,
        3488
      ],
      "parameters": {
        "columns": {
          "value": {
            "Date": "={{ $now.toFormat('yyyy-MM-dd') }}",
            "Style": "={{ $('Parse Tweet Data').item.json.style }}",
            "Topic": "={{ $('Parse Tweet Data').item.json.topic }}",
            "Tweet": "={{ $('Parse Tweet Data').item.json.tweet }}",
            "Row ID": "={{ $('Parse Tweet Data').item.json.rowId }}",
            "Status": "={{ $('Parse Tweet Data').item.json.status }}",
            "Hashtags": "={{ $('Parse Tweet Data').item.json.hashtags }}",
            "Created At": "={{ $('Parse Tweet Data').item.json.createdAt }}",
            "Scheduled Time": "={{ $('Parse Tweet Data').item.json.scheduledTime }}"
          },
          "schema": [
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Topic",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Topic",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Style",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Style",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tweet",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Tweet",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Hashtags",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Hashtags",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Scheduled Time",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Scheduled Time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Created At",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Created At",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Row ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Row ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "YOUR_GOOGLE_NAME"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "6c823455-2add-4c77-a553-0efc5ae4d33b",
      "name": "Fetch Google News RSS",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        352,
        3488
      ],
      "parameters": {
        "url": "https://news.google.com/rss?hl=en-IN&gl=IN&ceid=IN:en",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "n8n-bot"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "4dd7cf28-6f51-488a-9f11-f04fcd1c0c1e",
      "name": "Read Used Articles",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        576,
        3488
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "YOUR_GOOGLE_NAME"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "4510619f-40c4-4bc9-a343-fb34a5dba556",
      "name": "Log Article as Used",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2304,
        3488
      ],
      "parameters": {
        "columns": {
          "value": {
            "Title ": "={{ $('Process Article Extraction').first().json.topic }}",
            "Date Used": "={{ $now.toFormat('yyyy-MM-dd') }}"
          },
          "schema": [
            {
              "id": "Date Used",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Date Used",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Title ",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Title ",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Row ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "YOUR_GOOGLE_NAME"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "c0a0ce95-ba4c-49d0-ade7-273a6d19f1c9",
      "name": "Limit to 10 Entries",
      "type": "n8n-nodes-base.limit",
      "position": [
        2080,
        3488
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "68ad6247-7deb-48be-b7d9-ee72979cf01d",
      "name": "Send Telegram Message",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1632,
        3488
      ],
      "parameters": {
        "text": "=\ud83d\udcf0 *{{ $json.topic }}*\n\n\u270d\ufe0f {{ $json.tweet }}\n\n{{ $json.hashtags }}\n\n\u23f0 {{ $json.scheduledTime }}\n\ud83c\udd94 `{{ $json.rowId }}`",
        "chatId": "=REPLACE_WITH_YOUR_CHAT_ID",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "\u2705 Approve",
                    "additionalFields": {
                      "callback_data": "={{ 'approve::' + $json.rowId }}"
                    }
                  },
                  {
                    "text": "\u274c Reject",
                    "additionalFields": {
                      "callback_data": "={{ 'reject::' + $json.rowId }}"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "45b79175-4283-402d-9ea4-9b2b04e0af95",
      "name": "AI Groq Interaction",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "position": [
        1024,
        3648
      ],
      "parameters": {
        "model": "qwen/qwen3-32b",
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "62a7666a-2067-4f12-8011-22e418ac7708",
      "name": "Decision: Approve or Reject",
      "type": "n8n-nodes-base.if",
      "position": [
        576,
        4064
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.action }}",
              "value2": "approve",
              "operation": "contains"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9d3e0133-78ec-497c-8cf1-2fcfedf3baab",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -480,
        3360
      ],
      "parameters": {
        "width": 464,
        "height": 704,
        "content": "## AI Tweet Generation & Approval Workflow\n\n### How it works\n\n1. Triggers on a schedule to fetch Google News RSS feed.\n2. Retrieves and filters already used articles from Google Sheets.\n3. Generates tweets based on news articles and schedules them.\n4. Sends the generated tweets as text messages and logs them.\n5. Updates Google Sheets with tweet logs and marks articles as used.\n6. Processes Telegram callbacks for approval and updates Google Sheets based on decisions.\n\n### Setup steps\n\n- [ ] Configure Google Sheets API credentials for logging and data retrieval.\n- [ ] Set up Telegram bot credentials for sending messages.\n- [ ] Configure the schedule for fetching news RSS feeds.\n\n### Customization\n\nAdjust the criteria for tweet generation and approval logic as needed."
      },
      "typeVersion": 1
    },
    {
      "id": "2561d66a-6fcf-46fd-b724-7b8e1a9f3976",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        3856
      ],
      "parameters": {
        "color": 7,
        "width": 1200,
        "height": 512,
        "content": "## Telegram callback handling\n\nTriggers on Telegram callback, processes data, and updates Google Sheets based on approval or rejection."
      },
      "typeVersion": 1
    },
    {
      "id": "8dfa7650-aee9-4e69-bf11-a9bc0585477c",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        3360
      ],
      "parameters": {
        "color": 7,
        "width": 848,
        "height": 304,
        "content": "## News fetching and processing\n\nSchedules and triggers the fetching of news RSS, retrieves used articles, extracts useful data, and prepares it for tweet generation."
      },
      "typeVersion": 1
    },
    {
      "id": "5294b3b8-2679-471b-9e5e-472a1e7d09cd",
      "name": "Sticky Note14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        3328
      ],
      "parameters": {
        "color": 7,
        "width": 352,
        "height": 480,
        "content": "## Tweet generation\n\nGenerates tweets using AI based on processed articles."
      },
      "typeVersion": 1
    },
    {
      "id": "d9cc4679-27a3-488a-ac3c-5431983d0fd5",
      "name": "Sticky Note15",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        3360
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 304,
        "content": "## Tweet scheduling and sending\n\nSchedules the generated tweets, sends them as telegram messages, and logs the details."
      },
      "typeVersion": 1
    },
    {
      "id": "fb030892-fe05-4258-b2a3-4f26bbcd2857",
      "name": "Sticky Note16",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2032,
        3360
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 304,
        "content": "## Article logging and finalization\n\nLogs tweets to Google Sheets, limits the number of logs, and marks articles as used."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Parse Tweet Data": {
      "main": [
        [
          {
            "node": "Send Telegram Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Used Articles": {
      "main": [
        [
          {
            "node": "Process Article Extraction",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Groq Interaction": {
      "ai_languageModel": [
        [
          {
            "node": "AI Tweet Generation Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Limit to 10 Entries": {
      "main": [
        [
          {
            "node": "Log Article as Used",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Tweets in Sheets": {
      "main": [
        [
          {
            "node": "Limit to 10 Entries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Google News RSS": {
      "main": [
        [
          {
            "node": "Read Used Articles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Telegram Message": {
      "main": [
        [
          {
            "node": "Log Tweets in Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Scheduled at 8 AM": {
      "main": [
        [
          {
            "node": "Fetch Google News RSS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Tweet Generation Agent": {
      "main": [
        [
          {
            "node": "Parse Tweet Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Telegram Callback": {
      "main": [
        [
          {
            "node": "Decision: Approve or Reject",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Approval in Sheets": {
      "main": [
        [
          {
            "node": "Send Approval Confirmation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Article Extraction": {
      "main": [
        [
          {
            "node": "AI Tweet Generation Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Rejection in Sheets": {
      "main": [
        [
          {
            "node": "Send Rejection Confirmation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decision: Approve or Reject": {
      "main": [
        [
          {
            "node": "Update Approval in Sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Update Rejection in Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Telegram Callback Triggered": {
      "main": [
        [
          {
            "node": "Extract Telegram Callback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

This n8n template automates the creation and approval of Twitter (X) content using real-time news data, AI-generated text, and a human approval workflow.

Source: https://n8n.io/workflows/14842/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This workflow creates a multi-talented AI assistant named Simran that interacts with users via Telegram. It can handle text and voice messages, understand the user's intent, and perform various tasks.

MongoDB, Chain Llm, Google Gemini Chat +11
AI & RAG

Generate AI viral videos with NanoBanana & VEO3, shared on socials via Blotato 2. Uses @blotato/n8n-nodes-blotato, googleSheets, lmChatOpenAi, toolThink. Event-driven trigger; 94 nodes.

@Blotato/N8N Nodes Blotato, Google Sheets, OpenAI Chat +9
AI & RAG

This project is a template for building a complete academic virtual assistant using n8n. It connects to Telegram, answers frequently asked questions by querying MongoDB, keeps the community informed a

Telegram, MongoDB, Telegram Trigger +6
AI & RAG

This template is designed for marketers, content creators, and e-commerce brands who want to automate the creation of professional ad videos at scale. It’s ideal for teams looking to generate consiste

Telegram, Telegram Trigger, Google Drive +8
AI & RAG

This automation is designed to help you generate AI-powered music tracks, cover art, and fully rendered music videos — all triggered from a simple Telegram chat and managed via Google Sheets.

OpenAI Chat, Memory Buffer Window, Output Parser Structured +11