A Spreadsheet Where Every Cell Is a Cited, Verified Extraction

A document matrix where every cell is a cited, verified extraction: per-cell retrieval, a strict two-outcome schema, and literal-substring verification.

Every legal team eventually has the same afternoon. There are forty NDAs, or two hundred leases, or a due-diligence folder nobody has fully read, and one question that has to be answered for each one. What is the governing law? Does this auto-renew? Who indemnifies whom, and up to what cap?

The natural shape for that work is a spreadsheet: one row per document, one column per question, one answer in each cell. The Document Matrix is exactly that shape. Rows are your documents, columns are your questions, and each cell is a single answer with the quotes it came from, which you can click to land on the exact passage in the exact document.

A spreadsheet is the easy part. A spreadsheet full of confident AI guesses is worse than no spreadsheet, because it looks like work product and launders a hallucination into a tidy grid. This post is about the design choices that make each of those cells something a lawyer can rely on, and, at the end, a test you can run against any tool that claims to do the same thing.

One cell is one document and one question, extracted alone

The unit of work is a single (row, column) pair. Each cell is extracted on its own: it loads its own column question, its own row's document, and the parent matrix in a single database round-trip, then runs a self-contained pipeline that ends by writing one result back. Nothing about cell B7 depends on what cell B6 decided, unless you explicitly wire a dependency, which we will come to.

That independence is what lets the grid fan out. A run over two hundred documents becomes two hundred small tasks, each durable, retryable, and idempotent, and a failure stays contained: if one document is a corrupt scan, that document's cells say so and the other 199 rows are unaffected.

Here is the whole per-cell pipeline, including the conditional gate and the citation check that later sections unpack.

Loading diagram...

The retriever can only see one document per cell

The first trust decision is the narrowest one. For a given cell, retrieval is scoped hard to that one document. The search that gathers passages to answer from is filtered to a single document id, so the cell for lease number twelve can only ever pull text from lease number twelve.

This sounds obvious and it is the single most important guard in the whole feature. The classic cross-document failure is a matrix that answers "what is the termination notice period?" for document A using a clause that actually lives in document B, because both documents are similar and the retriever grabbed the wrong one. By construction, that cannot happen here: every quote in a cell is provably from that cell's document, because no other document's text was ever in the room.

We retrieve a few dozen passages per cell, ranked by the same hybrid dense-plus-sparse search and cross-encoder reranking the rest of the product uses, just filtered to one document id. Twenty is a deliberate number. An earlier, smaller number missed instances on aggregation-style questions over long documents, where the answer is spread across many clauses, so we roughly doubled recall at a modest prompt-size cost. The result of that same (document, question) retrieval is cached for a day and keyed on a hash of the question text, so a re-run skips the embedding and search entirely, while editing the column question naturally invalidates the cache. When retrieval comes back full, meaning it returned the full twenty and there may be more relevant passages than we looked at, the cell carries a "possibly incomplete" flag so the reader is warned on aggregation-style questions over long documents ("list every indemnification clause") instead of being shown a partial answer as if it were complete.

A schema with exactly two outcomes, so the model cannot waffle

When the passages reach the language model, we do not let it write freeform. The response is constrained to a strict schema whose status field has exactly two legal values.

// Simplified from our cell output schema.
{
  "status": "extracted" | "not_found",
  "answer": "...",                 // empty string when not_found
  "citations": [                    // each is a passage ref + a verbatim quote
    { "ref": 3, "quote": "Governed by the laws of the State of Delaware" }
  ]
}

There is no third path. The model cannot return "probably", it cannot return "the document seems to suggest", and it cannot return an answer with no citation attached. It either extracts a value and points at the passages that support it, or it says the retrieved text does not answer the question. Removing the middle option removes the exact behavior that makes AI grids dangerous, which is a plausible-sounding answer with nothing underneath it.

Each citation is a reference number pointing at one of the passages we showed the model, plus the verbatim substring of that passage the answer rests on. That quote is a promise the model is making, and the next step is to hold it to it.

Literal-substring verification: the quote has to actually be there

A model asked to quote verbatim will still, sometimes, produce a quote that is almost right. A word swapped, a number nudged, a phrase smoothed. So after the model answers, we check every citation against the passage it claims to be quoting, and the check is deliberately dumb: is this quote, normalized for whitespace and case, a literal substring of that passage?

# Simplified from _resolve_and_verify_citations.
for citation in model_citations:
    passage = passages[citation.ref - 1]          # resolve the ref to a real chunk
    if normalize(citation.quote) not in normalize(passage.text):
        drop(citation)                            # the quote was not really there

A citation whose quote is not literally present is dropped before anything is persisted. The reader never sees it. And then the consequence that matters: if the model claimed to extract an answer but none of its citations survive verification, the cell is downgraded from "extracted" to "not_found". An ungrounded answer does not get to be an answer.

There is one more leak we close. Because the model also weaves its quotes into the readable answer text for the cell, a fabricated quote could ride along inside the prose even after its citation was dropped. So if a quote that failed verification still appears inside the answer string, we downgrade the whole cell rather than ship a fabrication dressed as a sentence. The rule is asymmetric on purpose. A correct multi-citation answer is not thrown away just because one redundant supporting quote was imperfect, but a bad quote that actually reached the reader taints the cell.

What this layer guarantees is precise and valuable: the answer is faithful to the passages that were retrieved, and every cited quote genuinely appears in the cited passage. That is grounding, and it is exactly the job of this layer. Retrieval quality and the broader reliability signals are separate jobs, handled by separate parts of the system, so each layer does one thing well and the cell you read has cleared all of them.

Conditional columns: a cell that only runs when an upstream cell earns it

