{
  "name": "Draft payment reminders for overdue freee sales with Claude, every amount checked",
  "nodes": [
    {
      "id": "c9000000-0000-4000-8000-000000000001",
      "name": "Every weekday 9:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * 1-5"
            }
          ]
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000002",
      "name": "Run now",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        200
      ],
      "parameters": {}
    },
    {
      "id": "c9000000-0000-4000-8000-000000000003",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        100
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": false,
        "assignments": {
          "assignments": [
            {
              "id": "c01",
              "name": "freee_company_id",
              "value": "PASTE_YOUR_FREEE_COMPANY_ID",
              "type": "string"
            },
            {
              "id": "c02",
              "name": "staff_email",
              "value": "staff@example.com",
              "type": "string"
            },
            {
              "id": "c03",
              "name": "sender_signature",
              "value": "Your company name / Accounts",
              "type": "string"
            },
            {
              "id": "c04",
              "name": "min_days_overdue",
              "value": 1,
              "type": "number"
            },
            {
              "id": "c05",
              "name": "max_customers",
              "value": 10,
              "type": "number"
            },
            {
              "id": "c06",
              "name": "timezone",
              "value": "Asia/Tokyo",
              "type": "string"
            },
            {
              "id": "c07",
              "name": "today_override",
              "value": "",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000004",
      "name": "Read unsettled sales",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        100
      ],
      "parameters": {
        "url": "=https://api.freee.co.jp/api/1/deals?company_id={{ $json.freee_company_id }}&type=income&status=unsettled&limit=100",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000005",
      "name": "Read freee partners",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        100
      ],
      "parameters": {
        "url": "=https://api.freee.co.jp/api/1/partners?company_id={{ $('Config').first().json.freee_company_id }}&limit=3000",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000006",
      "name": "Find overdue sales",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        100
      ],
      "parameters": {
        "jsCode": "// Past the due date with an amount still due, grouped by customer. Customers without an e-mail get no draft.\nconst c = $('Config').first().json;\nconst deals = $('Read unsettled sales').first().json;\nconst ps = $json;\nconst summary = { problem: '', drafts: [], no_email: [], no_partner: [], count: 0 };\nif (deals.statusCode !== 200) summary.problem = 'Could not read sales (deals) in freee (HTTP ' + deals.statusCode + '). Check the app permission for deals.';\nelse if (ps.statusCode !== 200) summary.problem = 'Could not read partners in freee (HTTP ' + ps.statusCode + ').';\nconst today = c.today_override ? DateTime.fromISO(c.today_override, { zone: c.timezone }) : DateTime.now().setZone(c.timezone);\nconst cut = today.minus({ days: Number(c.min_days_overdue) - 1 }).toFormat('yyyy-MM-dd');\nconst byId = new Map(((ps.body && ps.body.partners) || []).map((p) => [p.id, p]));\nconst groups = new Map();\nif (!summary.problem) for (const d of deals.body.deals || []) {\n  if (!d.due_date || d.due_date >= cut || !(d.due_amount > 0)) continue;\n  const key = d.partner_id || 'none';\n  if (!groups.has(key)) groups.set(key, []);\n  groups.get(key).push({ number: d.ref_number || null, deal_id: d.id, issue_date: d.issue_date, due_date: d.due_date, amount: d.amount, due_amount: d.due_amount,\n    days_overdue: Math.round(today.startOf('day').diff(DateTime.fromISO(d.due_date, { zone: c.timezone }), 'days').days) });\n}\nconst customers = [];\nfor (const [key, items] of groups) {\n  summary.count += items.length;\n  const p = byId.get(key);\n  const total = items.reduce((s, x) => s + x.due_amount, 0);\n  if (!p) { summary.no_partner.push({ items, total }); continue; }\n  const entry = { partner_id: p.id, name: p.long_name || p.name, title: p.default_title && p.default_title !== '(空白)' ? p.default_title : '御中', contact: p.contact_name || '', email: String(p.email || '').trim(), items, total };\n  if (!entry.email) summary.no_email.push(entry); else customers.push(entry);\n}\ncustomers.sort((a, b) => b.total - a.total);\nconst take = customers.slice(0, Number(c.max_customers));\nif (customers.length > take.length) summary.problem = (customers.length - take.length) + ' more customers were left for the next run (max_customers).';\nsummary.drafts = take.map((x) => x.name);\nsummary.today = today.toFormat('yyyy-MM-dd');\nconst yen = (v) => Number(v).toLocaleString('ja-JP') + '円';\nconst jaDate = (s) => { const d = DateTime.fromISO(s); return d.year + '年' + d.month + '月' + d.day + '日'; };\nconst out = take.map((x) => ({ json: { ok: true, summary, customer: x,\n  facts: x.items.map((i) => '- 請求書番号: ' + (i.number || '（番号なし）') + ' / 請求額: ' + yen(i.amount) + ' / お支払い期日: ' + jaDate(i.due_date) + ' / 未入金の額: ' + yen(i.due_amount) + (i.due_amount < i.amount ? '（一部ご入金済み: ' + yen(i.amount - i.due_amount) + '）' : '')).join('\\n')\n    + (x.items.length > 1 ? '\\n- 未入金の合計: ' + yen(x.total) : '') } }));\nreturn out.length ? out : [{ json: { ok: false, summary } }];"
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000007",
      "name": "Any reminder to write?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000008",
      "name": "Claude Haiku 4.5",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.5,
      "position": [
        1320,
        320
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-haiku-4-5-20251001",
          "cachedResultName": "Claude Haiku 4.5"
        },
        "options": {}
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000009",
      "name": "Write the reminder",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "promptType": "define",
        "text": "={{ '宛名: ' + $json.customer.name + ' ' + $json.customer.title + ($json.customer.contact ? '（ご担当: ' + $json.customer.contact + '様）' : '') + '\\n差出人の署名: ' + $('Config').first().json.sender_signature + '\\n\\n未入金の請求（この内容だけを使う）:\\n' + $json.facts }}",
        "messages": {
          "messageValues": [
            {
              "type": "SystemMessagePromptTemplate",
              "message": "You write a polite payment reminder e-mail in Japanese from a small company to a customer.\nUse only the facts given: the invoice numbers, the remaining amounts, the due dates. Write every invoice number, every remaining amount (\"未入金の額\") and every due date exactly as given. If an invoice was partly paid, say how much remains and thank them for the partial payment. Do not add any other amount, date, fee, interest or penalty. Do not threaten, do not mention legal action, collection agencies or late fees.\nSay that if the payment was already made and the messages crossed, they can ignore this mail. Ask them to tell you the planned payment date. End with the sender's signature exactly as given.\nReturn only JSON: {\"subject\": \"...\", \"body\": \"...\"}"
            }
          ]
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000010",
      "name": "Check the drafts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        0
      ],
      "parameters": {
        "jsCode": "// Every number in the draft must come from freee. A draft that fails is not saved.\nconst src = $('Find overdue sales').all();\nconst nf = (s) => String(s ?? '').normalize('NFKC');\nconst BANNED = /延滞金|遅延損害金|損害金|利息|法的|訴訟|弁護士|裁判|債権回収|督促状を送付|差し押さえ/;\nreturn $input.all().map((item, i) => {\n  const x = src[i].json.customer;\n  let a;\n  try { a = JSON.parse(String(item.json.text ?? '').match(/\\{[\\s\\S]*\\}/)[0]); } catch (e) { return { json: { ok: false, customer: x, why: ['the AI answer was not JSON'] } }; }\n  const body = nf(a.body), subject = nf(a.subject);\n  const why = [];\n  const flat = body.replace(/,/g, '');\n  const yenIn = (v) => flat.includes(String(v) + '円') || flat.includes('¥' + v) || flat.includes('￥' + v);\n  for (const it of x.items) {\n    if (it.number && !body.includes(nf(it.number))) why.push('invoice number ' + it.number + ' is missing');\n    if (!yenIn(it.due_amount)) why.push('the remaining amount ' + it.due_amount + ' yen for ' + (it.number || 'an invoice') + ' is missing');\n    const d = DateTime.fromISO(it.due_date);\n    if (![it.due_date, d.year + '年' + d.month + '月' + d.day + '日', d.month + '月' + d.day + '日', d.toFormat('yyyy/MM/dd'), d.year + '/' + d.month + '/' + d.day].some((f) => body.includes(f))) why.push('the due date ' + it.due_date + ' is missing');\n  }\n  // Every yen amount in the draft must be one of these\n  const allowed = new Set([x.total, ...x.items.flatMap((it) => [it.due_amount, it.amount, it.amount - it.due_amount])].map(Number));\n  const found = [...flat.matchAll(/(?:[¥￥]\\s?(\\d+)|(\\d+)\\s?円)/g)].map((m) => Number(m[1] || m[2]));\n  const extra = found.filter((v) => !allowed.has(v));\n  if (extra.length) why.push('amounts not in freee: ' + [...new Set(extra)].join(', '));\n  const bad = (body + subject).match(BANNED);\n  if (bad) why.push('contains \"' + bad[0] + '\" (threats, fees or legal words are not allowed)');\n  if (!a.subject || !a.body) why.push('no subject or body');\n  return { json: { ok: !why.length, customer: x, subject: a.subject, body: a.body, why } };\n});"
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000011",
      "name": "Draft passed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1760,
        -100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000012",
      "name": "Save as Gmail draft",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1980,
        -200
      ],
      "parameters": {
        "resource": "draft",
        "operation": "create",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.body }}",
        "options": {
          "sendTo": "={{ $json.customer.email }}"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "c9000000-0000-4000-8000-000000000013",
      "name": "Write the notice",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        200
      ],
      "parameters": {
        "jsCode": "// One mail to the staff: drafts saved, drafts stopped and why, customers with no e-mail, sales with no customer.\nconst yen = (v) => Number(v).toLocaleString('ja-JP') + ' yen';\nconst first = $('Find overdue sales').first().json;\nconst s = first.summary;\nconst checked = $('Check the drafts').isExecuted ? $('Check the drafts').all().map((i) => i.json) : [];\nconst line = (it) => '  ' + (it.number || '(no number)') + ': ' + yen(it.due_amount) + ' due, ' + it.days_overdue + ' days past ' + it.due_date + (it.due_amount < it.amount ? ' (partly paid; billed ' + yen(it.amount) + ')' : '');\nconst L = [];\nif (s.problem) L.push('!! ' + s.problem, '');\nconst okd = checked.filter((x) => x.ok), bad = checked.filter((x) => !x.ok);\nif (okd.length) { L.push('Reminder drafts saved in Gmail (NOT sent; check and send them yourself):'); okd.forEach((x) => { L.push('- ' + x.customer.name + ' <' + x.customer.email + '> total ' + yen(x.customer.total)); x.customer.items.forEach((it) => L.push(line(it))); }); L.push(''); }\nif (bad.length) { L.push('Drafts NOT saved (the check failed; write these by hand):'); bad.forEach((x) => { L.push('- ' + x.customer.name + ': ' + x.why.join('; ')); }); L.push(''); }\nif (s.no_email.length) { L.push('No e-mail address in freee (no draft):'); s.no_email.forEach((x) => { L.push('- ' + x.name + ' total ' + yen(x.total)); x.items.forEach((it) => L.push(line(it))); }); L.push(''); }\nif (s.no_partner.length) { L.push('Sales with no customer set in freee:'); s.no_partner.forEach((x) => x.items.forEach((it) => L.push(line(it)))); L.push(''); }\nconst n = s.count;\nconst title = '[n8n overdue] ' + (n ? n + ' overdue sales, ' + okd.length + ' reminder drafts' : 'No overdue sales') + ' (' + s.today + ')';\nif (!n && !s.problem) L.push('No sales are past their due date with an amount still due.');\nL.push('Read from freee Accounting: unsettled income deals (first 100).');\nreturn [{ json: { title, message: L.join('\\n'), to: $('Config').first().json.staff_email } }];"
      },
      "executeOnce": true
    },
    {
      "id": "c9000000-0000-4000-8000-000000000014",
      "name": "Tell the staff",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2200,
        200
      ],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $json.to }}",
        "subject": "={{ $json.title }}",
        "emailType": "text",
        "message": "={{ $json.message }}",
        "options": {
          "appendAttribution": false
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000015",
      "name": "Draft failed: say so",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2200,
        -100
      ],
      "parameters": {
        "jsCode": "const err = $json.error && ($json.error.message || $json.error);\nreturn [{ json: { title: '[n8n overdue] A reminder draft could not be saved', message: 'Gmail refused to save a draft' + (err ? ': ' + String(err).slice(0, 300) : '') + '. The notice lists the drafts that should exist; check Gmail.', to: $('Config').first().json.staff_email } }];"
      },
      "executeOnce": true
    },
    {
      "parameters": {
        "content": "## Draft payment reminders for overdue freee sales\nEvery weekday morning this reads **unsettled sales in freee Accounting**, finds the ones **past their due date with an amount still due**, and groups them by customer. **Claude writes a polite reminder** (in Japanese) for each customer with an e-mail address. **Code checks every number in it against freee**: each invoice number, the remaining amount (not the billed total), each due date, and no other amount; threats, late fees and legal words are rejected. Passing reminders are saved as **Gmail drafts — nothing is sent**. The staff get one mail: drafts saved, drafts stopped and why, customers with no e-mail.\n\n### Setup\n1. A freee app with permissions for deals and partners (read). Put the invoice number in the deal's 管理番号 (ref number) if you want it in the reminder.\n2. n8n **OAuth2 API** credential for freee (auth URL https://accounts.secure.freee.co.jp/public_api/authorize, token URL .../public_api/token, authentication: Body). Also Gmail and Anthropic.\n3. Fill **Config** (company ID, staff e-mail, signature). Use **Run now** first; set today_override to test with a past date.",
        "height": 1000,
        "width": 600,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -660,
        -320
      ],
      "id": "c9000000-0000-4000-9000-000000000015",
      "name": "Overview"
    },
    {
      "parameters": {
        "content": "## 1. Read freee\nUnsettled income deals (first 100) and the partners (for names, honorifics and e-mail addresses).",
        "height": 520,
        "width": 840,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -20,
        -120
      ],
      "id": "c9000000-0000-4000-9000-000000000016",
      "name": "Section 1"
    },
    {
      "parameters": {
        "content": "## 2. Overdue, by customer\nPast the due date (min_days_overdue), amount still due. Customers without an e-mail are listed, not drafted.",
        "height": 520,
        "width": 400,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        860,
        -120
      ],
      "id": "c9000000-0000-4000-9000-000000000017",
      "name": "Section 2"
    },
    {
      "parameters": {
        "content": "## 3. Claude writes, code checks\nClaude gets only the facts. Code rejects a draft that misses a number, remaining amount or due date, adds any other amount, or uses words like 延滞金 or 法的.",
        "height": 820,
        "width": 640,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1280,
        -320
      ],
      "id": "c9000000-0000-4000-9000-000000000018",
      "name": "Section 3"
    },
    {
      "parameters": {
        "content": "## 4. Gmail drafts and the notice\nDrafts are saved for a person to read and send. One notice mail to the staff with everything found.",
        "height": 820,
        "width": 520,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1940,
        -320
      ],
      "id": "c9000000-0000-4000-9000-000000000019",
      "name": "Section 4"
    }
  ],
  "connections": {
    "Every weekday 9:00": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run now": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Read unsettled sales",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read unsettled sales": {
      "main": [
        [
          {
            "node": "Read freee partners",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read freee partners": {
      "main": [
        [
          {
            "node": "Find overdue sales",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find overdue sales": {
      "main": [
        [
          {
            "node": "Any reminder to write?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any reminder to write?": {
      "main": [
        [
          {
            "node": "Write the reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Haiku 4.5": {
      "ai_languageModel": [
        [
          {
            "node": "Write the reminder",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Write the reminder": {
      "main": [
        [
          {
            "node": "Check the drafts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check the drafts": {
      "main": [
        [
          {
            "node": "Draft passed?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Write the notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft passed?": {
      "main": [
        [
          {
            "node": "Save as Gmail draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the notice": {
      "main": [
        [
          {
            "node": "Tell the staff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save as Gmail draft": {
      "main": [
        [],
        [
          {
            "node": "Draft failed: say so",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft failed: say so": {
      "main": [
        [
          {
            "node": "Tell the staff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}