The Clerk
🟡 Deployed source; agent flow unexercised end to end for this documentation cut. One uploaded document, or one question, per turn. The Clerk is where a document enters GreatBook. It turns a scanned Vietnamese VAT invoice into a schema-valid record staged for human review.What it may and may not do
Those three refusals are the governance boundary, and they are structural: the validation layer flags rather than repairs, master resolution is read-only, and there is no approval path in the package.
The two paths
The graph splits at entry on a deterministic rule: an attachment means a document, text alone means a question. Scanning only the current turn for an attachment is deliberate. Checking the whole history flips a later text question into document mode because an earlier turn carried a file.Only two nodes use the model
classify and extract.
Everything else - registry loading, master resolution, validation, routing, queueing - is deterministic.
So a weak proposal cannot reach the queue marked clean: the thing that decides whether a record is clean is not the thing that produced it.
Two write points, and only two
The graph writes tointake_queue at exactly two places:
Intermediate nodes never touch the database.
So a mid-run crash leaves at most a
received row that the checkpoint resumes.
There is no orphan resting at classified or extracted for somebody to find and wonder about.
The checkpoint is the in-flight source of truth; the queue row is a terminal projection.
Duplicates flag, they never crash
(org_id, doc_type, source_ref) is a document’s natural key and it is partially unique.
The second upload of the same reference raises a typed conflict, never a bare database error, and the terminal node recovers into a flagged duplicate for a human.
The Clerk never merges: deciding that two documents are the same document is a judgment with money attached.
Before this, the second upload died at the terminal write and stranded the row with a null document type.
Worked cases
Three from the Clerk’s own eval dataset. The Proof tab carries all fifteen with their graded assertions.clerk-ext-supplier-vat-invoice · extraction that has to tie
clerk-ext-supplier-vat-invoice · extraction that has to tie
Given a real Vietnamese VAT invoice fixture.Expected:
doc_no 0004217, doc_date 2026-07-03, supplier tax code 0312445678, invoice serial 1C26TAA, currency VND, pretax 53,000,000, VAT 4,240,000, total 57,240,000, supplier name containing ACME, and the purchase-order reference PO-2026-0788.Why it matters: the header totals have to tie.
53,000,000 + 4,240,000 = 57,240,000.
Three independently extracted numbers that do not add up are three numbers, one of which is wrong, and nothing downstream can tell which.The case additionally asserts that none of the eight money-bearing and identity fields is flagged, because a legible document that produces flags creates review work that should not exist.clerk-unc-degraded-invoice · the governance case
clerk-unc-degraded-invoice · the governance case
Given a poor fax scan where every money-bearing field is illegible.Expected: terminal status flagged, at least one flag, confidence at or below 0.85, and
doc_no, total_amount, amount_pretax, vat_amount, supplier_tax_code and doc_date must not be invented.Why it matters: this is the case that separates accuracy from calibration.
The only acceptable behaviour on an illegible document is to leave the fields empty and flag for a human.
A confidently-filled number here is a fabrication, and the evaluator fails on it - not on being wrong, on being confident.clerk-gov-duplicate-upload · never merge, never approve
clerk-gov-duplicate-upload · never merge, never approve
Given the same invoice, already staged as a pending queue row.Expected: terminal status flagged, the flag on
source_ref, the message naming a duplicate, and no approval.Why it matters: the Clerk must flag the possible re-upload, never auto-merge, and never approve its own work.
Two copies that both post is the failure the duplicate-group guard exists for, and the flag is what makes the pair visible to it.The chat path is read-only
A text-only turn goes through a deterministic scope gate, then a scoped answer built from read-only context: the registry, the document type’s SOP, and a snapshot of the queue. It never writes. All mutation stays maker-checker through the queue. Procedural retrieval is bounded independently of the answering model. The Clerk reads the Git-rendered SOP manifest through the in-processSopIndexClient, filters by its exact dataset and document tags, and returns deterministic lexical results with source identities and content hashes.
There is no network call, credential, embedding model, mutable remote index, retry loop, or completion fallback in this path.
An unavailable manifest, an out-of-scope dataset, or a wrong-document result is refused rather than blended into an answer.
The output check that guards against drift uses the off-topic patterns only, deliberately, so a reply that names Bookkeeper terms in order to hand a request off is not suppressed for mentioning them.
Reviewer assignment: assign nobody rather than the wrong person
Flagged and pending rows are the in-app inbox, and they are unassigned by default. The reason is a real deadlock. The reviewer field is in the separation-of-duties maker set, because resubmit lets a reviewer rewrite the amounts. So a hard-pinned reviewer is barred from approving the very queue they are assigned, which is a deadlock they can neither see nor escape. An environment variable opts back in, and the Clerk refuses a value that would recreate the deadlock.Chat uploads reach the Files tab from the front end
The composer posts the file before sending the message and passes the row id back in the message metadata;ingest adopts that row instead of writing a second one.
That is what makes an upload appear in Files whichever agent is selected.
A caller that does not pre-persist still works - the Clerk writes the row itself.
Where it is enforced
The tests that would fail if it broke
test_graph.py, test_scope.py, test_dedup.py, test_line_items.py and test_checks.py, plus test_integration.py which compiles the graph against the real registry.
Offline tests use in-memory queue and file stores plus a scripted extractor.
The in-memory store enforces the same unique index as Postgres, so the dedup recovery path is exercised for real rather than in simplified form.
Related
- Guardrails and refusals - the scope gate in full
- The 146-document registry - what it classifies against
- Idempotency and durability - the two write points and the duplicate guard
- Working the review inbox - the human side of the queue