Late invoice reminder drafts
A weekly loop: decide who is due a reminder, draft it in the owner's words, and stop at the final stage so the owner can call. The agent drafts; the owner sends.
Inputs
invoices.csv, the owner's export of unpaid invoices, withinvoice, customer, contact_name, contact_email, amount, due_date, last_stage_sent, last_sent_date, status, never_remind(see examples/invoices.csv).statusisopen,disputedorpaid(paid since the export).- The owner's stage texts, written with the owner once, with placeholders such as
{invoice},{amount},{due_date}(see examples/stages.md). - The owner's stage days (when each stage applies) and the least gap between two reminders.
The intern never logs in to the accounting tool; the owner exports the list, or the agent reads it through the AHL Action Gateway. The agent holds only its agent token and stores no credentials in files or prompts.
Steps
- Plan the week:
The script never skips a stage (an invoice 40 days late that had no reminder gets stage 1, not the final notice), never reminds twice inside the gap, skipsnode scripts/plan.mjs examples/invoices.csv --as-of 2026-10-06 --stages 7,21,35 --min-gap 7paid,disputedandnever_remindrows, and moves invoices past the final stage to the owner's call list. Compare examples/expected-plan.md. - Draft each "Draft now" line from the owner's stage text. Fill only the placeholders: invoice number, amount with cents, due date in words, days late, contact first name. Do not add new content: no late fees, interest, discounts, payment plans, deadlines or consequences the owner did not write.
- Check every draft before saving: the amount and due date match the export exactly; the recipient is the invoice contact; the text contains none of the owner's never-automate phrases; it names only payment methods the owner listed in the stage text; no other customer's details appear.
- Save drafts through the gateway. One
gmail.draftper reminder (ormanual.draftwhen the business has no connected Gmail). Sending is the owner's:gmail.sendandmanual.sendare always held for approval, and in rehearsal drafts are held too. Never batch several customers into one message. - Report to the owner: the drafts, the call list with each invoice's reminder history, and what was skipped and why.
- Handle replies. When a customer replies, summarise it in one line for the owner: promised to pay (with date), disputes, asks for time, or other. A dispute or a request for time stops the reminders for that invoice: set
statustodisputedin the owner's next export note.
Output
The plan from the script, then the drafts and the owner note, as in examples/expected-drafts.md.
Edge cases
- Partial payment received: remind only for the balance, and only if the owner confirms the remaining amount.
- Several late invoices for one customer: one draft listing each invoice, at the stage of the oldest.
- Contact bounced or left: no draft; ask the owner for the right contact.
- Customer is an individual, not a business: draft as usual, but tell the owner. The FTC describes the Fair Debt Collection Practices Act as covering third-party collectors of consumer debts; if the owner wants to pass debts to a collector or sell them, that is outside this skill and needs the owner's own adviser.
- Hardship mentioned in a reply (illness, bereavement, job loss): stop reminders and flag it kindly to the owner.
Never
- Send, schedule or auto-approve a reminder.
- Threaten, mention credit reports, collections agencies or legal action, or add fees, interest or deadlines the owner did not write.
- Offer discounts, payment plans or write-offs.
- Contact anyone other than the invoice contact, or by any channel other than the owner's mailbox.
- Remind a customer marked disputed, paid or never-remind.
- Take payment details or card numbers by email.
Hand over
The owner opens the held drafts, checks each against their own records (especially anything paid since the export), edits if they wish, and approves or declines. They make the calls on the call list and record outcomes so next week's plan starts from them.
The skill folder
Every file in the folder. Download each one into a folder with the same layout, or copy it.
late-invoice-reminder-drafts/ SKILL.md examples/expected-drafts.md examples/expected-plan.md examples/invoices.csv examples/stages.md scripts/plan.mjs
SKILL.md
---
name: late-invoice-reminder-drafts
description: Works out which unpaid invoices are due a reminder today and at which of the owner's stages (friendly, firmer, final), then drafts each reminder from the owner's own wording for approval and puts invoices past the final stage on the owner's call list. Use when an owner or bookkeeper exports unpaid invoices and asks to chase late payers, send overdue reminders or run the weekly collections loop. Drafts only, through the gateway; the owner approves every send.
metadata:
ahl-category: money
ahl-jobs: collections
ahl-industries: cleaning, plumbing, cafe-bakery, catering, consulting, marketing-agency
ahl-connectors: gmail, quickbooks-online, xero
ahl-workflows: money--03-weekly-late-invoice-reminders, money--04-decide-which-late-payers-need-a-call, money--1425-cafe-bakery-wholesale-invoices-and-statements, money--1446-catering-deposits-and-balance-reminders
ahl-human-check: The owner approves every reminder before it goes out and handles disputes, calls, payment plans and fees personally.
ahl-proposed-status: approved
ahl-version: 1
ahl-sources: |
Federal Trade Commission — Fair Debt Collection Practices Act — https://www.ftc.gov/legal-library/browse/statutes/fair-debt-collection-practices-act — 2026-10-06
---
# Late invoice reminder drafts
A weekly loop: decide who is due a reminder, draft it in the owner's words, and stop at the final stage so the owner can call. The agent drafts; the owner sends.
## Inputs
- `invoices.csv`, the owner's export of unpaid invoices, with `invoice, customer, contact_name, contact_email, amount, due_date, last_stage_sent, last_sent_date, status, never_remind` (see [examples/invoices.csv](examples/invoices.csv)). `status` is `open`, `disputed` or `paid` (paid since the export).
- The owner's stage texts, written with the owner once, with placeholders such as `{invoice}`, `{amount}`, `{due_date}` (see [examples/stages.md](examples/stages.md)).
- The owner's stage days (when each stage applies) and the least gap between two reminders.
The intern never logs in to the accounting tool; the owner exports the list, or the agent reads it through the AHL Action Gateway. The agent holds only its agent token and stores no credentials in files or prompts.
## Steps
1. **Plan the week:**
```bash
node scripts/plan.mjs examples/invoices.csv --as-of 2026-10-06 --stages 7,21,35 --min-gap 7
```
The script never skips a stage (an invoice 40 days late that had no reminder gets stage 1, not the final notice), never reminds twice inside the gap, skips `paid`, `disputed` and `never_remind` rows, and moves invoices past the final stage to the owner's call list. Compare [examples/expected-plan.md](examples/expected-plan.md).
2. **Draft each "Draft now" line** from the owner's stage text. Fill only the placeholders: invoice number, amount with cents, due date in words, days late, contact first name. Do not add new content: no late fees, interest, discounts, payment plans, deadlines or consequences the owner did not write.
3. **Check every draft** before saving: the amount and due date match the export exactly; the recipient is the invoice contact; the text contains none of the owner's never-automate phrases; it names only payment methods the owner listed in the stage text; no other customer's details appear.
4. **Save drafts through the gateway.** One `gmail.draft` per reminder (or `manual.draft` when the business has no connected Gmail). Sending is the owner's: `gmail.send` and `manual.send` are always held for approval, and in rehearsal drafts are held too. Never batch several customers into one message.
5. **Report** to the owner: the drafts, the call list with each invoice's reminder history, and what was skipped and why.
6. **Handle replies.** When a customer replies, summarise it in one line for the owner: promised to pay (with date), disputes, asks for time, or other. A dispute or a request for time stops the reminders for that invoice: set `status` to `disputed` in the owner's next export note.
## Output
The plan from the script, then the drafts and the owner note, as in [examples/expected-drafts.md](examples/expected-drafts.md).
## Edge cases
- **Partial payment received**: remind only for the balance, and only if the owner confirms the remaining amount.
- **Several late invoices for one customer**: one draft listing each invoice, at the stage of the oldest.
- **Contact bounced or left**: no draft; ask the owner for the right contact.
- **Customer is an individual, not a business**: draft as usual, but tell the owner. The FTC describes the Fair Debt Collection Practices Act as covering third-party collectors of consumer debts; if the owner wants to pass debts to a collector or sell them, that is outside this skill and needs the owner's own adviser.
- **Hardship mentioned in a reply** (illness, bereavement, job loss): stop reminders and flag it kindly to the owner.
## Never
- Send, schedule or auto-approve a reminder.
- Threaten, mention credit reports, collections agencies or legal action, or add fees, interest or deadlines the owner did not write.
- Offer discounts, payment plans or write-offs.
- Contact anyone other than the invoice contact, or by any channel other than the owner's mailbox.
- Remind a customer marked disputed, paid or never-remind.
- Take payment details or card numbers by email.
## Hand over
The owner opens the held drafts, checks each against their own records (especially anything paid since the export), edits if they wish, and approves or declines. They make the calls on the call list and record outcomes so next week's plan starts from them.
examples/expected-drafts.md
# Reminder drafts for the owner, 2026-10-06 (3 drafts; nothing sent) Each draft below was saved with `manual.draft` through the gateway and is waiting on the owner's agents page. Before approving, check none of these invoices was paid since the export. ## 1. INV-3301, Maple Row Dental, stage 1 To: ap@maplerow.example Subject: Invoice INV-3301 from Brightline Office Cleaning Hi Jordan, a quick note that invoice INV-3301 for $1,240.00 was due on September 25. If it is already on its way, thank you and please ignore this. If anything on it looks wrong, just reply and I will sort it out. Pat Brightline Office Cleaning ## 2. INV-3288, Fenwick Builders, stage 2 To: office@fenwick.example Subject: Second reminder: invoice INV-3288 Hi Casey, invoice INV-3288 for $4,810.50 is now 26 days past its due date of September 10. Could you let me know when we can expect payment? Reply here if there is a problem with the invoice. Pat Brightline Office Cleaning ## 3. INV-3275, Lakeside Florist, stage 3 (final notice) To: morgan@lakesideflorist.example Subject: Final reminder: invoice INV-3275 Hi Morgan, this is my last email reminder for invoice INV-3275 ($385.00, due August 28). I will give you a call this week so we can settle it together. Pat Brightline Office Cleaning --- For the owner, not drafted: Hilltop Cafe (INV-3260, $920.00) is on your call list after the final notice of September 22. Grant Street Deli (disputed) and Pine Ridge Church (never-remind) were skipped.
examples/expected-plan.md
# Reminder plan as of 2026-10-06 (stages at 7, 21, 35 days; at least 7 days apart) ## Draft now (3) - INV-3301 Maple Row Dental $1240.00, 11 days past due: draft stage 1 to ap@maplerow.example - INV-3288 Fenwick Builders $4810.50, 26 days past due: draft stage 2 to office@fenwick.example - INV-3275 Lakeside Florist $385.00, 39 days past due: draft stage 3 (final notice) to morgan@lakesideflorist.example ## Owner's call list (1) - INV-3260 Hilltop Cafe $920.00, 52 days past due: final notice sent 2026-09-22, put on the owner's call list ## Not yet (1) - INV-3312 Cobalt Yoga $610.00, 4 days past due: first reminder at 7 days ## Skipped (2) - INV-3297 Grant Street Deli $275.00, 21 days past due: disputed, owner handles it - INV-3290 Pine Ridge Church $500.00, 24 days past due: owner marked never-remind Nothing has been sent. Re-check paid status with the owner before any draft goes out.
examples/invoices.csv
invoice,customer,contact_name,contact_email,amount,due_date,last_stage_sent,last_sent_date,status,never_remind INV-3301,Maple Row Dental,Jordan,ap@maplerow.example,1240.00,2026-09-25,,,open,no INV-3288,Fenwick Builders,Casey,office@fenwick.example,4810.50,2026-09-10,1,2026-09-19,open,no INV-3275,Lakeside Florist,Morgan,morgan@lakesideflorist.example,385.00,2026-08-28,2,2026-09-29,open,no INV-3260,Hilltop Cafe,Robin,robin@hilltopcafe.example,920.00,2026-08-15,3,2026-09-22,open,no INV-3312,Cobalt Yoga,Avery,avery@cobaltyoga.example,610.00,2026-10-02,,,open,no INV-3297,Grant Street Deli,Sam,sam@grantdeli.example,275.00,2026-09-15,1,2026-09-24,disputed,no INV-3290,Pine Ridge Church,Lee,lee@pineridge.example,500.00,2026-09-12,,,open,yes
examples/stages.md
# Reminder stages (invented example: Brightline Office Cleaning, owner "Pat")
Payment methods the owner allows in reminders: "pay through the link on the invoice, or by cheque to the office address on the invoice".
## Stage 1 (friendly)
Subject: Invoice {invoice} from Brightline Office Cleaning
Hi {contact_name}, a quick note that invoice {invoice} for {amount} was due on {due_date}. If it is already on its way, thank you and please ignore this. If anything on it looks wrong, just reply and I will sort it out.
## Stage 2 (firmer)
Subject: Second reminder: invoice {invoice}
Hi {contact_name}, invoice {invoice} for {amount} is now {days_late} days past its due date of {due_date}. Could you let me know when we can expect payment? Reply here if there is a problem with the invoice.
## Stage 3 (final notice)
Subject: Final reminder: invoice {invoice}
Hi {contact_name}, this is my last email reminder for invoice {invoice} ({amount}, due {due_date}). I will give you a call this week so we can settle it together.
scripts/plan.mjs
#!/usr/bin/env node
// Decides, for each unpaid invoice, which reminder stage is due today, or why none is.
// Usage: node scripts/plan.mjs <invoices.csv> --as-of YYYY-MM-DD [--stages 7,21,35] [--min-gap 7]
// Local files only, no network. It plans; it sends nothing.
import fs from "node:fs";
// Owner's choices, passed as options. Defaults match the example only:
// --stages: days past due when stage 1, 2, 3... become due (the last stage is the final notice).
// --min-gap: least number of days between two reminders to the same invoice, so nobody gets two in a week.
let STAGES = [7, 21, 35];
let MIN_GAP_DAYS = 7;
function parseCsv(path) {
let text;
try { text = fs.readFileSync(path, "utf8"); } catch { console.error(`Cannot read ${path}. Check the file name.`); process.exit(2); }
const rows = [];
let row = [], field = "", q = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (q) { if (c === '"' && text[i + 1] === '"') { field += '"'; i++; } else if (c === '"') q = false; else field += c; }
else if (c === '"') q = true;
else if (c === ",") { row.push(field); field = ""; }
else if (c === "\n" || c === "\r") { if (c === "\r" && text[i + 1] === "\n") i++; row.push(field); field = ""; if (row.some((x) => x !== "")) rows.push(row); row = []; }
else field += c;
}
if (field !== "" || row.length) { row.push(field); if (row.some((x) => x !== "")) rows.push(row); }
const [head, ...data] = rows;
return data.map((r) => Object.fromEntries(head.map((h, i) => [h.trim(), (r[i] ?? "").trim()])));
}
const day = (d) => Math.round(Date.parse(d + "T00:00:00Z") / 86400000);
const money = (c) => "$" + (c / 100).toFixed(2);
const args = process.argv.slice(2);
const opt = (n) => { const i = args.indexOf(n); if (i < 0) return undefined; const v = args[i + 1]; args.splice(i, 2); return v; };
const asOf = opt("--as-of"), st = opt("--stages"), gap = opt("--min-gap");
if (st) STAGES = st.split(",").map(Number);
if (gap) MIN_GAP_DAYS = Number(gap);
if (!args[0] || !asOf || Number.isNaN(day(asOf))) { console.error("Usage: node scripts/plan.mjs <invoices.csv> --as-of YYYY-MM-DD [--stages 7,21,35] [--min-gap 7]"); process.exit(2); }
const today = day(asOf);
const groups = { draft: [], wait: [], skip: [], call: [] };
for (const inv of parseCsv(args[0])) {
const late = today - day(inv.due_date);
const sent = Number(inv.last_stage_sent || 0);
const since = inv.last_sent_date ? today - day(inv.last_sent_date) : Infinity;
const amt = money(Math.round(Number(inv.amount) * 100));
const id = `${inv.invoice} ${inv.customer} ${amt}, ${late} days past due`;
if (inv.status === "paid") groups.skip.push(`${id}: marked paid since the export`);
else if (inv.status === "disputed") groups.skip.push(`${id}: disputed, owner handles it`);
else if (inv.never_remind === "yes") groups.skip.push(`${id}: owner marked never-remind`);
else if (late < STAGES[0]) groups.wait.push(`${id}: first reminder at ${STAGES[0]} days`);
else if (sent >= STAGES.length) groups.call.push(`${id}: final notice sent ${inv.last_sent_date}, put on the owner's call list`);
else {
const due = STAGES.filter((d) => late >= d).length; // highest stage the lateness allows
const next = Math.min(sent + 1, due); // never skip a stage
if (next <= sent) groups.wait.push(`${id}: stage ${sent} sent ${inv.last_sent_date}, stage ${sent + 1} at ${STAGES[sent]} days`);
else if (since < MIN_GAP_DAYS) groups.wait.push(`${id}: last reminder ${since} days ago, waiting for ${MIN_GAP_DAYS}`);
else groups.draft.push(`${id}: draft stage ${next}${next === STAGES.length ? " (final notice)" : ""} to ${inv.contact_email}`);
}
}
console.log(`# Reminder plan as of ${asOf} (stages at ${STAGES.join(", ")} days; at least ${MIN_GAP_DAYS} days apart)\n`);
for (const [title, k] of [["Draft now", "draft"], ["Owner's call list", "call"], ["Not yet", "wait"], ["Skipped", "skip"]]) {
console.log(`## ${title} (${groups[k].length})`);
for (const l of groups[k]) console.log(`- ${l}`);
console.log("");
}
console.log("Nothing has been sent. Re-check paid status with the owner before any draft goes out.");
Use it in Claude Code
Put the folder at .claude/skills/late-invoice-reminder-drafts/ in your project (or ~/.claude/skills/late-invoice-reminder-drafts/ to use it in every project). Claude Code finds SKILL.md there and uses the skill when a request matches its description.
Other agents that read the Agent Skills format take the same folder.
Sources
- Federal Trade Commission: Fair Debt Collection Practices ActChecked 6 Oct 2026