AutomationFlowsAI & RAG › Draft Follow-up Email Reminders in Gmail with Anthropic Claude

Draft Follow-up Email Reminders in Gmail with Anthropic Claude

ByNocode Expert @iamrup on n8n.io

This workflow runs every weekday morning to find Gmail threads where your last sent message has not been replied to, generates short follow-up drafts with Anthropic Claude, saves each as a Gmail draft in the original thread, and emails you a digest of the queued follow-ups. Runs…

Cron / scheduled trigger★★★★☆ complexity16 nodesGmailHTTP Request
AI & RAG Trigger: Cron / scheduled Nodes: 16 Complexity: ★★★★☆ Added:

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

This workflow follows the Gmail → 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 →

Download .json
{
  "id": "0vwBb2ZpCXSkcNvl",
  "name": "Find unanswered emails in Gmail and draft AI follow-ups with Claude",
  "tags": [],
  "nodes": [
    {
      "id": "overview",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1040,
        -208
      ],
      "parameters": {
        "width": 780,
        "height": 816,
        "content": "## Chase the emails that never got a reply\n\nEvery week a few threads go quiet and you forget which ones. This workflow reads your sent mail, works out which threads never came back, writes a follow-up for each, and leaves it as a Gmail draft so nothing sends without you.\n\n### How it works\nOn a schedule it pulls the messages you sent in your look-back window and the messages you received in the same window, then matches them by thread. A thread counts as unanswered when the newest message in it is still yours, so replies that arrived before your last message do not fool it. Automated senders are filtered out by keyword, and threads that are too fresh or too stale are dropped by your follow-up window. One AI call then writes a short, specific nudge per thread and rates how promising each one looks. Each nudge is saved as a Gmail draft on the original thread, and you get one digest email listing everything, worst first.\n\n### Setup\n- Edit the config node: look-back days, the minimum and maximum days of silence, excluded sender keywords, your name, tone and the digest recipient.\n- Add credentials in the canvas: Gmail on all four Gmail nodes, and Anthropic on the drafting node.\n- Run it once manually before activating the schedule, then read the drafts.\n\n### Customization\nSend the drafts automatically instead of saving them, post the digest to Slack, log every chased thread to Google Sheets, or narrow the Gmail queries to one label so it only chases sales or invoices.\n\nBuilt by **nocode.expert**, done-for-you automation and AI workflows. https://nocode.expert/resources/automated-follow-up-emails"
      },
      "typeVersion": 1
    },
    {
      "id": "section-read-both-sides",
      "name": "Section: Read both sides",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 176,
        "content": "## 1. Read both sides of the mailbox\nPull what you sent and what came back over the same window, so the comparison is like for like."
      },
      "typeVersion": 1
    },
    {
      "id": "section-find-the-silence",
      "name": "Section: Find the silence",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        416,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 512,
        "height": 176,
        "content": "## 2. Find the silence\nMatch by thread and keep only threads where the newest message is still yours. Your window drops threads that are too fresh or too old to be worth chasing."
      },
      "typeVersion": 1
    },
    {
      "id": "section-draft-the-nudge",
      "name": "Section: Draft the nudge",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        992,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 380,
        "height": 176,
        "content": "## 3. Draft the nudge\nOne AI call writes a short follow-up per thread and rates how promising it looks."
      },
      "typeVersion": 1
    },
    {
      "id": "section-save-and-report",
      "name": "Section: Save and report",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1424,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 476,
        "height": 176,
        "content": "## 4. Save and report\nEach nudge becomes a Gmail draft on the original thread. Nothing sends on its own. You get one digest, worst first."
      },
      "typeVersion": 1
    },
    {
      "id": "every-weekday-morning",
      "name": "Every weekday morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -160,
        480
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "set-config-window-tone-and-recipients",
      "name": "Set config: window, tone and recipients",
      "type": "n8n-nodes-base.set",
      "position": [
        112,
        480
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "lookbackDays",
              "type": "number",
              "value": 30
            },
            {
              "id": "a2",
              "name": "minDaysSilent",
              "type": "number",
              "value": 3
            },
            {
              "id": "a3",
              "name": "maxDaysSilent",
              "type": "number",
              "value": 21
            },
            {
              "id": "a4",
              "name": "excludeTerms",
              "type": "string",
              "value": "no-reply, noreply, no_reply, notifications, mailer-daemon, calendar-notification, donotreply"
            },
            {
              "id": "a5",
              "name": "tone",
              "type": "string",
              "value": "warm, brief and low-pressure"
            },
            {
              "id": "a6",
              "name": "yourName",
              "type": "string",
              "value": "YOUR NAME"
            },
            {
              "id": "a7",
              "name": "digestTo",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "a8",
              "name": "maxThreads",
              "type": "number",
              "value": 25
            },
            {
              "id": "a9",
              "name": "model",
              "type": "string",
              "value": "claude-haiku-4-5"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "fetch-the-messages-you-sent",
      "name": "Fetch the messages you sent",
      "type": "n8n-nodes-base.gmail",
      "position": [
        368,
        480
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "q": "={{ 'in:sent newer_than:' + $('Set config: window, tone and recipients').first().json.lookbackDays + 'd' }}"
        },
        "options": {},
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 2.1
    },
    {
      "id": "fetch-the-replies-you-received",
      "name": "Fetch the replies you received",
      "type": "n8n-nodes-base.gmail",
      "position": [
        608,
        480
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "q": "={{ '-in:sent newer_than:' + $('Set config: window, tone and recipients').first().json.lookbackDays + 'd' }}"
        },
        "options": {},
        "operation": "getAll",
        "returnAll": true
      },
      "executeOnce": true,
      "typeVersion": 2.1
    },
    {
      "id": "find-threads-with-no-reply",
      "name": "Find threads with no reply",
      "type": "n8n-nodes-base.code",
      "position": [
        800,
        480
      ],
      "parameters": {
        "jsCode": "// Compare what you sent against what came back, per thread. No API calls here.\nconst cfg = $('Set config: window, tone and recipients').first().json;\nconst sent = $('Fetch the messages you sent').all().map((it) => it.json);\nconst received = $input.all().map((it) => it.json);\n\nconst header = (m, name) => {\n  const hs = (m.payload && m.payload.headers) || m.headers || [];\n  const hit = Array.isArray(hs) ? hs.find((h) => String(h.name || '').toLowerCase() === name) : null;\n  return hit ? String(hit.value || '') : '';\n};\nconst stamp = (m) => {\n  const raw = m.internalDate || m.internalDateMs || '';\n  const n = Number(raw);\n  if (Number.isFinite(n) && n > 0) return n;\n  const d = Date.parse(header(m, 'date') || m.date || m.Date || '');\n  return Number.isFinite(d) ? d : 0;\n};\nconst addressOf = (v) => {\n  const s = String(v || '');\n  const m = s.match(/<([^>]+)>/);\n  return (m ? m[1] : s).trim().toLowerCase();\n};\nconst clean = (s) => String(s || '').replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/g, ' ').replace(/\\s+/g, ' ').trim();\n\nconst blocked = String(cfg.excludeTerms || '')\n  .split(',').map((t) => t.trim().toLowerCase()).filter(Boolean);\n\n// Latest outbound message per thread.\nconst lastSent = new Map();\nfor (const m of sent) {\n  const thread = m.threadId || m.id;\n  if (!thread) continue;\n  const ts = stamp(m);\n  const prev = lastSent.get(thread);\n  if (prev && prev.ts >= ts) continue;\n  lastSent.set(thread, {\n    ts,\n    to: addressOf(header(m, 'to') || m.to || m.To),\n    subject: header(m, 'subject') || m.subject || m.Subject || '(no subject)',\n    snippet: clean(m.snippet || m.text || m.textPlain || '').slice(0, 400),\n  });\n}\n\n// Latest inbound message per thread.\nconst lastReply = new Map();\nfor (const m of received) {\n  const thread = m.threadId || m.id;\n  if (!thread) continue;\n  const ts = stamp(m);\n  if (!lastReply.has(thread) || lastReply.get(thread) < ts) lastReply.set(thread, ts);\n}\n\nconst now = Date.now();\nconst out = [];\nfor (const [thread, s] of lastSent.entries()) {\n  if (!s.to) continue;\n  if (blocked.some((t) => s.to.includes(t))) continue;\n  const replied = lastReply.get(thread) || 0;\n  if (replied > s.ts) continue; // they answered after your last message\n  const daysSilent = Math.floor((now - s.ts) / 86400000);\n  out.push({ json: {\n    threadId: thread,\n    to: s.to,\n    subject: s.subject,\n    snippet: s.snippet,\n    daysSilent,\n    lastSentAt: new Date(s.ts).toISOString().slice(0, 10),\n  } });\n}\n\nout.sort((a, b) => b.json.daysSilent - a.json.daysSilent);\nreturn out.slice(0, Number(cfg.maxThreads) || 25);"
      },
      "typeVersion": 2
    },
    {
      "id": "keep-threads-past-your-follow-up-window",
      "name": "Keep threads past your follow-up window",
      "type": "n8n-nodes-base.filter",
      "position": [
        992,
        480
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f1",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.daysSilent }}",
              "rightValue": "={{ $('Set config: window, tone and recipients').first().json.minDaysSilent }}"
            },
            {
              "id": "f2",
              "operator": {
                "type": "number",
                "operation": "lte"
              },
              "leftValue": "={{ $json.daysSilent }}",
              "rightValue": "={{ $('Set config: window, tone and recipients').first().json.maxDaysSilent }}"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "build-the-follow-up-request",
      "name": "Build the follow-up request",
      "type": "n8n-nodes-base.code",
      "position": [
        1168,
        480
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: window, tone and recipients').first().json;\nconst threads = $input.all().map((it) => it.json);\nconst schema = { type: 'object', additionalProperties: false, required: ['followups'], properties: {\n  followups: { type: 'array', items: { type: 'object', additionalProperties: false,\n    required: ['i', 'priority', 'reason', 'body'], properties: {\n      i: { type: 'integer' },\n      priority: { type: 'string', enum: ['high', 'medium', 'low'] },\n      reason: { type: 'string' },\n      body: { type: 'string' } } } } } };\n\nconst system = 'You write short follow-up emails for messages that never got a reply. Tone: ' + cfg.tone +\n  '. Rules: never guess facts that are not in the original message, never apologise for following up, ' +\n  'never use the phrase \"just checking in\", keep each body under 70 words, make the ask concrete, ' +\n  'and sign off as ' + cfg.yourName + '. Set priority from how much the thread looks like it was going somewhere, ' +\n  'not from how old it is. Reason is one short line explaining the priority, for the sender only. ' +\n  'Do not use em dashes.';\n\nconst user = 'UNANSWERED THREADS (reference each by index i):\\n' +\n  threads.map((t, i) => '[' + i + '] To: ' + t.to + ' | Subject: ' + t.subject +\n    ' | Silent for ' + t.daysSilent + ' days since ' + t.lastSentAt + '\\nWhat you last sent: ' + t.snippet).join('\\n\\n');\n\nreturn [{ json: { body: {\n  model: cfg.model, max_tokens: 3000, system,\n  messages: [{ role: 'user', content: user }],\n  output_config: { format: { type: 'json_schema', schema } },\n} } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "write-follow-ups-with-claude",
      "name": "Write follow-ups with Claude",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1344,
        480
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.ANTHROPIC_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "build-drafts-and-digest",
      "name": "Build drafts and digest",
      "type": "n8n-nodes-base.code",
      "position": [
        1520,
        480
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: window, tone and recipients').first().json;\nconst threads = $('Keep threads past your follow-up window').all().map((it) => it.json);\nconst blocks = ($json.content || []);\nconst text = (blocks.find((b) => b.type === 'text') || {}).text || '{\"followups\":[]}';\nlet followups = [];\ntry { followups = (JSON.parse(text).followups) || []; } catch (e) {}\n\nconst rank = { high: 0, medium: 1, low: 2 };\nconst rows = followups\n  .map((f) => ({ f, t: threads[f.i] }))\n  .filter((r) => r.t && r.f.body)\n  .sort((a, b) => (rank[a.f.priority] ?? 3) - (rank[b.f.priority] ?? 3) || b.t.daysSilent - a.t.daysSilent);\n\nconst pad = (s, n) => String(s).slice(0, n).padEnd(n);\nconst table = rows.map((r) =>\n  pad(r.f.priority.toUpperCase(), 7) + pad(String(r.t.daysSilent) + 'd', 5) + pad(r.t.to, 30) + r.t.subject\n).join('\\n');\n\nconst digest = rows.length\n  ? rows.length + ' thread' + (rows.length === 1 ? '' : 's') + ' went quiet. A draft reply is waiting in Gmail for each one.\\n\\n' +\n    pad('PRIORITY', 7) + pad('AGE', 5) + pad('WHO', 30) + 'SUBJECT\\n' + table + '\\n\\n' +\n    rows.map((r) => 'To: ' + r.t.to + '\\nRe: ' + r.t.subject + '\\nWhy: ' + r.f.reason + '\\n\\n' + r.f.body).join('\\n\\n----\\n\\n')\n  : 'No unanswered threads in your window. Nothing to chase.';\n\nif (!rows.length) return [{ json: { digest, digestTo: cfg.digestTo, empty: true } }];\n\nreturn rows.map((r) => ({ json: {\n  threadId: r.t.threadId,\n  to: r.t.to,\n  subject: r.t.subject.toLowerCase().startsWith('re:') ? r.t.subject : 'Re: ' + r.t.subject,\n  body: r.f.body,\n  priority: r.f.priority,\n  reason: r.f.reason,\n  daysSilent: r.t.daysSilent,\n  digest,\n  digestTo: cfg.digestTo,\n  empty: false,\n} }));"
      },
      "typeVersion": 2
    },
    {
      "id": "save-a-draft-reply-on-each-thread",
      "name": "Save a draft reply on each thread",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1920,
        384
      ],
      "parameters": {
        "message": "={{ $json.body }}",
        "options": {
          "sendTo": "={{ $json.to }}",
          "threadId": "={{ $json.threadId }}"
        },
        "subject": "={{ $json.subject }}",
        "resource": "draft"
      },
      "typeVersion": 2.1
    },
    {
      "id": "email-you-the-chase-list",
      "name": "Email you the chase list",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1920,
        608
      ],
      "parameters": {
        "sendTo": "={{ $json.digestTo }}",
        "message": "={{ $json.digest }}",
        "options": {},
        "subject": "Follow-ups waiting in your drafts",
        "emailType": "text"
      },
      "executeOnce": true,
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "d1291e60-c6dd-43be-9290-45970f48eb64",
  "nodeGroups": [],
  "connections": {
    "Every weekday morning": {
      "main": [
        [
          {
            "node": "Set config: window, tone and recipients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build drafts and digest": {
      "main": [
        [
          {
            "node": "Save a draft reply on each thread",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email you the chase list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find threads with no reply": {
      "main": [
        [
          {
            "node": "Keep threads past your follow-up window",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build the follow-up request": {
      "main": [
        [
          {
            "node": "Write follow-ups with Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch the messages you sent": {
      "main": [
        [
          {
            "node": "Fetch the replies you received",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write follow-ups with Claude": {
      "main": [
        [
          {
            "node": "Build drafts and digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch the replies you received": {
      "main": [
        [
          {
            "node": "Find threads with no reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep threads past your follow-up window": {
      "main": [
        [
          {
            "node": "Build the follow-up request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set config: window, tone and recipients": {
      "main": [
        [
          {
            "node": "Fetch the messages you sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

This workflow runs every weekday morning to find Gmail threads where your last sent message has not been replied to, generates short follow-up drafts with Anthropic Claude, saves each as a Gmail draft in the original thread, and emails you a digest of the queued follow-ups. Runs…

Source: https://n8n.io/workflows/17778/ — 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 runs twice daily to fetch recent Instagram post comments via the Instagram Graph API, uses OpenAI to classify sentiment, saves new positive comments to Google Sheets, and emails an HTML

HTTP Request, Google Sheets, Gmail
AI & RAG

This workflow runs every 6 hours to read new NPS responses from Google Sheets, uses OpenAI to draft a personalized reply, sends it via Gmail, routes respondents by NPS segment, logs promoter quotes ba

Google Sheets, HTTP Request, Gmail
AI & RAG

This workflow runs daily to pull job applicants from Google Sheets, score each candidate against role requirements using the Google Gemini API, log results back to Google Sheets, and notify a Slack ch

Google Sheets, HTTP Request, Slack +1
AI & RAG

Founder's Discovery Engine. Uses googleSheets, googleDrive, httpRequest, gmail. Scheduled trigger; 18 nodes.

Google Sheets, Google Drive, HTTP Request +1
AI & RAG

This workflow runs every Monday, pulls stories from your chosen RSS feeds, uses OpenAI Chat Completions to draft a grouped HTML newsletter, and saves it as a draft broadcast in Kit (ConvertKit) or ema

RSS Feed Read, HTTP Request, Gmail