A guess dressed as a decision
A classifier that must always answer will place an unreadable filename somewhere. A client stops being chased for a document nobody actually confirmed they sent -- and nothing anywhere reports the gap.
EOFY document chasing for accounting firms
Docket reads a filename, decides what it can safely conclude, and leaves the rest open rather than guessing. What it cannot place stays unplaced โ it is never marked satisfied on no evidence, and the CRM is never told a client was chased until a real letter exists.
The blind spot
Most document-chasing automation is built to always produce an answer. The failure mode is not that it is slow -- it is that a confident wrong answer stops the next person from looking.
A classifier that must always answer will place an unreadable filename somewhere. A client stops being chased for a document nobody actually confirmed they sent -- and nothing anywhere reports the gap.
A chase letter that re-asks for a document already on file reads as carelessness to the one person -- the client -- whose patience the whole exercise depends on.
If "chased" is written the moment a job starts rather than after the letter actually sends, a refused draft leaves the CRM asserting a contact that never happened -- and nobody follows up, because the record already says it is done.
How it works
Every step either produces a real answer or an honest refusal. Nothing in between -- there is no step that outputs a plausible guess.
Filename rules answer first, for free. Only what they cannot read is sent to a model -- and what the model also cannot place stays unplaced, not guessed.
docket/classify_rules.py
docket/classify_model.py
Each required document type is checked against what this client actually sent, in the right financial year. Satisfaction is a lookup, never an inference.
docket/requirements.py
docket/rules.py
The chase letter is built only from what is still missing. A draft naming an already-satisfied document, or stating a figure, is refused rather than sent.
docket/letter.py
The CRM is marked "chased" only once that exact letter exists. A refused draft leaves the record exactly as it was -- a blank that gets checked, not a lie that looks finished.
docket/crm.py
Try it
Pick a real filename from Docket's own test corpus. Compare what a classifier forced to always answer would do against what Docket actually does.
The name states what it is. Filed as a bank statement.
confidence not applicable -- rules matched
Same result -- the filename genuinely says what it is. Tier 1 handled it for free.
docket.classify_filename() -> ('bank_statement', 'Westpac')
What this changes on the client's chase letter
Both classifications above were run for real against Docket's code on 2026-09-19, one fictional client, no live model call. The letter and CRM panels reflect the outcome of that same run.
The rules
Three rules decide whether a document counts, and every one of them has a plain-English statement a partner can check without reading code.
A document uploaded outside the financial year being checked does not satisfy that year's requirement. Last year's bank statement is a real document that still leaves this year's requirement open.
A document the firm generated -- a tax notice, a remittance advice -- never appears on a client's list of things to send. Asking a client for a document the firm wrote is the mistake this rule exists to prevent.
A requirement is met only by a document of exactly that type, sent by the client, inside the year. An unclassified document -- Docket does not know what it is -- satisfies nothing.
| Requirement | Status |
|---|---|
| Bank statement | Satisfied |
| Payslip | Missing |
| Invoice | Missing |
| Receipt | Missing |
| Contract | Missing |
| Scan0001.pdf | Unclassified -- counted, not guessed |
Only the bank statement is satisfied. The letter this client receives asks for the other four -- never for the bank statement, and never for a fifth item invented to fill the unclassified document's gap.
What you get
Written to be read by whoever signs off on the EOFY chase, not only by whoever built the pipeline.
Built only from what is genuinely missing. Refused outright rather than sent if it would name something already on file or state a figure.
Answers: what does this client still owe?
Written from the letter that actually exists, never before it. A refusal leaves the account reading "not chased" -- true, not softened.
Answers: did this client actually get chased?
How many documents rules handled for free, how many needed the model, and how many stayed unclassified -- a measured split, not a claim.
Answers: how much of this cost anything?
Every document a filename could not settle and a model could not either is counted, not silently dropped or forced into a category.
Answers: what does nobody actually know yet?
Every result above ran against a synthetic corpus and a Salesforce sandbox. Wiring it to a real firm's inbox and CRM is the next step, and it hasn't happened yet -- this page says so on purpose.