Skip to main content

Upload to posted journal entry

The upload path preserves one evidence identity, derives one reviewable structured result and, after two human decisions, posts one accounting fact. The stages below name the artifact, its location and its job, then state what that stage refuses.

1. Land the original bytes

Artifact, location, job. The artifact is the original byte sequence; it lives in the private documents object-storage bucket at raw-intake/sha256/<first two hex>/<sha256>; its job is byte-exact evidence. RawIntakeLane.land hashes the bytes, writes the content-addressed object, reads it back and requires both byte equality and SHA-256 equality before extraction can start. The source states the boundary plainly: “Original customer bytes live in the existing private documents object-storage bucket and are addressed by SHA-256.” The landing stage refuses an empty document, an invalid MIME type, an unexpected object reference and any round-trip byte or digest mismatch. Equal bytes have one storage identity even when their filenames differ; the filename remains metadata.

2. Record the evidence identity

Artifact, location, job. The artifact is a file_uploads row in Postgres; its location is the verified organization’s upload store; its job is to record the exact bucket, object_path, source_storage_reference, source_sha256, original filename, MIME type, size and uploader for the stored evidence. The upload endpoint derives the organization and actor from its verified DiscoveryPrincipal, inserts the row, then returns the database id, storage linkage and a server-minted upload_authority. The authority token binds the organization, verified principal, server session, file id, SHA-256 and storage reference. The Clerk adoption path rechecks that binding, row ownership, storage reference, digest and the attachment bytes; it refuses a missing, expired, cross-user, cross-session, cross-organization or altered capability. The browser cannot establish identity by sending an actor name of its own.
At the web and backend origin/main revisions named on Technical flows and authority, this browser hop is not fully connected. The upload API returns upload_authority, but worthstate-ui/src/lib/multimodal-utils.ts keeps only file_upload_id, bucket and object_path in attachment metadata. agents/clerk/files.py requires the missing authority to adopt that persisted row, so the browser upload-to-Clerk adoption chain is untraced at this boundary rather than claimed as working. The Clerk’s direct attachment fallback can create and authorize its own row, but that is a different source path.

3. Extract without replacing the evidence

Artifact, location, job. The artifact is the Clerk graph state containing extracted fields, lines and source provenance; its location is the durable Clerk run until the terminal queue write; its job is to turn the document into typed candidates while retaining the original storage reference and digest. The graph runs ingest → classify → load_registry → sop_retrieve → extract before mapping. The verified submitter is stamped by the server-side web proxy on the Clerk run; source_file_id links the run and eventual queue row back to file_uploads. The Clerk refuses to guess an unknown or low-confidence document type. It can attempt at most two shape-only repairs, and it does not silently repair semantic doubts such as an unknown master, an impossible date or a suspected duplicate. No raw Markdown artifact is written here. If the optional Clerk retrieval store is configured, CogneeExtractedTextStore publishes a .txt document containing extracted text and the original storage lineage to a separate Clerk-only Cognee dataset. That is retrieval material, not evidence and not the structured result awaiting approval.

4. Resolve against Teable masters

Artifact, location, job. The artifact is a set of projected resolution results inside the Clerk state and later intake_queue.payload.resolutions; its mapping source is existing Teable master rows; its job is to attach known master record ids and display values to extracted names or codes. Teable is the mapping target, not the document store: the PDF, parsed fields and queue row do not land there. KnowledgeBase.masters.resolve_all is read-only. The Clerk refuses to create a counterparty or account to make a document fit, flags ambiguous or missing applicable masters, and treats a master store it cannot read as unverified rather than as a clean match. Only document types with an applicable bound master table receive this check; “not applicable” is not reported as a successful resolution.

5. Stage the structured result

Artifact, location, job. The artifact is intake_queue.payload; its location is the intake_queue table’s JSONB column in Postgres; its job is the structured result awaiting human acceptance. The terminal projection contains fields, lines, projected resolutions and source_provenance, while source_file_id links the row to the evidence record and submitter and drafted_by preserve who filed and who staged it. The queue store first opens the row as received, then commits the complete terminal projection once as pending or flagged. The queue refuses to auto-merge a suspected duplicate. A duplicate becomes a visible flag, an unclassifiable document stays reviewable as uncertainty, and a natural-reference collision cannot quietly create a second active claim. This row is not a draft journal entry and it cannot post itself.