Not every question applies to every document, and asking anyway wastes money and clutters the grid. So a column can depend on another column.

A dependent column names an upstream column and a gate expression. Before the dependent cell does any retrieval or any model call, it looks at the upstream cell in the same row and evaluates the gate. The gate grammar is intentionally tiny, things like "is the upstream answer non-empty", "does it read as yes", "does it equal this value", "is it one of these options", so the interface can build and validate a gate without a parser, and any expression it cannot parse fails closed rather than silently letting an unintended cell run.

If the gate fails, the cell is marked "not applicable" and no model is ever called. If the gate passes and the column is set to inject the upstream answer, that upstream answer is prepended to the prompt as context, so a follow-up question can build on what the first column already found in that same document. "Does this contract contain a limitation of liability clause?" gates "what is the liability cap?", and only the documents that cleared the first question pay for the second.

There is a timing wrinkle we handle without building a scheduler. When a dependent cell runs before its upstream cell has finished, we do not guess and we do not block a worker. The cell raises a specific pending signal, and the background task catches it and retries with backoff. The dependency resolves itself by waiting, no dependency graph required.

Re-runs overwrite in place, and never clobber a human

The grid is idempotent. There is one cell per (row, column) pair enforced at the database level, so re-running a cell overwrites that one cell rather than appending a duplicate. Re-run a whole matrix and it recomputes each cell into the same slot.

The default re-run picks up cells that are pending, errored, not-found, or not-applicable, and skips cells that already have a good extracted answer, so you are not paying to redo work that is done. Crucially, "not applicable" is included in that set. If you change an upstream answer, the downstream gate gets re-evaluated on the next run, so a cell that was correctly skipped can come back to life when the thing it depended on changes.

And the write is guarded. If you manually edited or approved a cell while a background extraction for it was still in flight, the extraction is not allowed to overwrite your value. The final write is a single conditional update whose WHERE clause restricts the write to the five machine-owned statuses (pending, running, error, not found, not applicable). Postgres evaluates that predicate atomically per row, so a cell that flipped to "edited" or "approved" between the worker reading it and writing back is simply skipped, and no lock or read-modify-write dance is needed. Human intent wins over a late-arriving robot, every time.

What we deliberately did not do: over-agentify the cell

The fashionable way to build this would be to make each cell a little agent, free to call tools, re-search, second-guess, and loop until it feels done. We did not. Each cell is a single, deterministic pass: one scoped retrieval, one constrained model call, one round of citation verification, one write. There is no per-cell tool use, and there is intentionally no "deep" versus "standard" tier for cells.

This is a feature, not a limitation. Tool-using agents fail in ways that are hard to see and hard to reproduce: they wander, call the wrong tool, and burn a token budget to return something shaped like an answer. Across a two-hundred-cell run, a small per-cell failure rate compounds into a grid you cannot trust and cannot explain. A deterministic cell is auditable: the same document and question produce the same shaped work every time, the cost is predictable, and when a cell is wrong you can see exactly which passage misled it. For a spreadsheet a lawyer will rely on, boring and inspectable beats clever and opaque.

A second opinion, only when you ask for it

The guards above run on every cell for free. There is also an opt-in check, one click in the cell drawer, that spends one extra model call to ask a stricter question: taking the cited passages together, do they actually entail this answer? It returns one of four verdicts, supported, partially supported, unsupported, or contradicted, plus a short explanation, and it saves that verdict onto the cell.

We keep it opt-in rather than always-on because running it on every cell would raise the cost of a large matrix substantially for a small precision gain. The reader spends that money on the cells that matter to them, on the answer they are about to rely on, not on all two hundred.

Test it yourself, and questions to ask any vendor

You do not need our code to judge a matrix tool. You need a folder of documents and a few minutes.

  1. Run one question across twenty documents, then click every cell's citation. Confirm the quote actually appears in that specific document, on that page. The tell of a weak tool: a quote that is close but not present, or a citation that opens the wrong document.

  2. Include a document that genuinely does not answer the question. A strong tool leaves that cell empty or marks it not found. The tell: a confident, specific answer in a cell whose document says nothing on the subject.

  3. Feed it a document that is only a scan with no extractable text. A strong tool tells you the document has no searchable text, distinctly, rather than filling every column with a bland "not found" that hides an ingestion problem.

  4. Set up a dependent column and then change the upstream answer. A strong tool re-evaluates the dependency on the next run. The tell: a downstream cell that stays frozen on an answer its precondition no longer supports.

  5. Re-run the matrix after hand-editing one cell. A strong tool leaves your edit alone. The tell: your correction silently overwritten by the machine.

And the questions worth asking the person who built it:

  • When a cell answers, is retrieval scoped to that one document, or can a cell quote text from a different document in the set?
  • Can the model return an answer with no citation, or is a grounded citation structurally required?
  • What happens to a cell when the model's quote is not actually in the source: is it dropped, or displayed anyway?
  • Is each cell a deterministic extraction, or an agent that can loop and call tools, and how do you keep two hundred of those explainable?

A vendor who answers all four concretely has thought about the problem you are buying a solution to. A vendor who points at a demo where every cell happened to be right has shown you a good afternoon, not an architecture, and the difference is the one you will feel on the two-hundred-document folder nobody has fully read.

New legal AI guides, weekly.

Priyansh Khodiyar

Priyansh Khodiyar

Co-Founder & CTO

Priyansh leads engineering and AI at Vaquill, from the matter workbench to drafting, document comparison, document matrix, and citation-verified research.

Research, review, and draft, with a source on every answer.

Vaquill AI reads your documents and knows the law. Every answer shows where it came from. 7-day free trial.