Workflow, Not Agent: Deep Research an Analyst Can Trust.
Deep research demos look perfect while up to six claims in ten are wrong. How we built a biotech diligence pipeline where every claim is traceable, derived, or visibly marked uncertain.

Deep research is easy to demo and hard to trust. We build AI reports for biotech investors, where the accuracy of the information is key. Here is what we learned building a research system around one rule: every material claim is traceable, derived, or visibly marked uncertain. Never a confident guess.
The demo always works
Every deep research demo looks great. You paste in a question, wait a few minutes, and get back a confident, well-structured report with citations at the bottom. Honestly, it is genuinely impressive. It is also the most dangerous kind of software: a system whose failures are invisible.
A 2026 audit of frontier deep research agents found the links they produce are almost always valid (94%+) and topically relevant (80%+). Factual accuracy sits anywhere between 39% and 77%. Read that again. The citations check out, and up to six claims out of ten are still wrong.
Salesforce researchers ran their own audit on deep research modes of GPT-5, Perplexity, Copilot, and Gemini and got citation accuracy ranging from 40% to 80%. Notably, one flagship system produced reports in which 97.5% of statements were not supported by the sources they cited.
A fabricated fact and a found fact look identical on the page. Deep research is easy to demo, hard to trust.
Our problem looked different from the demos
We build an AI platform for biotech investment analysis. The central artifact is a structured profile of a drug asset: efficacy numbers, competitors, market size, regulatory status, valuation inputs, etc. An analyst reads it cell by cell and acts on the numbers.
There is no open-ended chat question here, no clarifying questions, no improvised web browsing, and no free-form essay at the end. The schema is fixed, and the source families in biotech are known: the FDA label, ClinicalTrials.gov, openFDA approval records, epidemiology tables, and SEC filings.
What we do not know in advance is which record wins. Good sources disagree all the time. The label reports one data cutoff, the paper reports another. Two trials measure the same endpoint in different patients. So our source set is not a list, it is an order plus a rule for conflicts:
- Family order. FDA label first, then posted ClinicalTrials.gov results, then SEC filings, then papers, then press releases.
- Match first, then date. Two numbers only compete if they measure the same endpoint in the same population. When they do, the later cutoff wins.
- If it stays unresolved, keep it unresolved. Two good sources, two different numbers, both survive the rule: show both, mark the cell as conflicting, never quietly pick one.
We know which families the truth should live in. And that exposes the real problem, the one no demo shows: often, it does not live anywhere. The trial has not posted results. The company is pre-revenue, so there is no product revenue in any SEC filing. No drug in the same class has been approved yet, so there is no competitor record to find.
What does a language model do when the answer is absent from every source you gave it? It answers anyway.
Why models fill the gaps
This is not a bug you can scold out of the model. As an OpenAI paper put it, models hallucinate because training and evaluation reward guessing over acknowledging uncertainty - a model that guesses scores better than one that abstains, so guessing is what it learns. A recent taxonomy of deep research failures found hallucinations creep in at every stage of the pipeline (retrieval, synthesis, citation), not just in the final write-up.
Trust must be structural, not behavioral. You don't ask the model to be honest, you build a system where an ungrounded claim is caught by a gate instead of by a reader. No matter how good it sounds.
Our pipeline is built on three commitments:
- Fix the shape - named, gated steps, not a free loop.
- Fix the world - fetch once, reason inside it.
- Fix the claims - grounded or dropped.
1. Fix the shape - named, gated steps, not a free loop
We are not alone in this conclusion. When Anthropic's Barry Zhang gave his "How We Build Effective Agents" talk at the AI Engineer Summit, two of his three takeaways were "don't build agents for everything" and "keep it simple". A recent AI Engineer workshop on building deep research systems frames it as a spectrum (workflow → single agent with tools → multi-agent) with one rule: stay as far left as possible, and move right only when the task forces you to. In practice, most useful agents are really workflows with retrieval bolted on. Which is not a failure. It is what makes them shippable.
But a workflow by itself is just a container. Five properties do the real work, and a workflow is simply the cheapest place to enforce all five:
- Bounded retrieval. A known set of sources, fetched once.
- Frozen evidence. The reasoning step cannot go back for more.
- A fixed schema. No room to write around a gap.
- Provenance. Every value knows its run and its source record.
- Rejection gates. A claim that fails a check is dropped, not softened.
An agent can do all five too, it is just harder, because each one is a limit on a loop that was built to be free.
Our pipeline runs as a durable workflow (we use Inngest) of about twenty named steps, each memoized:
resolve anchors → fetch-fda-label, lookup-icd10-codes, resolve-indication
fetch sources → query-competitors, query-asset-trials, fetch-biomarkers
freeze → assemble-sot-bundle
reason → efficacy, positioning, valuation (no network access)
emit → full structural output
Each step retries independently, and its result is memoized, so a crash mid-run replays cleanly instead of re-fetching the world. Every report carries its run ID. Emissions are keyed by a content-based dedup key, so a retry refreshes the same row instead of forking a duplicate. When an analyst asks "where did this number come from?", the answer is a specific step in a specific run. Not a vibe.
The LLM still does the hard cognitive work. It just doesn't get to drive.
2. Fix the world - fetch once, reason inside it
Authoritative sources are still incomplete, and absence is exactly where memory leaks in. If a trial has no posted results, the model will simply "remember" some plausible numbers. The defense is to make the boundary between the world and the reasoning physical.
Our runs are split into two layers.
Layer 1 touches the network exactly once. It fetches full records (the FDA label, ClinicalTrials.gov studies with their outcome measures, competitor approvals, epidemiology rows and SEC company facts) and freezes them into what we call the SoT bundle, for source of truth: a single, content-hashed object. The hash means identical content produces an identical bundle, so runs are reproducible, and the reasoning layer is unit-testable without ever mocking a network call.
Layer 2 is a pure function of the bundle. Reasoning steps get the bundle and nothing else. No network. No retrieval. If a fact is not in the bundle, it does not exist for this run.
We call this closed-world research. It is the same idea behind DeepMind's FACTS Grounding benchmark, which judges a model only on whether its response is grounded in the document it was given. We just enforce it in the architecture instead of measuring it after the fact.
One rule keeps a closed world honest: absence from the bundle means unknown, not false. And unknown is not one thing. A single "-" hides four different situations, and the analyst should treat each one differently. So every section carries a status instead:
| Status | Meaning | What the analyst does |
|---|---|---|
found | a source gave us a value | click the citation |
conflicting | two good sources, two different numbers, both survive the tie-break | both are shown, the disagreement itself is the signal |
not_reported | the record exists, the value was never published (a trial with no posted results) | not a negative, check back after readout |
none_found | we queried the right source and it is genuinely empty | treat as a real negative; the source we queried is named |
not_searched | skipped on purpose (a Phase 1 asset has no FDA label) | nothing out of scope for this asset |
error | the fetch failed: timeout, quota, exception | re-run; this cell proves nothing |
The closed world doesn't stop the model from trying to fill gaps from memory. It makes the attempt detectable, because now every claim can be checked against a finite, known set of text.
3. Fix the claims - grounded or dropped
You can't just prompt your way to a trustworthy system, you have to enforce it structurally. Here are three mechanisms for doing that, in increasing order of strictness.
Claims must point to text
Research on factuality converged on a key insight: you can't verify an essay, but you can verify an atomic claim. FActScore decomposes long text into individual facts and checks each one (ChatGPT scored just 58% on biographies under that lens). We push the same idea upstream, into generation itself.
Every reasoning step has to emit each value together with the verbatim quote it came from:
{
"value": "18.4 months",
"sourceQuote": "median overall survival was 18.4 months (95% CI ...)"
}
A verifier keeps the claim only if the quote appears, character for character, in the bundle. For numbers, the number must also occur inside that quote. The model cannot launder a memorized fact past this check, because it has to point at real text that a run actually fetched.
Citations are an allowlist, not decoration
The 2026 audit above found exactly the failure we designed against: near-perfect links attached to wrong claims. A valid URL proves nothing by itself. So citations in our pipeline are not trusted output - they are checked against a per-run catalog of the sources the workflow actually fetched.
The rules are simple and strict:
- A citation URL survives only on an exact match against the catalog. Fabricated or altered links (invented NCT or PubMed IDs) get dropped.
- A bare text label either resolves to a real deep link (a trial ID becomes its actual ClinicalTrials.gov study page) or is deleted.
The field is reaching the same conclusion. A 2026 study of 53,090 citation URLs from ten commercial LLMs and deep-research agents found 3–13% hallucinated outright, and showed that simple URL-verification tooling cuts unresolvable citations to under 1%.
But an allowlist answers one question only: did we fetch this source in this run? Worth having - it kills invented NCT and PubMed IDs. It is also the easiest of the 5 checks, and treating it as all five is how a report gets trusted for the wrong reason:
| Check | The question | Today |
|---|---|---|
| source validity | did we fetch this URL this run? | yes, exact match against the catalog |
| evidence presence | does the quoted text contain the value? | yes, on the efficacy fields |
| claim entailment | does the text actually support the claim? | no, this is the real gap |
| source authority | is this the right kind of source for this field? | partly, the family order above |
| temporal validity | is this the current number or an old cutoff? | partly, we record the cutoff, we do not gate on it |
Entailment is the check that turns "the quote exists" into "the claim holds." It is what we build next: a separate step that judges (claim, quote) pairs and is scored against an analyst - never the model grading its own work.
Not every claim is a fact - label it
Not everything a diligence report says can be a fact. Pretending otherwise is just another way to lie. Three labels turned out to be too blunt: "inference" was covering four different things. So we use six:
| Basis | Means |
|---|---|
extracted | copied from a source, with the quote |
derived | calculated from extracted numbers by a fixed rule |
inferred | model judgement over the bundle, with no calculation behind it |
modeled | comes out of assumptions - valuation, market size, uptake |
hypothesis | plausible, but this bundle does not support it |
unknown | not in the bundle (see the statuses above) |
Valuation is the one that matters most. A discounted cash flow is not a fact and not an inference. It is assumptions with a number on top. Unless it is labeled modeled with those assumptions visible, it is the most dangerous cell in the report: it looks like data and behaves like an opinion. Today, the code still ships three labels (fact / inference / hypothesis). The six-label version is our next schema change, and the reason for it is that it is modeled.
What the analyst sees
Back to where this post started: the demo always works. It comes back full and confident, with an answer in every box. Our report is the opposite, it looks plain and has gaps, and both are on purpose. The gaps are the point.
Every empty cell says which of four kinds of emptiness it is. The value is not published yet, the source we checked is genuinely empty, the field was skipped on purpose, or the fetch failed and needs a re-run. Each kind tells the analyst to do something different.
Rows that are not plain facts say so. A badge marks them as inference or hypothesis, so a judgment never dresses up as data. And every citation is a real link from this run - click it, and the quote behind the number is on that page.
Empty is better than wrong. An empty cell is easy to fix. The analyst sees the gap and checks the source themselves. A confident wrong cell is worse. The analyst acts on it. And when the error finally comes out, they stop trusting every other number in the report. Trust is built cell by cell - and lost all at once.
Deep research agents win on first impression. Diligence tools live and improve over time.
Further Reading
If you want to go deeper on any of the threads in this post:
Talks
- Build Your Own Deep Research Agents - Bouchard, Iusztin & Vaid, AI Engineer. The workflow→agent spectrum, and why to stay left.
- How We Build Effective Agents - Barry Zhang, Anthropic.
Articles
- Onweller et al., Cited but Not Verified: Source Attribution in LLM Deep Research Agents (2026)
- Venkit et al., DeepTRACE: Auditing Deep Research AI Systems (2025)
- Rao, Wong & Callison-Burch, Detecting and Correcting Reference Hallucinations in Commercial LLMs and Deep Research Agents (2026)
- Kalai et al., Why Language Models Hallucinate (2025)
- Min et al., FActScore: Fine-grained Atomic Evaluation of Factual Precision (2023)
- Jacovi et al., The FACTS Grounding Leaderboard (2025)
- Zhan et al., Why Your Deep Research Agent Fails (2026)
