{
  "name": "DEMO — Client document chaser (PauSolutions)",
  "nodes": [
    {
      "parameters": {
        "content": "## Client document chaser — DEMO\nFictional accounting firm and clients, no credentials.\n\n1. Replace **Client list (demo data)** with your Google Sheets/Excel node.\n2. Connect your email account in **Send request / reminder** and enable it.\n3. Rules: first request, reminder after 5 days, second reminder after 5 more, then **call the client** (no more emails).\n\nInteractive version: pausolutions.com/demos/document-chaser/\n\n_All dates in this demo are examples (sample \"today\": 21 Oct 2026). In production the workflow uses the real current date._",
        "height": 330,
        "width": 460
      },
      "id": "1286c532-bc20-4980-acb1-bc8b28a939be",
      "name": "About this demo",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -80,
        -340
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "id": "f8aca091-8466-4000-a69b-d928f82ffd27",
      "name": "Every weekday at 9:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {},
      "id": "bf68561d-0e12-4000-a15f-2fa48490663b",
      "name": "Test run",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// DEMO DATA — fictional clients. In production, replace this node with a\n// Google Sheets / Excel node that reads your client list.\nconst clients = [\n  {\n    \"id\": \"c1\",\n    \"client\": \"Oakridge Furniture Ltd (demo)\",\n    \"email\": \"accounts@oakridge.example\",\n    \"requested\": [\n      \"Bank statement\",\n      \"Purchase invoices\",\n      \"Fuel receipts\"\n    ],\n    \"received\": [\n      \"Purchase invoices\"\n    ],\n    \"reminders\": 1,\n    \"lastReminder\": \"2026-10-14\"\n  },\n  {\n    \"id\": \"c2\",\n    \"client\": \"Bluebell Design (demo)\",\n    \"email\": \"hello@bluebell.example\",\n    \"requested\": [\n      \"Bank statement\",\n      \"Sales invoices\"\n    ],\n    \"received\": [\n      \"Bank statement\",\n      \"Sales invoices\"\n    ],\n    \"reminders\": 0,\n    \"lastReminder\": \"\"\n  },\n  {\n    \"id\": \"c3\",\n    \"client\": \"Green Leaf Café (demo)\",\n    \"email\": \"owner@greenleaf.example\",\n    \"requested\": [\n      \"Bank statement\",\n      \"Purchase invoices\",\n      \"Payroll summary\"\n    ],\n    \"received\": [],\n    \"reminders\": 0,\n    \"lastReminder\": \"\"\n  },\n  {\n    \"id\": \"c4\",\n    \"client\": \"Hartley & Co (demo)\",\n    \"email\": \"admin@hartley.example\",\n    \"requested\": [\n      \"Bank statement\"\n    ],\n    \"received\": [],\n    \"reminders\": 3,\n    \"lastReminder\": \"2026-10-16\"\n  },\n  {\n    \"id\": \"c5\",\n    \"client\": \"North Bikes (demo)\",\n    \"email\": \"info@northbikes.example\",\n    \"requested\": [\n      \"Bank statement\",\n      \"Sales report\"\n    ],\n    \"received\": [\n      \"Sales report\"\n    ],\n    \"reminders\": 2,\n    \"lastReminder\": \"2026-10-12\"\n  },\n  {\n    \"id\": \"c6\",\n    \"client\": \"Maple Physio (demo)\",\n    \"email\": \"frontdesk@maplephysio.example\",\n    \"requested\": [\n      \"Bank statement\",\n      \"Card terminal report\"\n    ],\n    \"received\": [\n      \"Card terminal report\"\n    ],\n    \"reminders\": 1,\n    \"lastReminder\": \"2026-10-19\"\n  }\n];\nreturn clients.map((c) => ({ json: c }));"
      },
      "id": "5aaf827d-d6e1-4e00-a498-fbc4ce3e2edb",
      "name": "Client list (demo data)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "// Same logic as the interactive demo on pausolutions.com/demos/ (rules + templates, no AI).\nconst TEXT = {\n  \"locale\": \"en-GB\",\n  \"firm\": \"Harbour Lane Accounting (demo)\",\n  \"period\": \"September 2026\",\n  \"subjects\": [\n    \"Documents needed for {period}\",\n    \"Reminder: documents for {period}\",\n    \"Second reminder: documents for {period} needed by {deadline}\"\n  ],\n  \"bodies\": [\n    \"Dear {client},\\n\\nto prepare your {period} accounts we still need:\\n{list}\\n\\nYou can simply reply to this email with the files attached. Thank you!\\n\\n{firm}\",\n    \"Dear {client},\\n\\na quick reminder: we're still waiting for these documents for {period}:\\n{list}\\n\\nCould you send them this week? Just reply with the files attached.\\n\\n{firm}\",\n    \"Dear {client},\\n\\nwe still haven't received these documents for {period}:\\n{list}\\n\\nWe need them by {deadline} to complete your accounts on time. If anything is unclear, reply to this email and we'll help.\\n\\n{firm}\"\n  ],\n  \"actions\": {\n    \"first\": \"first request\",\n    \"reminder\": \"reminder\",\n    \"last\": \"second reminder\",\n    \"call\": \"call\",\n    \"waiting\": \"waiting\",\n    \"complete\": \"complete\"\n  },\n  \"callReason\": \"No reply after 3 emails: call the client\",\n  \"summarySubject\": \"Document status for the office\"\n};\nconst RULES = {\n  \"everyDays\": 5,\n  \"maxReminders\": 3\n};\nconst today = \"2026-10-21\"; // Demo: fixed example date, matching the sample data. In production use: new Date().toISOString().slice(0, 10)\n\nfunction fill(tpl, vars) {\n  return tpl.replace(/\\{(\\w+)\\}/g, (m, k) => (k in vars ? String(vars[k]) : m));\n}\n\nfunction daysBetween(fromIso, toIso) {\n  return Math.round((Date.parse(toIso + \"T00:00:00Z\") - Date.parse(fromIso + \"T00:00:00Z\")) / 86400000);\n}\n\nfunction addDays(iso, days) {\n  return new Date(Date.parse(iso + \"T00:00:00Z\") + days * 86400000).toISOString().slice(0, 10);\n}\n\nfunction longDate(iso, locale) {\n  return new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"long\", year: \"numeric\", timeZone: \"UTC\" }).format(new Date(iso + \"T00:00:00Z\"));\n}\n\nfunction money(amount, locale) {\n  return new Intl.NumberFormat(locale, { style: \"currency\", currency: \"EUR\" }).format(amount);\n}\n\nfunction cleanText(value, max, multiline) {\n  const s = String(value ?? \"\").replace(multiline ? /[\\u0000-\\u0009\\u000B-\\u001F\\u007F]/g : /[\\u0000-\\u001F\\u007F]/g, \" \");\n  const collapsed = multiline ? s.replace(/[ \\t]+/g, \" \").replace(/\\n{3,}/g, \"\\n\\n\") : s.replace(/\\s+/g, \" \");\n  return collapsed.trim().slice(0, max);\n}\n\nfunction hasTerm(text, term, whole) {\n  if (!/^[\\p{L}\\p{N}]/u.test(term)) return text.includes(term);\n  const escaped = term.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n  return new RegExp(\"(^|[^\\\\p{L}\\\\p{N}])\" + escaped + (whole ? \"(?![\\\\p{L}\\\\p{N}])\" : \"\"), \"u\").test(text);\n}\n\nfunction planChaser(clients, today, text, rules) {\n  return clients.map((c) => {\n    const missing = c.requested.filter((d) => !c.received.includes(d));\n    const base = { id: c.id, client: c.client, email: c.email, missing, remindersBefore: c.reminders, remindersAfter: c.reminders, stage: null, nextReminder: null, subject: null, body: null, reason: null };\n    if (missing.length === 0) return { ...base, status: \"complete\", action: text.actions.complete };\n    if (c.reminders >= rules.maxReminders) return { ...base, status: \"call\", action: text.actions.call, reason: text.callReason };\n    const since = c.lastReminder ? daysBetween(c.lastReminder, today) : Infinity;\n    if (since < rules.everyDays) return { ...base, status: \"waiting\", action: text.actions.waiting, nextReminder: addDays(c.lastReminder, rules.everyDays) };\n    const stage = Math.min(c.reminders, 2);\n    const vars = { client: c.client, period: text.period, firm: text.firm, list: missing.map((d) => \"- \" + d).join(\"\\n\"), deadline: longDate(addDays(today, rules.everyDays), text.locale) };\n    const action = stage === 0 ? text.actions.first : stage === 1 ? text.actions.reminder : text.actions.last;\n    return { ...base, status: \"email\", action, stage, remindersAfter: c.reminders + 1, subject: fill(text.subjects[stage], vars), body: fill(text.bodies[stage], vars) };\n  });\n}\n\nreturn planChaser($input.all().map((i) => i.json), today, TEXT, RULES).map((o) => ({ json: o }));"
      },
      "id": "16d0e552-6370-4f80-a8e7-9cebd469e09e",
      "name": "Work out what to send",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ad6c0e20-c342-4400-a35b-7799f2eec8e0",
              "leftValue": "={{ $json.status }}",
              "rightValue": "email",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "582cc6a3-67dd-4200-a61b-bf1a94374705",
      "name": "Only clients to email",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        740,
        0
      ],
      "notes": "Clients flagged for a phone call are not emailed.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "fromEmail": "office@harbourlane.example",
        "toEmail": "={{ $json.email }}",
        "subject": "={{ $json.subject }}",
        "emailFormat": "text",
        "text": "={{ $json.body }}",
        "options": {}
      },
      "id": "574caeec-d766-4e00-a97b-d755156f3cb4",
      "name": "Send request / reminder",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        980,
        0
      ],
      "disabled": true,
      "notes": "Disabled in the demo. Connect your own SMTP/Gmail/Outlook account, then enable.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Same logic as the interactive demo on pausolutions.com/demos/ (rules + templates, no AI).\nconst TEXT = {\n  \"locale\": \"en-GB\",\n  \"firm\": \"Harbour Lane Accounting (demo)\",\n  \"period\": \"September 2026\",\n  \"subjects\": [\n    \"Documents needed for {period}\",\n    \"Reminder: documents for {period}\",\n    \"Second reminder: documents for {period} needed by {deadline}\"\n  ],\n  \"bodies\": [\n    \"Dear {client},\\n\\nto prepare your {period} accounts we still need:\\n{list}\\n\\nYou can simply reply to this email with the files attached. Thank you!\\n\\n{firm}\",\n    \"Dear {client},\\n\\na quick reminder: we're still waiting for these documents for {period}:\\n{list}\\n\\nCould you send them this week? Just reply with the files attached.\\n\\n{firm}\",\n    \"Dear {client},\\n\\nwe still haven't received these documents for {period}:\\n{list}\\n\\nWe need them by {deadline} to complete your accounts on time. If anything is unclear, reply to this email and we'll help.\\n\\n{firm}\"\n  ],\n  \"actions\": {\n    \"first\": \"first request\",\n    \"reminder\": \"reminder\",\n    \"last\": \"second reminder\",\n    \"call\": \"call\",\n    \"waiting\": \"waiting\",\n    \"complete\": \"complete\"\n  },\n  \"callReason\": \"No reply after 3 emails: call the client\",\n  \"summarySubject\": \"Document status for the office\"\n};\n\nfunction chaserSummary(outcomes, text) {\n  const open = outcomes.filter((o) => o.status !== \"complete\");\n  return {\n    subject: text.summarySubject,\n    emailsSent: outcomes.filter((o) => o.status === \"email\").length,\n    toCall: outcomes.filter((o) => o.status === \"call\").map((o) => o.client),\n    waiting: outcomes.filter((o) => o.status === \"waiting\").length,\n    complete: outcomes.length - open.length,\n    lines: open.map((o) => o.client + \": \" + o.missing.join(\", \") + \" → \" + o.action).join(\"\\n\"),\n  };\n}\n\nreturn [{ json: chaserSummary($input.all().map((i) => i.json), TEXT) }];"
      },
      "id": "544c708a-c3b1-4a00-aa7b-093349e159a6",
      "name": "Status summary for the office",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        240
      ]
    }
  ],
  "pinData": {},
  "connections": {
    "Every weekday at 9:00": {
      "main": [
        [
          {
            "node": "Client list (demo data)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Test run": {
      "main": [
        [
          {
            "node": "Client list (demo data)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Client list (demo data)": {
      "main": [
        [
          {
            "node": "Work out what to send",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Work out what to send": {
      "main": [
        [
          {
            "node": "Only clients to email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Status summary for the office",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Only clients to email": {
      "main": [
        [
          {
            "node": "Send request / reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "b888e9c8-f721-4c00-a6bf-907125b3a9f8",
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": [],
  "_demo": {
    "by": "PauSolutions",
    "note": "DEMO with fictional data. No credentials included. Email nodes are disabled until you connect your own account.",
    "tags": [
      "demo",
      "accounting"
    ]
  }
}
