{
  "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
          }
        ]
      ]
    }
  }
}