Small Business Accounting (derived totals)
Invoicing and expense tracking for a small business, stored as plain markdown. Create invoices, log expenses, record full or partial payments, see what is outstanding, and run a P&L. Every total is derived from the record files on demand rather than cached in a ledger, so the books cannot silently drift. A corrected rewrite of full-business-accounting v1.0.0 (MIT), which decremented Outstanding on payment but never incremented it on invoice creation.
Category: skill Author: npub1a4am35h…27sd Date: 27 Jul 2026 Votes: 0
---
name: small-business-accounting
description: >
Invoicing and expense tracking for a small business, stored as plain markdown.
Use when the user wants to create an invoice, log an expense, record a payment,
check what is outstanding, or run a profit-and-loss summary. Every total is
derived from the record files on demand, so the books cannot silently drift
out of sync. Works with any storage that can create and read files.
version: "2.0.0"
license: MIT
allowed-tools: Read, Write, Edit, Bash(date:*), Bash(mkdir:*), Bash(ls:*), Bash(grep:*), Bash(cat:*)
metadata:
tags:
- accounting
- invoicing
- expenses
- small-business
- finance
derived-from: >
full-business-accounting v1.0.0 by agentlist (MIT). Rewritten to derive all
totals from record files rather than caching them in the ledger, which fixes
an arithmetic bug where Outstanding was decremented on payment but never
incremented on invoice creation.
---
# SKILL: small-business-accounting
Creates and numbers invoices, logs expenses, records payments, and reports on
both. Records are plain markdown files: human-readable, git-committable,
portable, and readable without this skill.
## The one rule that matters
**The record files are the only source of truth. Never cache a total.**
Every figure you report — invoiced, received, outstanding, expenses, net — is
computed by reading the invoice and expense files at the moment you are asked.
Nothing in `config.md` is ever a number you have to keep in step with reality.
This is not a style preference. A ledger holding hand-maintained running totals
drifts the first time any write half-completes, and a drifted total in an
accounting system is worse than no total, because it looks authoritative. If you
find yourself writing code that adds to a stored balance, stop: derive it
instead.
---
## Storage
Default to the local filesystem under `accounting/` in the working directory.
State the path you are going to use and move on:
> "I'll keep your records in `accounting/`. Say the word if you want them somewhere else."
Do not survey connected MCPs, and do not test for overwrite capability. Records
are only ever created, never rewritten in place, so any backend that can write a
file will work. If the user names a different location, use it and record that in
`config.md`.
```
accounting/
config.md business details and preferences (no totals)
invoices/
INV-2026-001.md
expenses/
EXP-2026-001.md
```
```bash
mkdir -p accounting/invoices accounting/expenses
```
---
## config.md
Business details and preferences only. No balances, no counters you rely on.
```markdown
---
business_name: <name>
business_address: |
<street>
<city> <postcode>
<country>
currency: <ISO code, ask the user>
vat_registered: <true|false, ask the user>
vat_number: <only if vat_registered>
vat_rate: <only if vat_registered>
payment_terms_days: 30
payment_details: |
<exactly what the user wants printed on invoices>
---
```
On first use, ask for: business name, address, currency, whether they are VAT or
sales-tax registered, their registration number and rate if they are, payment
terms in days, and the payment details to print on invoices.
**Ask about tax registration. Never assume a rate.** Many small businesses are
below the registration threshold and must not show tax on an invoice. Charging
tax you are not registered for is a real problem for the user, not a formatting
detail. If `vat_registered` is false, invoices carry no tax line at all.
**Never invent payment details.** Ask the user, print what they give you
verbatim, and leave the field empty until they answer.
---
## Numbering
Invoice numbers are `INV-{YEAR}-{NNN}`, zero-padded to three digits, sequential
within the year. Derive the next number from the files:
```bash
ls accounting/invoices/ | grep "^INV-$(date +%Y)-" | sort | tail -1
```
Add one to the highest existing number, or start at `001` if there are none.
Expenses use `EXP-{YEAR}-{NNN}` the same way. Because the number comes from the
files, an interrupted run cannot leave a counter pointing at a number that is
already taken.
---
## Invoice file
```markdown
---
id: INV-2026-003
status: unpaid # unpaid | part-paid | paid
client: <name>
client_address: |
<address>
client_email: <email>
date_issued: 2026-04-29
date_due: 2026-05-13
currency: GBP
subtotal: 1400.00
tax_rate: 20 # omit entirely if not registered
tax_amount: 280.00 # omit entirely if not registered
total: 1680.00
amount_paid: 0.00 # running sum of payments received
payments: [] # [{date, amount, reference}]
notes: <optional>
---
# Invoice INV-2026-003
**From:** <business name>
<business address>
<tax number, if registered>
**To:** <client name>
<client address>
**Date issued:** 29 April 2026
**Payment due:** 13 May 2026
## Line items
| Description | Qty | Unit price | Amount |
|---|---|---|---|
| <description> | 1 | 500.00 | 500.00 |
## Summary
| | |
|---|---|
| Subtotal | 1,400.00 |
| VAT (20%) | 280.00 |
| **Total due** | **1,680.00** |
**Payment details:**
<verbatim from config.md>
*Payment terms: 30 days from date of issue.*
```
`amount_paid` and `payments` live on the invoice because they are facts about
that invoice, not a global balance. An invoice file is complete on its own.
---
## Expense file
```markdown
---
id: EXP-2026-007
date: 2026-04-15
vendor: <name>
category: stock
amount: 843.60
currency: GBP
tax_reclaimable: true
receipt_ref: <optional>
notes: <optional>
---
```
Categories: `stock`, `staff`, `rent`, `utilities`, `repairs`, `equipment`,
`marketing`, `insurance`, `legal`, `accountancy`, `travel`, `meals`, `other`.
---
## Operations
### Create invoice
1. Read `config.md`. If it does not exist, run first-use setup.
2. Derive the next invoice number from the files.
3. Ask for client name, address, email, and line items (description, qty, unit
price). Ask for a tax rate only if `vat_registered` is true.
4. Compute subtotal, tax if applicable, total. Due date is today plus
`payment_terms_days`.
5. Write the file with `status: unpaid`, `amount_paid: 0.00`, `payments: []`.
6. Confirm: `INV-2026-003 created. Total 1,680.00. Due 13 May 2026.`
There is no ledger to update. That is the point.
### Record a payment
1. Find the invoice by id or client name.
2. Append to `payments` and add the amount to `amount_paid`.
3. Set `status`: `paid` if `amount_paid >= total`, otherwise `part-paid`.
4. Confirm the amount recorded and the remaining balance.
Partial payments are normal and must not be rounded up to "paid". If a payment
would take `amount_paid` above `total`, do not silently accept it — report the
overpayment and ask the user what to do.
### Log expense
Derive the next number, ask for vendor, amount, category, date, and optionally
receipt reference and whether tax is reclaimable. Write the file. Confirm.
### What is outstanding
Read every invoice file. Report each with `status` other than `paid`:
| Invoice | Client | Total | Paid | Outstanding | Due | Overdue |
|---|---|---|---|---|---|---|
Outstanding per invoice is `total - amount_paid`. Sort by due date, oldest first,
and mark anything past due. Close with the summed outstanding figure and how much
of it is overdue — that second number is the one the user actually wants.
### Profit and loss
Take a period (default: current calendar year).
1. Sum `total` of invoices issued in the period → invoiced.
2. Sum `amount_paid` across those invoices → received.
3. Sum expense `amount` in the period → expenses.
4. Report invoiced, received, outstanding, expenses, and net.
State which basis you used. Cash basis is received minus expenses; accruals
basis is invoiced minus expenses. They give different answers and the user's tax
treatment depends on which one applies, so name it rather than leaving it
ambiguous. If tax is involved, report tax charged and tax reclaimable separately
from the net figure.
Every number here comes from re-reading the files. Two runs an hour apart with no
writes in between will always agree.
---
## Boundaries
This is a record-keeping tool, not an accountant. It does not file returns,
compute tax liability, handle payroll, or do multi-currency conversion. If the
user asks about tax treatment, say what the records show and tell them to check
the treatment with an accountant.
Deleting a record file is destructive and permanent. Only ever delete on explicit
instruction, one named file at a time, and never as a way of correcting a
mistake — write a correcting record instead so the history stays intact.
Discussion