Monthly rent
monthly_rentRule appliedA cited value beats an uncited one. The lease pointed at the clause it came from; the other two just asserted a number.
The lease, the abstract and the fact sheet all describe the same property, and they do not always agree. Send them together, get one cited record back, and see exactly which document won each contested field.
A commercial lease is never one document. There is the signed lease, an abstract someone typed up two years ago, a broker's fact sheet, and a spreadsheet the asset manager keeps. Each one states the rent. They do not all state the same rent.
So someone opens all four, finds the disagreements, decides which to believe, and types the answer into a system. That person is usually the only record of why the number is what it is. When they leave, the reasoning leaves with them.
The reconciliation is the work. Reading the documents is just how you get there.
The order matters, and it is the reason the result is predictable. Every document is read on its own, then the results are folded by rule.
Read each file once, whatever it is. A scanned lease and a spreadsheet arrive as the same kind of thing downstream.
Each document is extracted on its own, against your schema, with a citation for every field it filled.
The per-document results collapse into one record by documented rules. No model decides this part, which is why it never surprises you twice.
Every contested field is listed with what each document said and which one won. Your own checks run here too.
One record, plus what each document said on its own, into the system that tracks the portfolio.
Three documents, three contested fields, three different rules deciding them. Nothing is averaged and nothing is blended — a value is taken whole, with its citation, from exactly one document.
Rule appliedA cited value beats an uncited one. The lease pointed at the clause it came from; the other two just asserted a number.
Rule appliedAny real value beats an empty field. The lease never states the area, so it had nothing to contribute and was not asked to guess.
Rule appliedBoth cited the figure just as plainly, so the tie went to the earlier file in the request. Upload order decided this one.
That last rule is worth sitting with. Reverse the two files in the request and the winner flips — this is measured behaviour, not a guess. And no wording in your instructions will change it, because instructions shape the extraction and the fold happens afterwards. Turn grounding off and every comparison becomes a tie, which makes the first file win everything.
The shape can come from your code, from a saved action, or from one sentence of prose. Pick per call.
await sdk.extract({
file: {id: leaseId},
citations: true,
schema: leaseSchema,
})await sdk.extract({
files: [lease, abstract, factSheet],
unit: 'across_documents',
citations: true,
schema: leaseSchema,
})await sdk.extract({
files: [lease, abstract],
unit: 'across_documents',
action: 'lease-abstract',
citations: true,
})await sdk.extract({
file: {id: leaseId},
citations: true,
hints: 'Commercial lease: rent, term, dates, renewal.',
})Most of what people expect from a document merge is wrong, and wrong in ways that cost you an afternoon. Here is what actually happens.
A citation is not a footnote. It carries the file, the page and the box on that page, so a value you are unsure about takes one click to check rather than an afternoon with a PDF.
Extracted fields
const run = await sdk.extract({
files: [lease, abstract, factSheet],
unit: 'across_documents',
citations: true,
judge: true, // re-score each value against the passage it cites
schema: leaseSchema,
})
// A citation names the file, the page, and the box on that page. That is
// what the panel opposite is drawing, and what makes a value checkable.
for (const [field, cited] of Object.entries(run.output.citations)) {
for (const c of cited) {
console.log(field, c.fileId, c.page, c.bbox, c.confidence)
}
}
// Confident values carry on. The rest go to a person — and the citation
// is the first thing they open.
const needsReview = Object.entries(run.output.citations)
.filter(([, cited]) => cited.some((c) => c.confidence <= REVIEW_THRESHOLD))
.map(([field]) => field)Three steps and a decision. The merge is a field on the request, not a pipeline you assemble.
One file record per document, then PUT the bytes to each upload URL. Keep the order deliberate — request order is what breaks ties in the fold, so the document you trust most goes first.
Send a files array rather than a single file, set unit to across_documents, and leave citations on. Grounding is what lets the fold rank two competing values instead of falling back to order alone.
Set judge to true and a second pass re-scores each value against the passage it cites. It needs citations on to have anything to read. Low scores are your queue for a person, not a reason to discard the value.
You get the merged record, and every citation carries the fileId it came from — which is what makes the merge auditable rather than merely convenient. Keep the run and it becomes a permanent record instead of expiring with its default TTL.
Once the terms are structured, the documents that quote them can be produced rather than typed.
Everything above ships as a runnable TypeScript example under MIT, with sample documents included. Bun install, drop in an API key, run it. All four ways to call the extraction are selectable in the UI, and the review screen is the one in the video.
The interesting experiment takes ten seconds: reverse the two documents in the file list and watch the tied field change its answer. That is the whole argument for deterministic rules over a model that quietly picks a favourite.
github.com/CloudRaker/cloudraker-api-examples-typescript — the rental-lease-extraction folder.
One capability and three of its settings carry this whole workflow.
Take two leases you already argue about, send them in one call, and read the disagreement list. Ten minutes, and you will know whether this belongs in your stack.