Essay 15 min read
Why document collection is the perfect agentic AI workflow (the chase loop)
Mortgage files, insurance claims, and tax prep reduce to the same chase loop. I built an agentic AI workflow to run it: the agent owns the loop, a human in the loop owns the gates, and autonomy is earned gate by gate.
If you have ever bought a house, you know the week I mean. Monday, an email: “we need your last 30 days of pay stubs, 2 months of bank statements, and a copy of your ID.” Tuesday, you send four attachments from your phone. Wednesday: “the bank statement is missing page 2, and we need all accounts.” Thursday, you upload to a portal that says “pending review” and keeps saying it for nine days. Friday, a different person asks for the pay stubs again, because the first request lived in someone’s inbox and the inbox does not know what arrived.
Meanwhile a loan officer with forty files open is doing the other side of this: re-typing the same request for the fifth time, opening a PDF to check whether it is actually a bank statement, and keeping the real status of every file in their head.
Nobody in this story is stupid and nobody is lazy. The process is miserable because the work is a chase loop, and a chase loop is a kind of work humans are bad at and software has historically refused to do. It is also, I think, close to a perfect problem for an AI agent. I spent the last few weeks building one for mortgage document collection (Dossi), and this post is about the problem’s shape, why “agentic” is the right tool for it, and the design decisions that made it work. Including the one that sounds like heresy: most of the agent is not an LLM.
The anatomy of a chase loop
Mortgage conditions, insurance claims, tax prep, KYC, vendor onboarding: on paper these are different industries, but the work reduces to the same four properties.
The requirements are conditional. There is no fixed checklist. A W-2 borrower owes pay stubs and W-2s; a self-employed borrower owes two years of business returns and a year-to-date P&L; gift funds add a signed letter from a donor; a large deposit adds a letter of explanation. The list is a function of the applicant’s situation, which is why every file starts with a human assembling a bespoke checklist, and why the checklist is wrong by Wednesday.
The chase is multi-party. The borrower is only one target. The gift letter comes from a donor who has never heard of your company. The verification of employment comes from an HR department. Every extra party is another thread someone has to hold.
Every document needs judgment. “Received” is not “done.” Is the pay stub actually within 30 days? Is the ID expired? Is page 2 of 4 missing? Does the name match the borrower? Today a trained human eyeballs every upload, which is exactly the kind of high-volume, low-novelty judgment that queues up behind everything else.
The channels are stateless. This is the one that actually causes the misery. Email has no memory: it does not know what is outstanding, what arrived, or what was rejected and why. Portals have state but no agency: they hold a beautiful list of red X marks and wait. Nothing in the stack drives. So the loop gets driven the only way left, by a person nagging, and the true state of the file lives in nobody’s head and every inbox.
Why this is the perfect agentic workload
The demos that sell agents are the wrong workloads. “Book me a flight” is a task where the human is present, latency matters, the action space is huge, and a mistake is instantly visible and instantly annoying. Document collection is the opposite on every axis, and every one of those inversions favors an agent:
- It runs for weeks. A loan file is open for 30 to 45 days. No human wants a job whose core skill is remembering to nag on day 12. An agent does not get bored on day 12.
- It tolerates latency. Nobody expects a response in 800 milliseconds. If the agent takes two minutes to verify a document, that is roughly 10,000 times faster than the status quo.
- The action space is tiny. Request, remind, verify, escalate. That is nearly the whole vocabulary. Small action spaces are where agents are reliable today.
- The outcome is verifiable. A file is complete or it is not. Each condition is cleared or it is not. There is no vibes-based definition of done, which means you can actually measure the agent.
- The stakes are separable. Thousands of low-stakes decisions (is it time to send a reminder?) and a few high-stakes ones (is this document acceptable? should this email go out?). You can gate the second kind without slowing down the first.
That last property is the important one, because it points at the architecture.
What “semi-agentic” actually means
When I started, I had to confront a question that sounds like it undermines the whole project: the core loop of Dossi is deterministic code. A state machine decides when to follow up, what is overdue, and what happens next. Doesn’t that defeat the purpose of an AI agent?
No, and I want to argue the opposite: it is what makes the system genuinely agentic.
So the spectrum I actually design against has three points:
- Workflow. Software stores state; a human drives every transition. This is every portal you have ever hated.
- Semi-agentic. The agent drives: it initiates requests, follows up on its own schedule, verifies what arrives. Humans hold specific gates: irreversible or high-stakes actions require a person. This is where Dossi lives today.
- Fully agentic. The agent runs start to finish and a human sees only exceptions.
The crucial claim: moving from 2 to 3 is not a rewrite. It is the same architecture with gates removed one at a time as the agent earns trust. If your semi-agentic system needs a re-architecture to become fully agentic, it was not semi-agentic; it was a workflow with an LLM garnish.
What I built
Concretely, Dossi works like this. A loan officer fills in a short intake form: loan type, borrowers, employment types, a few flags like gift funds. A versioned requirements template (JSON, per lender and loan type) is evaluated against that profile and produces the concrete list of conditions. The borrower gets one link, scoped by a signed expiring token, with plain-language upload slots. From that moment a per-loan agent owns the file: it sends the initial request, wakes up on a schedule to chase whatever is still open, verifies every upload against the lender’s criteria, and surfaces only exceptions to the loan officer, who watches everything on a timeline instead of an inbox.
The stack is Cloudflare end to end: one Durable Object per loan as the agent’s home, D1 for the cross-loan dashboard, R2 for documents, and a queue between upload and verification. But the stack is not the interesting part. The design decisions are.
Decision 1: the model lives at three seams, and nowhere else
The whole system makes exactly three kinds of LLM calls, each behind a named function:
parseCondition: turn a messy underwriter stipulation (“2024 K-1 for all businesses owned >25%”) into a structured condition with a plain-language ask.draftMessage: write the actual email to the borrower, warm and specific, listing exactly what is outstanding. The model writes the words. It never decides whether to send.verifyDocument: judge an uploaded document against the lender’s criteria.
Everything else is code. The chase loop itself is a Durable Object alarm, and the comment in the source is the design principle:
// Chase-loop cadence
// - FOLLOWUP_INTERVAL_MS: how long an open condition may sit untouched
// before the loop drafts a follow-up.
// - CHASE_ALARM_INTERVAL_MS: how often the alarm wakes while work remains.
// - MAX_FOLLOWUP_ROUNDS: stop nudging after this many rounds.
// The DECISION to chase is made here in code (free); the LLM only drafts
// the words for the conditions code already selected.
const DEFAULT_FOLLOWUP_INTERVAL_MS = 2 * 24 * 60 * 60 * 1000; // 2 days
const DEFAULT_CHASE_ALARM_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours
const DEFAULT_MAX_FOLLOWUP_ROUNDS = 5;
The economics guardrail I wrote into the build instructions: if token cost scales with loans times ticks, you have put the LLM in the loop, and you should pull it back out. The alarm fires every 12 hours for every open loan forever; that path has to cost zero. The LLM runs on events (a stip arrives, a message is due, a document lands), which scale with actual work.
The requirements themselves are data, not prompts and not code:
{
"code": "paystub_30d",
"label": "Most recent 30 days of pay stubs",
"perBorrower": true,
"appliesWhen": { "employmentType": "w2" },
"validity": { "type": "rolling_window", "days": 30 },
"verify": { "docType": "paystub", "windowDays": 30 }
},
{
"code": "gift_letter",
"label": "Signed gift letter from the donor",
"appliesWhen": { "hasGiftFunds": true },
"target": "third_party",
"verify": { "docType": "gift_letter", "mustBeSigned": true }
}
A versioned template per lender and loan type, evaluated by a pure function with unit tests. This is what makes the conditional-requirements problem boring: the branching that humans get wrong under load is a deterministic evaluation, and the same engine forks to an insurance claim or a tax intake by authoring a new template, not by redeploying an agent.
Decision 2: the action log is the agent
Each loan is one Durable Object: single-writer, strongly consistent, with its own SQLite. Inside it, every event appends to one table:
CREATE TABLE IF NOT EXISTS actions (
action_id INTEGER PRIMARY KEY AUTOINCREMENT,
condition_id TEXT,
type TEXT NOT NULL, -- doc_received, followup_sent, flagged, ...
actor TEXT NOT NULL, -- 'agent' | 'borrower' | 'lo'
detail_json TEXT,
created_at INTEGER
);
Rows are never updated and never deleted, and this one boring table is doing three jobs at once:
- Agent memory. “How many follow-up rounds have I sent?” is a query, not a variable that can drift. The agent’s restraint (stop nudging after 5 rounds) is enforced by its own history.
- Audit trail. In a regulated domain, “the agent decided to email your client” is not an acceptable answer to “why?” Every action carries its actor and its reason, forever.
- The UI. The timeline both the loan officer and the borrower see is a straight render of this table. There is no separate “activity feed” to keep in sync, so the humans see exactly what the agent remembers.
That last point is a trust mechanism disguised as a schema decision. People extend autonomy to things they can watch. An agent whose memory is its user interface cannot quietly do something off the books.
Decision 3: the trust dial
Dossi is semi-agentic on purpose, and the gates are explicit:
A human approves the outbound. The agent generates conditions and drafts the first email, then stops. The loan officer reviews and hits send. Follow-ups then run autonomously, inside the cadence and round caps above. The gate sits at the first irreversible, outward-facing action.
Verification fails toward humans. Document verification is two stages. A cheap Workers AI vision model does a pre-screen (“is this legible and plausibly a pay stub?”) that fails open: any error and the document proceeds, because a cost optimization must never become a gate. Then a Claude judge reads the document against the lender’s criteria and returns a structured verdict:
export interface VerifyResult {
verdict: "pass" | "fail" | "needs_human";
confidence: "high" | "low";
reason: string;
}
// Deterministic fail-safe: no API key or any error routes to
// needs_human so a human always sees it.
const NEEDS_HUMAN: VerifyResult = {
verdict: "needs_human", confidence: "low", reason: ""
};
needs_human is the load-bearing design element. The judge is explicitly allowed to not know, and every failure mode in the pipeline (missing key, malformed output, thrown exception) collapses to the same safe verdict. A clean fail goes back to the borrower automatically with a warm, specific reason (“this statement covers March, we need the last 60 days”). Anything ambiguous becomes a flag in the loan officer’s exception queue. The human never re-reviews the agent’s passes; they see the escalations.
The point of building it this way is that “fully agentic” stops being a leap of faith and becomes a sequence of small, reversible policy changes. Auto-send for loans under some threshold. Auto-clear for high-confidence passes on low-risk document types. Each gate removal is measurable against the action log, and each is a one-line change, because the loop was built to run without the human from day one. The human was always a gate, never the engine.
Two smaller decisions earn a mention. Borrowers never get accounts: the upload link carries a signed, expiring token scoped to exactly the conditions it may touch, because the biggest failure mode of portals is that the person you are chasing will not log in. And because templates are data in KV, wave two (parsing free-text underwriter stips with parseCondition) lands in the same condition store; the state machine neither knows nor cares where a condition came from.
Why an app, not a memo
One more decision worth defending: this exists as a working product instead of a pitch document, and that was the cheapest choice available, not the expensive one.
The traditional way to advance an idea like this is a long document. A requirements PDF, a slide deck, an “AI transformation roadmap” that everyone nods at and nobody can evaluate. Documents about agents are especially weak, because the entire question is behavioral: does the follow-up email read like a human wrote it? What happens when someone uploads a photo of a screen? You cannot settle that in prose. A loan officer watching an expired ID get flagged with a warm, specific rejection reason learns more in ninety seconds than a 30-page spec can teach. The demo ends arguments the document can only start.
What changed is that the demo now costs less than the document. Dossi went from first commit to a deployed product in nine days: the Durable Object backend, both UI surfaces, the two-stage verification pipeline, multi-tenant auth, the marketing site, and a rendered demo video, built with Claude Code driving and me directing. And I should be honest about what “nine days” means, because it is not nine days of focused work. I have a day job and two kids. My build window opens on weekday nights and weekends, if bedtime goes well, which means mostly after 10pm. Sixty-two commits from the quietest hours of the house. A working, deployed, multi-tenant product used to be out of reach for that schedule. Writing a convincing spec for all of it, with mockups and stakeholder rounds, would honestly have taken longer. When the working system is cheaper than the artifact describing it, shipping the system is not ambition. It is the lazy option, and I mean that as praise.
This inverts how ideas like this get evaluated. You should be suspicious of anyone selling an agent strategy in a deck, because the deck is now the expensive, low-information option.
The pattern, without the mortgage
Strip the domain words away and the shape is: checklist + chase + verify, running for weeks, across parties who do not share a system. That is an insurance claim (adjuster chasing photos, police reports, contractor estimates). It is tax season (an accountant chasing 40 clients for 1099s and K-1s). It is KYC, clinical trial onboarding, SOC 2 evidence collection, immigration paperwork.
Every one of these is staffed today by a person whose real job title should be “the loop,” and every one has the same properties that make it agent-shaped: long-running, latency-tolerant, tiny action space, verifiable outcome, separable stakes. If you are looking for a place to apply agents in 2026 that is not a demo, look for the chase loops in your industry. Then build the boring version: procedure in the loop, judgment at the seams, humans at the gates, and an append-only log that lets the agent earn the right to run alone.
Dossi is live at dossidocs.com, demo video included. If you are drowning in one of these loops (mortgage or otherwise), I want to hear what your version of the gift-letter donor is.