6. Pass the intake gate

Artifact, location, job. The artifact is the accepted queue row with status='active' plus an intake_events audit event; its location is Postgres; its job is to record that a human accepted this structured document for the Bookkeeper to propose accounting. The acting identity comes from the verified DevCenter request, not a browser-supplied actor header. The intake service locks the row and its duplicate group. It refuses a row outside the reviewable states, a duplicate group that already has an accepted or posted member, and an approver who is the submitter, drafter or person who last resubmitted edited fields. Rejecting ends this path without writing money. Approval and launch intent commit together: an intake_launch_outbox row is written in the same transaction as status='active'. That outbox artifact lives in Postgres and its job is to make the Bookkeeper handoff retryable and observable as launched, deferred, failed or abandoned. An approved row can therefore honestly be “approved, but posting has not started”; intake approval is not posting.

7. Build a proposal and stop

Artifact, location, job. The artifact is the Bookkeeper’s balanced proposed journal lines; its location is the durable LangGraph checkpoint for the deterministic intake thread; its job is to show a human exactly what the posting rules derived from the active queue row. The Bookkeeper re-reads intake_queue.payload, reads master and chart-of-accounts data without writing them, and applies deterministic posting rules. The proposal stage refuses a non-active row, a missing organization base currency, an unmapped account or amount, an unavailable posting rule and an unbalanced result. There is no suspense-account fallback. The graph compiles with interrupt_before=['post'], so reaching this checkpoint cannot create a journal entry.

8. Pass the posting gate

Artifact, location, job. The artifact is a server-signed posting mandate; it is carried to the resumed Bookkeeper run and verified against server authority; its job is to bind the organization, intake record, checker and approve-or-reject verdict that may release the writer. The posting checker must be a named human who is not the submitter, drafter, resubmitter or intake approver. An agent identity cannot be the checker. The source warning is exact: “THE CHECKPOINT DECIDES NOTHING.” At post time the Bookkeeper verifies the mandate, re-reads the active queue row and re-derives the lines from that trusted record. It refuses substituted proposal or committed lines, a changed record, a forged outcome, a checkpoint that contradicts the mandate and a row that has already left active. A rejection records no ledger write.

9. Post once through the single writer

Artifact, location, job. The artifact is one posted journal_entries row, its journal_entry_lines, its hash-chained event_stream event and the queue’s result_entry_id; their location is the canonical Postgres ledger; their job is the immutable accounting fact and its source lineage. ledger_bridge.post_and_mark calls LedgerService.post_entry, performs applicable subledger writes, changes the intake row from active to posted and stamps result_entry_id in one database transaction. The LedgerService source calls itself “The single writer” and says it will “Validate the 18 invariants, then post an immutable, hash-chained entry.” It refuses fewer than two lines, two-sided or negative lines, imbalance, unknown or inactive accounts, unresolved currency or FX, missing required dimensions, a closed period, a conflicting idempotency replay and any other GL invariant failure. If the queue row cannot be marked from active to posted, the transaction rolls back instead of leaving a journal entry without its intake link.

Verified at source

The upload and evidence stages are in langgraph_chat/devcenter/uploads_api.py, agents/clerk/raw_intake.py, agents/clerk/upload_authority.py and agents/clerk/files.py. The browser identity and known attachment-metadata boundary are in worthstate-ui/src/app/api/[..._path]/route.ts, src/lib/langgraph-passthrough-guard.ts, src/lib/session-identity.ts and src/lib/multimodal-utils.ts. Extraction, mapping and queue staging are in agents/clerk/graph.py, agents/clerk/queue.py, agents/common/kb.py and data_entry/resolver.py. The gates and durable launch are in devcenter/intake_api.py, devcenter/intake_service.py, devcenter/intake_validation.py, agents/bookkeeper/graph.py and agents/bookkeeper/post_auth.py. The final write is in agents/bookkeeper/intake_source.py, agents/bookkeeper/ledger_bridge.py and backend/app/services/ledger_service.py.