{
  "name": "JAH · Operations + Back Office · Invoice Chaser",
  "nodes": [
    {
      "parameters": {
        "content": "## JAH Invoice Chaser\nLate invoices get chased for you.\n\nThis runs every day at 9 AM. It finds unpaid invoices. It sends a reminder at 7, 14, and 30 days late. Each reminder gets firmer. You get a Slack digest of what went out.\n\n### 5-minute setup\n1. Open the Config (EDIT ME) node. Add your business name.\n2. Run it once in test mode. It works with zero keys.\n3. Fill the placeholders listed in the credentials note.\n4. Swap the 3 gray end nodes for real email, Sheets, and Slack nodes.\n5. Set testMode to false. Turn the workflow on.",
        "height": 640,
        "width": 480,
        "color": 4
      },
      "id": "s-header",
      "name": "Sticky: Start Here",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -340,
        60
      ]
    },
    {
      "parameters": {
        "content": "## Placeholders to fill\nMost live in the Config (EDIT ME) node.\n- YOUR_BUSINESS_NAME: your company name.\n- YOUR_FROM_EMAIL: the address your reminders send from.\n- YOUR_ACCOUNTING_API_ENDPOINT: in the Fetch Unpaid Invoices node. Point it at your QuickBooks or Xero API.\n- YOUR_GOOGLE_SHEET_ID: the sheet where reminders get logged.\n- YOUR_SLACK_WEBHOOK_URL: where the daily digest posts.\n- OPENAI_API_KEY: set this as an environment variable on your n8n server. The AI Write Reminder node reads it.",
        "height": 420,
        "width": 640,
        "color": 6
      },
      "id": "s-creds",
      "name": "Sticky: Credentials To Fill",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        180,
        820
      ]
    },
    {
      "parameters": {
        "content": "## Step 1: Find and sort invoices\nTest mode builds 4 sample invoices. Live mode pulls from your accounting tool. The Code node sorts each invoice into a bucket: 7, 14, or 30 days late. Invoices under 7 days late get skipped.",
        "height": 180,
        "width": 640,
        "color": 7
      },
      "id": "s-bucket",
      "name": "Sticky: Step 1 Find And Sort",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        840,
        100
      ]
    },
    {
      "parameters": {
        "content": "## Step 2: Write the reminder\nThe Switch sends each invoice down a tone lane. 7 days is friendly. 14 days is firm. 30 days is a final notice. AI writes the email in that tone. Test mode uses a built-in template instead, so you need zero keys.",
        "height": 160,
        "width": 900,
        "color": 7
      },
      "id": "s-tone",
      "name": "Sticky: Step 2 Write The Reminder",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1520,
        40
      ]
    },
    {
      "parameters": {
        "content": "## Test it right now, zero keys\ntestMode is true out of the box. Click Execute Workflow. Or send a POST to the webhook path jah-test-ops-13. You will see 3 mock reminders and 1 digest. No email gets sent. The gray nodes are safe stand-ins.\n\nThe Test Trigger webhook exists only for testing. Delete it when you go live.",
        "height": 420,
        "width": 640,
        "color": 3
      },
      "id": "s-test",
      "name": "Sticky: Test It Right Now",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        880,
        820
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 9,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "id": "schedule-daily",
      "name": "Daily 9am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "jah-test-ops-13",
        "options": {}
      },
      "id": "test-webhook",
      "name": "Test Trigger (deletable)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        220,
        520
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "testMode",
              "value": true,
              "type": "boolean"
            },
            {
              "id": "a2",
              "name": "businessName",
              "value": "YOUR_BUSINESS_NAME",
              "type": "string"
            },
            {
              "id": "a3",
              "name": "fromEmail",
              "value": "YOUR_FROM_EMAIL",
              "type": "string"
            },
            {
              "id": "a4",
              "name": "accountingEndpoint",
              "value": "https://YOUR_ACCOUNTING_API_ENDPOINT/unpaid-invoices",
              "type": "string"
            },
            {
              "id": "a5",
              "name": "sheetId",
              "value": "YOUR_GOOGLE_SHEET_ID",
              "type": "string"
            },
            {
              "id": "a6",
              "name": "slackWebhookUrl",
              "value": "YOUR_SLACK_WEBHOOK_URL",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "config",
      "name": "Config (EDIT ME)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        440,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.testMode }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-test",
      "name": "Test Mode?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        660,
        400
      ]
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "={{ JSON.stringify({ invoices: [ { invoice_number: 'INV-1042', customer: 'Acme Landscaping', email: 'billing@acme.test', amount: 450, due_date: new Date(Date.now() - 8 * 86400000).toISOString().slice(0, 10) }, { invoice_number: 'INV-1036', customer: 'Bluebird Dental', email: 'accounts@bluebird.test', amount: 1200, due_date: new Date(Date.now() - 16 * 86400000).toISOString().slice(0, 10) }, { invoice_number: 'INV-1019', customer: 'Harbor Gym', email: 'owner@harborgym.test', amount: 780, due_date: new Date(Date.now() - 35 * 86400000).toISOString().slice(0, 10) }, { invoice_number: 'INV-1051', customer: 'Fresh Fade Barbers', email: 'hello@freshfade.test', amount: 300, due_date: new Date(Date.now() - 3 * 86400000).toISOString().slice(0, 10) } ] }) }}",
        "options": {}
      },
      "id": "mock-invoices",
      "name": "Mock Unpaid Invoices",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        880,
        280
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $json.accountingEndpoint }}",
        "options": {}
      },
      "id": "fetch-invoices",
      "name": "Fetch Unpaid Invoices (QuickBooks or Xero)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        880,
        520
      ]
    },
    {
      "parameters": {
        "jsCode": "// Sort every unpaid invoice into a reminder bucket: 7, 14, or 30 days late.\nconst cfg = $('Config (EDIT ME)').first().json;\nconst collected = [];\nfor (const item of $input.all()) {\n  const j = item.json || {};\n  if (Array.isArray(j.invoices)) { collected.push(...j.invoices); }\n  else if (Array.isArray(j.QueryResponse?.Invoice)) { collected.push(...j.QueryResponse.Invoice); }\n  else if (Array.isArray(j.Invoices)) { collected.push(...j.Invoices); }\n  else if (j.invoice_number || j.DocNumber || j.InvoiceNumber) { collected.push(j); }\n}\nconst now = Date.now();\nconst tones = { 7: 'friendly', 14: 'firm', 30: 'final notice' };\nconst out = [];\nfor (const inv of collected) {\n  const dueRaw = inv.due_date || inv.DueDate || '';\n  const due = new Date(dueRaw);\n  if (isNaN(due.getTime())) { continue; }\n  const days = Math.floor((now - due.getTime()) / 86400000);\n  let tier = null;\n  if (days >= 30) { tier = 30; } else if (days >= 14) { tier = 14; } else if (days >= 7) { tier = 7; }\n  if (!tier) { continue; }\n  out.push({ json: {\n    invoice_number: (inv.invoice_number || inv.DocNumber || inv.InvoiceNumber || 'UNKNOWN').toString(),\n    customer: (inv.customer || inv.CustomerRef?.name || inv.Contact?.Name || 'Customer').toString(),\n    email: (inv.email || inv.BillEmail?.Address || '').toString(),\n    amount: Number(inv.amount || inv.Balance || inv.AmountDue || 0),\n    due_date: due.toISOString().slice(0, 10),\n    days_overdue: days,\n    tier,\n    tone: tones[tier],\n    business_name: (cfg.businessName || 'YOUR_BUSINESS_NAME').toString()\n  } });\n}\nreturn out;"
      },
      "id": "bucket-age",
      "name": "Bucket Invoices By Age",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        400
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "sw-7",
                    "leftValue": "={{ $json.tier }}",
                    "rightValue": 7,
                    "operator": {
                      "type": "number",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "7 days"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "sw-14",
                    "leftValue": "={{ $json.tier }}",
                    "rightValue": 14,
                    "operator": {
                      "type": "number",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "14 days"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "sw-30",
                    "leftValue": "={{ $json.tier }}",
                    "rightValue": 30,
                    "operator": {
                      "type": "number",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "30 days"
            }
          ]
        },
        "options": {}
      },
      "id": "switch-tier",
      "name": "Reminder Tier",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1320,
        400
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "t1",
              "name": "subject_prefix",
              "value": "Friendly reminder:",
              "type": "string"
            },
            {
              "id": "t2",
              "name": "tone_line",
              "value": "This is a quick nudge in case it slipped by.",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "tone-7",
      "name": "Tone: Friendly (7 days)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1540,
        220
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "t1",
              "name": "subject_prefix",
              "value": "Second reminder:",
              "type": "string"
            },
            {
              "id": "t2",
              "name": "tone_line",
              "value": "Please send payment this week.",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "tone-14",
      "name": "Tone: Firm (14 days)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1540,
        400
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "t1",
              "name": "subject_prefix",
              "value": "Final notice:",
              "type": "string"
            },
            {
              "id": "t2",
              "name": "tone_line",
              "value": "This invoice is 30 or more days past due. Please pay today or reply so I can help.",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "tone-30",
      "name": "Tone: Final Notice (30 days)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1540,
        580
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $('Config (EDIT ME)').first().json.testMode }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-skip-ai",
      "name": "Skip AI? (test mode)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1760,
        400
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "m1",
              "name": "email_subject",
              "value": "={{ $json.subject_prefix }} Invoice {{ $json.invoice_number }} from {{ $json.business_name }}",
              "type": "string"
            },
            {
              "id": "m2",
              "name": "email_body",
              "value": "=Hi {{ $json.customer }}. Invoice {{ $json.invoice_number }} for ${{ $json.amount }} is {{ $json.days_overdue }} days past due. {{ $json.tone_line }} You can reply to this email with any questions. Thank you. (mock reminder, tone: {{ $json.tone }})",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "mock-reminder",
      "name": "Mock Reminder Email",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1980,
        280
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.OPENAI_API_KEY || '' }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-4o-mini', max_tokens: 400, messages: [{ role: 'user', content: `Write a short payment reminder email from ${$json.business_name}. Tone: ${$json.tone}. Rules: plain English, 5th grade reading level, sentences under 15 words, first person, polite, no threats at the friendly and firm tiers, clear ask to pay today at the final tier. Respond with ONLY raw JSON, no markdown fences, shaped exactly: {\"subject\": \"<subject line>\", \"body\": \"<email body>\"}.\\n\\nInvoice number: ${$json.invoice_number}\\nCustomer: ${$json.customer}\\nAmount due: $${$json.amount}\\nDays overdue: ${$json.days_overdue}\\nDue date: ${$json.due_date}` }] }) }}",
        "options": {}
      },
      "id": "ai-reminder",
      "name": "AI Write Reminder",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1980,
        520
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Pull the subject and body out of the AI response.\nconst ai = $json;\nconst raw = ai.choices?.[0]?.message?.content || '{}';\nlet parsed = { subject: '', body: '' };\ntry { parsed = JSON.parse(raw.replace(/```json|```/g, '').trim()); }\ncatch (e) { parsed = { subject: 'Invoice reminder', body: raw.toString() }; }\nconst inv = $('Bucket Invoices By Age').item.json;\nreturn { json: { ...inv, email_subject: (parsed.subject || 'Invoice reminder').toString(), email_body: (parsed.body || '').toString() } };"
      },
      "id": "parse-reminder",
      "name": "Parse AI Reminder",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2200,
        520
      ]
    },
    {
      "parameters": {},
      "id": "send-email",
      "name": "Send Reminder Email (add your email node)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        2420,
        400
      ]
    },
    {
      "parameters": {},
      "id": "log-sheet",
      "name": "Log Reminder to Sheet (add your Google Sheets node)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        2640,
        400
      ]
    },
    {
      "parameters": {
        "jsCode": "// Roll every reminder into one plain-English digest.\nconst items = $input.all();\nconst lines = items.map((i) => {\n  const j = i.json || {};\n  return '- ' + (j.customer || 'Customer') + ': invoice ' + (j.invoice_number || '?') + ' ($' + (j.amount || 0) + ', ' + (j.days_overdue || 0) + ' days late, tone: ' + (j.tone || '?') + ')';\n});\nreturn [{ json: {\n  digest: 'Invoice Chaser sent ' + items.length + ' reminders today.\\n' + lines.join('\\n'),\n  reminders_sent: items.length,\n  generated_at: new Date().toISOString()\n} }];"
      },
      "id": "build-digest",
      "name": "Build Daily Digest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2860,
        400
      ]
    },
    {
      "parameters": {},
      "id": "slack-digest",
      "name": "Slack Daily Digest (add your Slack node)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        3080,
        400
      ]
    }
  ],
  "connections": {
    "Daily 9am": {
      "main": [
        [
          {
            "node": "Config (EDIT ME)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Test Trigger (deletable)": {
      "main": [
        [
          {
            "node": "Config (EDIT ME)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config (EDIT ME)": {
      "main": [
        [
          {
            "node": "Test Mode?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Test Mode?": {
      "main": [
        [
          {
            "node": "Mock Unpaid Invoices",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch Unpaid Invoices (QuickBooks or Xero)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mock Unpaid Invoices": {
      "main": [
        [
          {
            "node": "Bucket Invoices By Age",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Unpaid Invoices (QuickBooks or Xero)": {
      "main": [
        [
          {
            "node": "Bucket Invoices By Age",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bucket Invoices By Age": {
      "main": [
        [
          {
            "node": "Reminder Tier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reminder Tier": {
      "main": [
        [
          {
            "node": "Tone: Friendly (7 days)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tone: Firm (14 days)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tone: Final Notice (30 days)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tone: Friendly (7 days)": {
      "main": [
        [
          {
            "node": "Skip AI? (test mode)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tone: Firm (14 days)": {
      "main": [
        [
          {
            "node": "Skip AI? (test mode)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tone: Final Notice (30 days)": {
      "main": [
        [
          {
            "node": "Skip AI? (test mode)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip AI? (test mode)": {
      "main": [
        [
          {
            "node": "Mock Reminder Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "AI Write Reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mock Reminder Email": {
      "main": [
        [
          {
            "node": "Send Reminder Email (add your email node)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Write Reminder": {
      "main": [
        [
          {
            "node": "Parse AI Reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Reminder": {
      "main": [
        [
          {
            "node": "Send Reminder Email (add your email node)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reminder Email (add your email node)": {
      "main": [
        [
          {
            "node": "Log Reminder to Sheet (add your Google Sheets node)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Reminder to Sheet (add your Google Sheets node)": {
      "main": [
        [
          {
            "node": "Build Daily Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Daily Digest": {
      "main": [
        [
          {
            "node": "Slack Daily Digest (add your Slack node)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
