Mailroom Reloaded
Agentic document pipeline · deployable rebuild
At a glance
2 LLM calls for a clean text-layer document (sort, then extract)
1,597 tests passed, 3 skipped, in the main-branch verification run (the suite has 1,134 test functions in 83 files by grep)
The problem
My Digital Mailroom design routes incoming documents: decide what each one is, pull out the fields that matter, and file it with a record of what happened. Agent frameworks make that easy to prototype and hard to run. If the model decides what happens next, you cannot say in advance how many calls a document will cost, where a crash will leave it, or why it ended up in a given folder.
Mailroom Reloaded is a rebuild of that design as a single deployable service, built around one rule: code owns the control flow, and models are called at fixed points.
Approach
The pipeline is a CrewAI Flow, but a deterministic driver walks it, not CrewAI’s event engine. A clean text document goes through local ingest, the ModernBERT fast path from Mailroom-ML, one sorter call, a gate, one specialist extraction call, a second gate, and then report, catalog and archive. The gates are plain threshold code with zero LLM calls. That is why the cost of a clean document is exactly two calls.
When a gate is not satisfied, fixed rules pick the next step, and the two gates have different branches:
- Classify gate: re-sort once, as a full pass, if the fast path and the sorter disagree about the class; retry a low-confidence sort, up to a configured limit; after the retries, park the document for human review.
- Extract gate: send a middling-confidence result to verification, where a judge grades the extraction and an arbiter decides whether to accept, re-extract or escalate; retry a low-confidence extraction, up to the limit; after the retries, escalate to a boss agent that can reassign the class or send the document to human review. An extraction that is invalid or cut off by the length limit is retried, then parked for human review.
The ModernBERT fast path runs first and fails open. If the package or model is missing, the input is oversized, or anything errors, the pipeline falls back to a full LLM sort instead of stopping.
What I built
- Durability. Documents move between bin folders (inbox, processing, review, failed, archive) by atomic rename, with a JSON manifest per document. Every completed node is checkpointed, so a crashed watcher resumes from the last finished node. A SQLite catalog and a hash-chained audit log record what happened; a second chain covers every run, with optional off-host anchoring.
- Observability. OpenTelemetry traces go to Phoenix, metrics to Prometheus, and provisioned Grafana dashboards (Pipeline, Serving & GPU, Quality) sit on top. Spans carry metadata, not document text.
- Two consoles.
/uiis a plain JavaScript runs page./tuiis a browser terminal with a replay viewer that scrubs through a recorded run station by station, shows retries and escalations, and verifies the ledger chain. - Deployment. One Docker Compose file with profiles for a local llamafile sidecar, a local vLLM server with GPU metrics, and a split watcher. Providers are switchable: a mock for tests, OpenRouter, vLLM, or llamafile. Gmail intake is an optional extra.
- An opt-in decision model. “Jev” is a calibrated, probabilistic decision model, not a chat model: it answers typed questions (a choice, a yes-probability, a score). When enabled and calibrated, it may overrule the gate only inside the medium-confidence band, never a hard rule. It is off by default and the pipeline is unchanged without it.
- An evaluation harness.
mailroom evalsamples a pinned dataset revision with a fixed seed, keeps ground truth away from the pipeline, and fits any learned gate on the training split only.
Results
I have no accuracy or cost numbers to publish for this service yet. The harness exists and is tested, but I have not committed evaluation results, so I am not quoting any. What I can show is that the machinery runs end to end: the main-branch verification ran four seeded documents through the stack with a mock provider and all four archived, and eleven browser checks of the replay viewer passed.



Heads up. The screenshots come from a demo run on a mock LLM provider with seeded documents, so the counts and costs in them describe the demo, not model quality.
What I learned
- Fixing the number of model calls at design time made everything else easier to reason about: cost, resume points, and what to log.
- Fail-open fast paths need to be tested as hard as the fast path itself.
- A hash chain on one disk detects edits, reordering and middle deletions, but not truncation of the tail. The docs say so, and the optional anchor exists for that reason.
- A replay viewer paid for itself quickly. Seeing a retry on a timeline beat reading logs.
Stack
Python CrewAI Flows FastAPI SQLite ModernBERT OpenTelemetry Phoenix Prometheus Grafana Docker Compose vLLM pytest