AutomationFlowsEmail & Gmail › Proj2 Newsletter

Proj2 Newsletter

Proj2 Newsletter. Uses googleSheets, httpRequest, chainLlm, lmChatGroq. Event-driven trigger; 28 nodes.

Event trigger★★★★☆ complexityAI-powered28 nodesGoogle SheetsHTTP RequestChain LlmGroq ChatGmail
Email & Gmail Trigger: Event Nodes: 28 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow follows the Chainllm → Gmail recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

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

Download .json
{
  "name": "Proj2 Newsletter",
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "id": "manual_trigger",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -440,
        320
      ],
      "parameters": {}
    },
    {
      "id": "schedule_trigger",
      "name": "Schedule Trigger 24h",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -440,
        540
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 8
            }
          ]
        }
      }
    },
    {
      "id": "config",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -220,
        320
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a_recipient",
              "name": "recipientEmail",
              "type": "string",
              "value": "YOUR_RECIPIENT_EMAIL"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "get_log",
      "name": "Get Log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        0,
        320
      ],
      "alwaysOutputData": true,
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultName": "Proj2_Claude"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "802787579",
          "cachedResultName": "Log"
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "get_targets",
      "name": "Get Targets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        220,
        320
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultName": "Proj2_Claude"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "0",
          "cachedResultName": "Targets"
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "build_rss_url",
      "name": "Build RSS URL",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        320
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const companyName = $json['Company Name'] || '';\nconst anchor = ($json['Anchor'] || companyName).toString();\nconst sector = $json['Sector'] || '';\nconst website = $json['Website URL'] || '';\nconst query = encodeURIComponent(anchor + ' when:2d');\nconst rssUrl = 'https://news.google.com/rss/search?q=' + query + '&hl=en-US&gl=US&ceid=US:en';\nreturn { json: { companyName, anchor, sector, website, rssUrl } };"
      }
    },
    {
      "id": "fetch_news",
      "name": "Fetch News",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        320
      ],
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "parameters": {
        "url": "={{ $json.rssUrl }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; n8n-newsletter/1.0)"
            }
          ]
        },
        "options": {
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 1500
            }
          },
          "response": {
            "response": {
              "responseFormat": "text"
            }
          },
          "timeout": 20000
        }
      }
    },
    {
      "id": "parse_articles",
      "name": "Parse Articles",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        320
      ],
      "alwaysOutputData": true,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const targets = $('Get Targets').all();\nconst responses = $input.all();\nconst cutoff = Date.now() - (48 * 60 * 60 * 1000);\nconst out = [];\nfor (let i = 0; i < responses.length; i++) {\n  const t = (targets[i] && targets[i].json) ? targets[i].json : {};\n  const companyName = t['Company Name'] || '';\n  const anchor = t['Anchor'] || '';\n  const sector = t['Sector'] || '';\n  const xml = (responses[i].json.data || '').toString();\n  const blocks = xml.match(new RegExp('<item>[^]*?</item>', 'g')) || [];\n  const getTag = (block, tag) => {\n    const m = block.match(new RegExp('<' + tag + '>([^]*?)</' + tag + '>'));\n    if (!m) return '';\n    let v = m[1];\n    v = v.split('<![CDATA[').join('').split(']]>').join('');\n    return v.trim();\n  };\n  let count = 0;\n  for (const block of blocks) {\n    if (count >= 6) break;\n    const title = getTag(block, 'title');\n    const link = getTag(block, 'link');\n    const pubDate = getTag(block, 'pubDate');\n    let description = getTag(block, 'description');\n    description = description.replace(new RegExp('<[^>]+>', 'g'), ' ').replace(new RegExp('  +', 'g'), ' ').trim().substring(0, 800);\n    const pubMs = pubDate ? new Date(pubDate).getTime() : 0;\n    if (pubMs && pubMs < cutoff) continue;\n    if (!title || !link) continue;\n    out.push({ json: { companyName, anchor, sector, title, url: link, pubDate, description } });\n    count++;\n  }\n}\nreturn out;"
      }
    },
    {
      "id": "relevance_filter",
      "name": "Relevance Pre-filter",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        320
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const companyName = ($json.companyName || '').toString();\nconst anchor = ($json.anchor || '').toString();\nconst sector = ($json.sector || '').toString();\nconst title = ($json.title || '').toString();\nconst url = ($json.url || '').toString();\nconst pubDate = ($json.pubDate || '').toString();\nconst description = ($json.description || '').toString();\nconst haystack = (title + ' ' + description.substring(0, 500)).toLowerCase();\nconst aliases = [];\nif (companyName) aliases.push(companyName.toLowerCase());\nif (anchor) aliases.push(anchor.toLowerCase());\ncompanyName.split(' ').forEach(tok => { if (tok.length >= 4) aliases.push(tok.toLowerCase()); });\nlet isRelevant = false;\nfor (const a of aliases) { if (a && haystack.indexOf(a) !== -1) { isRelevant = true; break; } }\nreturn { json: { companyName, anchor, sector, title, url, pubDate, description, isRelevant } };"
      }
    },
    {
      "id": "filter_dedup",
      "name": "Filter & Dedup",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        320
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const logUrls = new Set($('Get Log').all().map(item => (item.json['Signal URL'] || '').toString().trim()).filter(u => u.length > 0));\nconst survivors = [];\nfor (const input of $input.all()) {\n  const j = input.json || {};\n  const url = (j.url || '').toString().trim();\n  if (!url) continue;\n  if (!j.isRelevant) continue;\n  if (logUrls.has(url)) continue;\n  survivors.push({ json: j });\n}\nif (survivors.length === 0) {\n  return [{ json: { sentinel: true, url: '' } }];\n}\nreturn survivors;"
      }
    },
    {
      "id": "wait_3s",
      "name": "Wait 3s",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1540,
        320
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 3,
        "unit": "seconds"
      }
    },
    {
      "id": "classify",
      "name": "Classify",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.4,
      "position": [
        1760,
        320
      ],
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 5,
      "waitBetweenTries": 45000,
      "parameters": {
        "promptType": "define",
        "text": "=Company: {{ $json.companyName }}\nSector: {{ $json.sector }}\nTitle: {{ $json.title }}\nDescription: {{ $json.description }}\n\nClassify this article into EXACTLY ONE category from this list (use the exact label): Product Launch, Partnership, Funding, Leadership Change, Research Publication, Hiring Signal, Regulatory/Legal, Other.\n\nExtract any monetary amount central to the story (funding raised, acquisition or deal value). Use suffix notation like $30B, $500M, $2.5M. If there is no monetary amount, use N/A.\n\nWrite a factual 1-2 sentence summary of the development.\n\nReturn ONLY raw JSON with no markdown fences, in exactly this shape:\n{\"category\": \"<one of the categories above>\", \"funding\": \"<amount or N/A>\", \"summary\": \"<1-2 sentence summary>\"}\n\nOutput ONLY the requested content. Begin directly with the first line of output. Do not include any introductory text, preamble, or closing remarks."
      }
    },
    {
      "id": "groq_classify_model",
      "name": "Groq Classify Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "typeVersion": 1,
      "position": [
        1760,
        540
      ],
      "parameters": {
        "model": "llama-3.1-8b-instant",
        "options": {}
      },
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "groq_synth_model",
      "name": "Groq Synth Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "typeVersion": 1,
      "position": [
        2860,
        640
      ],
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {}
      },
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "parse_classification",
      "name": "Parse Classification",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        320
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const raw = ($json.text || '').toString().trim();\nconst src = $('Filter & Dedup').item.json || {};\nconst isSentinel = src.sentinel === true || !src.url;\nif (isSentinel) {\n  return { json: { isReal: false, isSentinel: true, include: false, companyName: '', anchor: '', sector: '', title: '', url: '', pubDate: '', description: '', category: '', funding: 'N/A', fundingMillions: null, summary: '', reason: 'No new articles' } };\n}\nconst companyName = (src.companyName || '').toString();\nconst anchor = (src.anchor || '').toString();\nconst sector = (src.sector || '').toString();\nconst title = (src.title || '').toString();\nconst url = (src.url || '').toString();\nconst pubDate = (src.pubDate || '').toString();\nconst description = (src.description || '').toString();\nconst allowed = ['Product Launch','Partnership','Funding','Leadership Change','Research Publication','Hiring Signal','Regulatory/Legal','Other'];\nfunction parseFundingToMillions(str) {\n  if (!str) return null;\n  let s = str.toString().toUpperCase().split(',').join('').split('$').join('').trim();\n  let mult = 1;\n  if (s.indexOf('B') !== -1) mult = 1000;\n  else if (s.indexOf('M') !== -1) mult = 1;\n  else if (s.indexOf('K') !== -1) mult = 0.001;\n  const num = parseFloat(s);\n  if (isNaN(num)) return null;\n  return num * mult;\n}\nfunction grab(text, key) {\n  const marker = '\"' + key + '\"';\n  let i = text.indexOf(marker);\n  if (i === -1) return '';\n  i = text.indexOf(':', i);\n  if (i === -1) return '';\n  const q1 = text.indexOf('\"', i);\n  if (q1 === -1) return '';\n  const q2 = text.indexOf('\"', q1 + 1);\n  if (q2 === -1) return '';\n  return text.substring(q1 + 1, q2);\n}\nlet category = 'Other';\nlet funding = 'N/A';\nlet summary = '';\ntry {\n  const cleaned = raw.split('```json').join('').split('```').join('').trim();\n  const parsed = JSON.parse(cleaned);\n  category = (parsed.category || 'Other').toString().trim();\n  funding = (parsed.funding || 'N/A').toString().trim();\n  summary = (parsed.summary || '').toString().trim();\n} catch (e) {\n  const c = grab(raw, 'category');\n  const f = grab(raw, 'funding');\n  const s = grab(raw, 'summary');\n  if (c) category = c.trim();\n  if (f) funding = f.trim();\n  if (s) summary = s.trim();\n}\nwhile (category.length && category.charAt(category.length - 1) === '.') category = category.slice(0, -1);\ncategory = category.trim();\nlet canon = 'Other';\nfor (const a of allowed) { if (a.toLowerCase() === category.toLowerCase()) { canon = a; break; } }\ncategory = canon;\nconst fundingMillions = parseFundingToMillions(funding);\nconst isMonetary = (category === 'Funding');\nlet include = false;\nif (category !== 'Other') {\n  if (isMonetary) { include = (fundingMillions !== null && fundingMillions >= 100); }\n  else { include = true; }\n}\nlet reason = 'Included';\nif (category === 'Other') reason = 'Excluded: category Other';\nelse if (isMonetary && !(fundingMillions !== null && fundingMillions >= 100)) reason = 'Excluded: below $100M threshold';\nreturn { json: { isReal: true, isSentinel: false, companyName, anchor, sector, title, url, pubDate, description, category, funding, fundingMillions, summary, include, reason } };"
      }
    },
    {
      "id": "if_real",
      "name": "IF Real",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2200,
        200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "cond_real",
              "leftValue": "={{ $json.isReal }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "if_include",
      "name": "IF Include",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2420,
        200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "cond_include",
              "leftValue": "={{ $json.include }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "log_included",
      "name": "Log Included",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        2640,
        100
      ],
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultName": "Proj2_Claude"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "802787579",
          "cachedResultName": "Log"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Company Name": "={{ $json.companyName }}",
            "Signal Title": "={{ $json.title }}",
            "Signal URL": "={{ $json.url }}",
            "Signal Type": "={{ $json.category }}",
            "Summary": "={{ $json.summary }}",
            "PubDate": "={{ $json.pubDate }}",
            "Logged": "={{ $now.toISO() }}",
            "Briefing Included": "Yes",
            "Funding": "={{ $json.funding }}"
          },
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "log_excluded",
      "name": "Log Excluded",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        2640,
        300
      ],
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultName": "Proj2_Claude"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "802787579",
          "cachedResultName": "Log"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Company Name": "={{ $json.companyName }}",
            "Signal Title": "={{ $json.title }}",
            "Signal URL": "={{ $json.url }}",
            "Signal Type": "={{ $json.category }}",
            "Summary": "={{ $json.summary }}",
            "PubDate": "={{ $json.pubDate }}",
            "Logged": "={{ $now.toISO() }}",
            "Briefing Included": "No",
            "Funding": "={{ $json.funding }}"
          },
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "aggregate_included",
      "name": "Aggregate Included",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2200,
        480
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const NL = String.fromCharCode(10);\nconst items = $input.all();\nconst included = [];\nfor (const it of items) {\n  const j = it.json || {};\n  if (j.include === true) included.push(j);\n}\nlet combined = '';\nfor (let i = 0; i < included.length; i++) {\n  const a = included[i];\n  combined += (i + 1) + '. Company: ' + (a.companyName || '') + NL;\n  combined += 'Category: ' + (a.category || '') + NL;\n  if (a.funding && a.funding !== 'N/A') combined += 'Funding/Value: ' + a.funding + NL;\n  combined += 'Headline: ' + (a.title || '') + NL;\n  combined += 'Summary: ' + (a.summary || '') + NL;\n  combined += 'Source: ' + (a.url || '') + NL + NL;\n}\ncombined = combined.substring(0, 6000);\nconst count = included.length;\nconst hasSignals = count > 0;\nreturn [{ json: { count, hasSignals, combined } }];"
      }
    },
    {
      "id": "if_has_signals",
      "name": "IF Has Signals",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2420,
        480
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "cond_has_signals",
              "leftValue": "={{ $json.hasSignals }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "synthesize",
      "name": "Synthesize",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.4,
      "position": [
        2640,
        460
      ],
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 5,
      "waitBetweenTries": 45000,
      "parameters": {
        "promptType": "define",
        "text": "=You are writing a concise daily AI industry newsletter.\n\nSynthesize the following company news signals into a cohesive briefing of NO MORE THAN 5 short paragraphs. Group related developments by theme and lead with the most significant. Do not simply list each item; weave them into flowing prose. Do not invent facts beyond what is provided. Do not use bullet points or headings.\n\nSignals:\n{{ $json.combined }}\n\nOutput ONLY the newsletter body. Begin directly with the first paragraph. Do not include a subject line, greeting, preamble, or closing remarks."
      }
    },
    {
      "id": "sanitize_text",
      "name": "Sanitize Text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2860,
        460
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const NL = String.fromCharCode(10);\nconst BS = String.fromCharCode(92);\nconst PARA = String.fromCharCode(1);\nlet text = ($json.text || '').toString();\ntext = text.split(BS + 'n').join(NL);\ntext = text.split(NL + NL).join(PARA);\ntext = text.split(NL).join(' ');\ntext = text.split(PARA).join(NL + NL);\nwhile (text.indexOf('  ') !== -1) { text = text.split('  ').join(' '); }\ntext = text.trim();\nconst html = text.split(NL + NL).join('<br><br>');\nreturn { json: { text, html } };"
      }
    },
    {
      "id": "gmail_digest",
      "name": "Gmail Digest",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        3080,
        460
      ],
      "parameters": {
        "sendTo": "={{ $('Config').first().json.recipientEmail }}",
        "subject": "=Daily AI Newsletter - {{ $now.format('yyyy-MM-dd') }}",
        "message": "={{ $json.html }}",
        "options": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "gmail_no_news",
      "name": "Gmail No News",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        2640,
        660
      ],
      "parameters": {
        "sendTo": "={{ $('Config').first().json.recipientEmail }}",
        "subject": "=Daily AI Newsletter - {{ $now.format('yyyy-MM-dd') }} (No major signals)",
        "message": "No major AI signals in the last 24 hours met the inclusion criteria.",
        "options": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "find_old_log_rows",
      "name": "Find Old Log Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        560
      ],
      "alwaysOutputData": true,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const rows = $('Get Log').all();\nconst cutoff = Date.now() - (7 * 24 * 60 * 60 * 1000);\nlet deleteCount = 0;\nfor (const r of rows) {\n  const logged = ((r.json || {})['Logged'] || '').toString();\n  const t = logged ? new Date(logged).getTime() : NaN;\n  if (!isNaN(t) && t < cutoff) { deleteCount++; } else { break; }\n}\nconst hasOld = deleteCount > 0;\nreturn [{ json: { deleteCount, hasOld, startIndex: 1 } }];"
      }
    },
    {
      "id": "if_has_old_rows",
      "name": "IF Has Old Rows",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        220,
        560
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "cond_old",
              "leftValue": "={{ $json.hasOld }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "delete_old_log_rows",
      "name": "Delete Old Log Rows",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        440,
        560
      ],
      "parameters": {
        "operation": "delete",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultName": "Proj2_Claude"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "802787579",
          "cachedResultName": "Log"
        },
        "toDelete": "rows",
        "startIndex": "={{ $json.startIndex }}",
        "numberToDelete": "={{ $json.deleteCount }}"
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger 24h": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Get Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Log": {
      "main": [
        [
          {
            "node": "Get Targets",
            "type": "main",
            "index": 0
          },
          {
            "node": "Find Old Log Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Old Log Rows": {
      "main": [
        [
          {
            "node": "IF Has Old Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Has Old Rows": {
      "main": [
        [
          {
            "node": "Delete Old Log Rows",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Get Targets": {
      "main": [
        [
          {
            "node": "Build RSS URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build RSS URL": {
      "main": [
        [
          {
            "node": "Fetch News",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch News": {
      "main": [
        [
          {
            "node": "Parse Articles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Articles": {
      "main": [
        [
          {
            "node": "Relevance Pre-filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Relevance Pre-filter": {
      "main": [
        [
          {
            "node": "Filter & Dedup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter & Dedup": {
      "main": [
        [
          {
            "node": "Wait 3s",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 3s": {
      "main": [
        [
          {
            "node": "Classify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify": {
      "main": [
        [
          {
            "node": "Parse Classification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Classification": {
      "main": [
        [
          {
            "node": "IF Real",
            "type": "main",
            "index": 0
          },
          {
            "node": "Aggregate Included",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Real": {
      "main": [
        [
          {
            "node": "IF Include",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "IF Include": {
      "main": [
        [
          {
            "node": "Log Included",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Excluded",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Included": {
      "main": [
        [
          {
            "node": "IF Has Signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Has Signals": {
      "main": [
        [
          {
            "node": "Synthesize",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail No News",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Synthesize": {
      "main": [
        [
          {
            "node": "Sanitize Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sanitize Text": {
      "main": [
        [
          {
            "node": "Gmail Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq Classify Model": {
      "ai_languageModel": [
        [
          {
            "node": "Classify",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Groq Synth Model": {
      "ai_languageModel": [
        [
          {
            "node": "Synthesize",
            "type": "ai_languageModel",
            "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

Proj2 Newsletter. Uses googleSheets, httpRequest, chainLlm, lmChatGroq. Event-driven trigger; 28 nodes.

Source: https://github.com/MDunn83/AI-Portfolio/blob/main/workflows/P02-newsletter-automation/claude-code-build/P02-newsletter-automation-claude-code.json — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

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

Email & Gmail

This workflow is a high-precision financial intelligence engine that monitors macroeconomic news to identify high-impact trading opportunities. It retrieves real-time data via SerpAPI, processes it th

Google Sheets, HTTP Request, Chain Llm +3
Email & Gmail

This workflow acts as an automated early-warning system for corporate risk. It pulls a list of companies from a Google Sheet, uses SerpAPI to scout the latest global news and employs Groq-powered AI t

Groq Chat, Output Parser Structured, Google Sheets +3
Email & Gmail

This workflow is a comprehensive automation engine that bridges the gap between raw client data and expert-level financial advice. Upon receiving a new onboarding form from Google Sheets, the system f

Chain Llm, Output Parser Structured, Google Sheets +4
Email & Gmail

This template is ideal for HR teams, startup founders, operations leads, remote-first companies, and freelancers managing onboarding manually or across multiple tools.

Google Sheets Trigger, Jira, HubSpot Trigger +7
Email & Gmail

n8n Graphic Design Team. Uses googleSheets, googleDrive, httpRequest, outputParserStructured. Event-driven trigger; 37 nodes.

Google Sheets, Google Drive, HTTP Request +5