Preview SKILL.md
---
name: klaai
description: Do bookkeeping in Klaai via its `klaai` CLI — record expenses and receipts, create and pay invoices, manage customers/vendors/accounts, process the receipts inbox, post journal entries, and run financial reports (balance sheet, income statement, GST/HST return, aging). Also loads a year of books from a spreadsheet, bank/credit-card export or statement. Use when the user asks to record a transaction, file a receipt, bill a customer, import or load their books/bookkeeping/financials from a file, reconcile a year, check cash/receivables/tax owing, or produce financial statements for a Canadian small business.
---
# Klaai
Klaai is double-entry bookkeeping for Canadian small business. You operate it
through the `klaai` CLI; the human reviews your work in the web app.
## The one thing to understand
**Every transaction is a balanced journal entry.** Documents (invoices,
receipts, payments) are the user-facing records that *generate* those entries:
```
invoice → Dr Accounts Receivable Cr Revenue, Cr GST/HST Payable
payment → Dr Bank Cr Accounts Receivable
expense → Dr Expense, Dr ITC Cr Bank / Credit Card / Accounts Payable
transfer → Dr destination Cr source
```
You almost never post raw journal entries. Prefer the document command
(`invoices create`, `receipts create`, `invoices pay`) — it writes the document
*and* its entry, and keeps them in sync. Reach for `journal create` only for
things that aren't a document: opening balances, year-end adjustments,
corrections, interest, depreciation.
## Before you start
**First, make sure the `klaai` CLI is installed — if it's missing, install it
yourself. Never send the user to a terminal to do it.**
```bash
command -v klaai >/dev/null 2>&1 || npm install -g @buzzerboyinc/klaai-cli
klaai --version # confirm it's on the PATH
```
`klaai` is an npm package and needs **Node 18+**. If `npm` isn't found either,
install Node first (from https://nodejs.org, or the user's package manager —
e.g. `brew install node`, `apt install nodejs npm`) and retry. If a global
install is blocked by permissions, install Node through a version manager
(nvm or fnm) so `npm install -g` works without sudo.
The CLI connects to **www.klaai.co** by default. For a local or self-hosted
server, point it there first: `klaai config set-url <url>`.
Then check you're signed in:
```bash
klaai login status --json # confirm signed in and which company
```
If not signed in, **run the sign-in yourself**:
```bash
klaai login url --device
```
This opens a browser page and waits. Tell the user what to do in one sentence —
*"I've opened a sign-in page; click Approve and I'll carry on"* — and show them
the code it prints so they can check it matches the page. The command returns on
its own once they approve. It blocks for up to 10 minutes, so run it and wait
rather than polling yourself.
**Never** ask them to open a terminal, run a command, or paste a token into the
chat. `klaai login url` *without* `--device` stops and waits for a human to type
a code, so it will hang forever if you run it — that form is for people working
in a terminal themselves.
Then orient yourself. **Do this before recording anything**, because account IDs
are per-company and cannot be guessed:
```bash
klaai accounts list --json # ids, codes, subtypes, `funding` flag
klaai customers list --json
klaai vendors list --json
```
Match by `code` or `name`, then use the `id`. Never invent an ID like
`acc_1000` — that is demo data, not this company's chart.
## Always use `--json`
Every command takes `--json` and returns the raw API response. Use it for
everything you parse. Reserve plain output for showing the user a result.
## Recording work
```bash
# expense paid by card or bank
klaai receipts create --description "Microsoft 365" --amount 7.99 --tax 1.04 \
--category <expenseAccountId> --paid-from <cardOrBankAccountId> \
--vendor <vendorId> --date 2025-01-09 --by claude --json
# money coming back from a vendor
klaai receipts refund --description "Microsoft 365 refund" --amount 13.85 --tax 1.80 \
--category <expenseAccountId> --to <accountRefundedInto> --date 2025-06-02 --by claude --json
# a bill (owed, not yet paid): omit --paid-from → credits Accounts Payable
klaai receipts create --description "Rent — July" --amount 1800 --tax 234 \
--category <rentAccountId> --date 2025-07-01 --by claude --json
# money moved between your own accounts (incl. paying a credit card)
klaai receipts create --description "Visa payment" --amount 1756.28 \
--from <bankAccountId> --to <creditCardAccountId> --date 2025-01-02 --by claude --json
# invoice a customer, then record the money when it lands
klaai invoices create --customer <customerId> --date 2025-01-31 --terms net_15 \
--tax 802.14 --line "Consulting — January|<revenueAccountId>|6170.27" \
--notes "Source invoice 2024-118" --json
klaai invoices pay <invoiceId> --amount 6972.41 --account <bankAccountId> \
--date 2025-02-14 --method e-transfer --json
# the inbox: documents the user forwarded, waiting to be filed
klaai inbox check # pull new mail from the configured mailbox
klaai inbox list --json
klaai inbox process <emailId> --description "Bell — July" --amount 92.50 \
--tax 12.03 --category <expenseAccountId> --paid-from <bankAccountId> --json
```
`inbox check` is the ONLY way mail is fetched — there is no scheduler, so run it
when the user asks whether anything new has arrived. It is safe to re-run:
messages already imported are skipped.
Pass **`--by claude`** on `receipts create` and `inbox process` so the web app's
review queue shows who recorded it. Anything you record arrives as
`pending_review` — that is correct and intended. Do not approve your own work
unless the user explicitly asks.
## Filing documents against a record
Statements, signed contracts, anything worth keeping. Attach to an **account**,
`invoice`, `receipt`, `vendor`, `customer` or `journal_entry`.
```bash
klaai attachments list --for account:acc_1000 --json
klaai attachments add file_85adac --for account:acc_1000 \
--label "January 2026 statement" --period-start 2026-01-01 --period-end 2026-01-31
klaai attachments remove att_81b117
```
`add` takes a **file id that already exists** — typically one that arrived in the
inbox (`klaai inbox list --json` shows `file_id`). There is no upload command;
uploading is something the user does in the web app.
Set `--period-start`/`--period-end` on anything covering a period. A statement
without one is much harder to find later, and it's what makes "do we have all of
2026?" answerable.
`remove` deletes the underlying file too, unless another record still points at
it. Don't remove a document you didn't file.
## When you can't decide: leave a to-do
**Guessing puts a wrong number in someone's books. A to-do costs them ten
seconds.** When a document is genuinely ambiguous — you can't tell which account
it belongs to, whether a cost is billable, whether a charge is personal — record
the question instead of picking an answer.
```bash
# the common case: a receipt you can't confidently categorise
klaai todos create "R-1043 includes a \$400 chair — Office Supplies or a fixed asset?" \
--link receipt:rcp_03 --priority high
klaai todos create "Confirm the home-office proration — I used 15%" --due 2026-08-31
klaai todos list --json # open items (default)
klaai todos list --status all --json # includes resolved
klaai todos show tod_4f2a --json
klaai todos done tod_4f2a # only when the user has actually decided
```
`--link <type:id>` points the item at what it's about, so the user can click
straight through to it: `receipt`, `invoice`, `vendor`, `customer`, `account` or
`journal_entry`. Use it whenever the question is about a specific record.
`--priority` is `low | normal | high` (default `normal`); reserve `high` for
things that change a filing or a total.
Items you create are attributed to you automatically — no `--by` flag. They show
up in the web app under **To do** with a badge, which is where the user clears
them.
**Still record the transaction.** A to-do is a question about work you have
already done, not a substitute for doing it. File the receipt with your best
guess *and* flag the uncertainty — never leave the books empty pending an answer.
The exception is when you cannot proceed at all (no funding account exists, say);
then say so directly rather than only leaving a to-do the user might not read.
Don't resolve your own to-dos. `todos done` is for after the user has told you
the answer.
## Rules that will bite you
1. **Amounts are tax-exclusive.** `--amount` is the subtotal; `--tax` is the tax
in dollars, not a rate. A $226.00 receipt at 13% HST is
`--amount 200 --tax 26`.
2. **Invoices are only paid when a payment is recorded.** Setting a status does
not move cash. Use `invoices pay`.
3. **Never guess an account ID.** List accounts first (see above).
4. **Use a `funding` account for `--paid-from` / `--account`.** `klaai accounts
list --json` marks these — it includes credit cards and lines of credit, not
just banks.
5. **Journal entries must balance**, and the server enforces it. Debits = credits
to the cent, or you get `Out of balance: …`.
6. **Optional fields take `null`, never `""`.** Omit the flag instead.
7. **Amounts may be messy text.** `$1,234.56`, `-$799.00` and `(65.00)` all
parse. But a **negative amount is never a plain expense** — it's a refund
(`receipts refund`) or a card payment (`receipts create --from/--to`). The
CLI rejects negatives on `receipts create` and tells you which to use.
8. **Owner money in or out** goes to the *Due to / from Shareholder* account
(`shareholder_loan`): the company owes them when it's a credit balance, they
owe the company when it's a debit. Use `journal create`.
9. **Interest earned is revenue**, not a negative expense — post it to an
`interest_income` account.
10. **Ambiguity is not your call to make.** If two accounts are both defensible,
file it with your best guess and raise a to-do (see above). Silently
choosing one is how a misclassification survives to the tax return.
## Fiscal years
**A year stays editable until it is explicitly closed** — the calendar rolling
over changes nothing. So importing prior-year history just works: the record's
**date** decides which year it lands in.
```bash
klaai years list # which years are open or closed
klaai years close 2025 # make FY2025 read-only
klaai years reopen 2025 # let the accountant correct it
klaai reports trial-balance --fy 2025 # `--fy` is a read filter
```
Writing into a closed year returns *"Fiscal year 2025 is closed."* — tell the
user and ask before reopening; a closed year usually means it's been filed.
### HST method
Set **per fiscal year**: `quick` or `detailed`. This records how the books
account for HST — nothing more.
```bash
klaai years hst-method # every year, and where its method came from
klaai years hst-method 2026 quick # use the Quick Method from 2026 on
klaai years hst-method 2026 --reset # go back to inheriting
```
A year with no setting of its own **takes the most recent earlier year that has
one**, so a new fiscal year carries on however the last one was kept and this is
normally set once. A **closed year cannot be changed** — its books are read-only.
**Don't change it yourself.** An Administrator or Accountant decides which method
the books are on. If the figures look like the wrong one is set, raise a to-do
saying so rather than switching it.
## Checking the books
```bash
klaai dashboard --json # cash, A/R, tax owing, net income
klaai reports trial-balance --json # debits must equal credits
klaai reports balance-sheet --json # assets = liabilities + equity
klaai reports income-statement --json
klaai reports gst-hst --json # CRA lines 101/105/108/109
klaai reports gst-hst --from 2025-07-01 --to 2025-09-30 --json
klaai reports ar-aging --json # who owes, by bucket
klaai accounts ledger <accountId> --json # one account, running balance
```
After any batch of work, **verify the trial balance still balances** and report
the figure to the user. If `total_debit ≠ total_credit`, stop and investigate —
that is a bug, not a rounding artifact.
## Setting up a company's chart of accounts
Every company starts with a small standard chart. Add real bank, card and
investment accounts before importing:
```bash
klaai accounts subtypes --json # the allowed --type values
klaai accounts create --code 1005 --name "Business Chequing" --type cash_chequing \
--institution "Example Bank" --account-no 0000 --json
klaai accounts create --code 2210 --name "Business Visa" --type credit_card --json
klaai accounts create --code 1025 --name "GIC — 12 month" --type investment_gic --json
```
The **subtype decides the accounting type** — a `credit_card` is always a
liability. You never specify debits/credits when creating an account.
Five accounts are tagged `system_role` (`ar`, `ap`, `tax_collected`, `tax_paid`,
`default_bank`) and are used automatically when posting. Don't delete or retype
them.
## Errors
Messages are written to be read. Common ones:
| Message | What to do |
|---|---|
| `Out of balance: debits X ≠ credits Y` | Fix the postings; don't retry unchanged |
| `Fiscal year YYYY is closed` | Ask the user before `klaai years reopen YYYY` |
| `Amount must be positive` | It's a refund or a card payment — see rule 7 |
| `Can't read "…" as an amount` | Clean the value; don't guess at it |
| `That overpays the document…` | Check the outstanding balance first |
| `Can't delete … N posting(s) reference it` | Deliberate guard — archive instead |
| `Not signed in` / `token has expired` | Ask the user to run `klaai login url` |
| `No company selected` | `klaai companies list` then `klaai companies use <id>` |
Surface the message to the user rather than working around it. These guards
exist to protect the ledger.
## Working across companies
```bash
klaai companies list --json
klaai companies use <companyId> # persists in ~/.klaai/config.json
klaai invoices list --company <id> --json # or override per command
```
## Importing books from a file
If the user points you at a spreadsheet, export or statement — *"load my
books from this file"* — **read `references/bulk-import.md` first and follow
it**. Do not start posting transactions from a file without it.
The short version: **profile the file, present a plan, get one confirmation,
then load.** Never ask the user to describe their file up front — work it out
yourself and confirm your reading of it.
### Reading the file
Pick the tool from what's actually installed rather than assuming:
```bash
python3 -c "import openpyxl, sys; print(openpyxl.__version__)" # .xlsx/.xlsm
python3 -c "import pandas; print(pandas.__version__)" # optional
csvkit --version 2>/dev/null; ssconvert --version 2>/dev/null # fallbacks
```
- **.xlsx / .xlsm** → `openpyxl` with `load_workbook(path, data_only=True)`.
`data_only=True` is essential: it returns cached *values* instead of formula
strings. If openpyxl is missing, install it (`pip3 install --user openpyxl`)
or convert to CSV with LibreOffice/`ssconvert`.
- **.csv / .tsv** → read directly; sniff the delimiter and encoding.
- **.pdf statements** → extract text first; if the layout won't parse cleanly,
say so rather than guessing at columns.
Warnings like *"Conditional Formatting extension is not supported"* are noise —
ignore them.
Read the file **once** into memory and work from that. Don't re-open it per row.