Skip to content
Talk to our solutions team

The Document API

Rules read documents through bbox.Doc(), a curated facade designed for rule authors. It is the API to write against.

A rule reads a document with an entry, a scope, a verb, and a coercion. That is the whole shape:

bbox.Doc() .Top(10) .Has("Closing Disclosure")
// entry scope verb
out.Set("amount", bbox.Doc().Right("Loan Amount").Currency());
// entry verb coercion

Scopes narrow, verbs ask or fetch, coercions turn a hit into a typed value. Most rules need one of each.

Matching is fuzzy and case-insensitive by default

Section titled “Matching is fuzzy and case-insensitive by default”

You do not configure matching. It is loose out of the box.

This is the single biggest change from the old surface, and it is easy to under-read as a convenience. It is not — it removes a category of work.

bbox.Doc().Has("closing disclosure") // matches "CLOSING DISCLOSURE"
bbox.Doc().Has("SELLER'S TRANSACTION") // also matches "SELLERS TRANSACTION"

Case, spacing and the apostrophe/punctuation variants that OCR produces are absorbed. You write the phrase as a human would say it, once.

The old API made this the author’s problem. Every call carried tolerance numbers:

.FindIPhraseFuzzy("Loan Terms", 2, 4, 0, 0)
.FuzzyTextNear("Name", "right", 1, 2, 0.3)

Nobody could tell you what 2, 4, 0, 0 should be for a phrase they had not seen. So authors copied them from a neighbouring rule, and a rule that failed to match was debugged by nudging numbers. Worse, it forced enumeration: a rule listing both SELLER'S TRANSACTION and SELLERS TRANSACTION exists only because the matcher would not absorb the difference.

The facade derives the tolerance from the phrase instead. A surface you choose from can be used correctly by a person and by a small language model; a surface you tune cannot. That was a design constraint, not a nicety.

When you genuinely need strictness — a document code, an identifier, a legal string where a near-miss is wrong:

ModifierEffect
Exact()Turns off loose matching for that chain
MatchCase()Case-sensitive, and implies Exact()
bbox.Doc().Exact().Has("FORM-1099-B")
bbox.Doc().MatchCase().Has("SECTION A")

MatchCase() implies Exact() deliberately: case-sensitive-but-still-fuzzy is not a combination that means anything useful, so the facade does not offer it.

Reach for these when a false positive costs you something. Leave them off otherwise — the default is right far more often than not, and every Exact() is a phrase you have promised to keep spelled exactly that way.

bbox.Doc() // the document
bbox.Doc("page") // a specific loaded document, by name

The name is optional because it is almost always ceremony — in the production corpus, 3,320 of 3,360 document lookups pass the same string. Supply it only when a rule genuinely works across more than one loaded document.

Each returns a narrowed Doc, so they chain.

ScopeNarrows to
Top(pct) / Bottom(pct)A band measured from the top or bottom, as a percentage
Band(fromPct, toPct)An arbitrary horizontal band
Region(x0, y0, x1, y1)An explicit rectangle, in percent
Page(n)One page
Section(heading)From a heading to the end of its section
Between(start, end)Between two phrases
Until(phrase)From here until a phrase
Line() / Lines(n)The current line, or the next n
Exact() / MatchCase()Opt out of loose matching — see Matching

Top(10) reads better than a rectangle and is what most rules meant. There is no fixed “header”: the corpus used depths of 15, 20, 25, 30, 40 and 50 percent, so the depth is yours to choose.

VerbReturns
Has(phrase)bool
HasAll(phrases...)bool — every phrase present
HasAny(phrases...)bool — at least one
HasPattern(pattern)bool
Count(phrase) / CountOf(pattern)int
Checked(label)bool — checkbox state

The four directional verbs read a value positioned relative to a label, and each returns a Value:

bbox.Doc().Right("Loan Amount")
bbox.Doc().Below("Total")
bbox.Doc().Left("Balance")
bbox.Doc().Above("Signature")

Extra arguments form a chain: each anchor is located below the one before it, and the read happens in the verb’s direction from the last.

bbox.Doc().Right("Date Issued", "Closing Date")
bbox.Doc().Below("Current name", "First name")

This replaced three separate string DSLs. Two were catalogued at the outset — "Date Issued<below>|Closing Date" and the chained variants — and a third turned up on inspection: a " |> "-separated anchor path, used at 111 sites, the fifth most-used method in the corpus.

They all meant the same thing. The third only ever walked downward, and of the other’s 161 directional hops, 146 were <below>. Three grammars for one idea, which is why they collapse into arguments rather than syntax.

Two things improve by making them arguments rather than syntax. Each label is separately checkable, so a failing chain reports which link broke instead of returning an empty string. And there is no string grammar to get subtly wrong.

RightOf(label) covers the rightward hops, which were 15 of the 161.

A Value becomes typed by asking:

CoercionProduces
Str() / Text()String
Num() / Number()Number
Currency()Money, symbols and separators handled
Date()Date
Percent()Percentage
YesNo()Boolean from yes/no text
TitleCase()Title-cased string

And for inspecting the read itself:

MethodUse
Found() / Missing()Whether anything matched
Or(fallback)A default when missing
Confidence()The match confidence
Match()What actually matched
Why() / Reason()Why a read failed — the debugging entry point

Why() is the one to reach for when a rule silently produces nothing. A Value that did not match knows which anchor in its chain failed.

EntryReturnsFor
Table() / TableWith(text)TableThe document’s table, or one containing text
Form(name) / HasForm(name)FormA named form
Rows(startHeading, endHeading)RowsA column-aligned section between two headings
Cell(rowLabel, colLabel)ValueOne cell by its labels

Table offers Row, RowWith, Column, Cell, Headers, Records, RowCount, ColumnCount. Rows — for column-aligned sections that are not real tables — offers Col, ColNth, ColRightOf, ColUnder, SplitOn, Strip, Records, Count.

Records() on either gives you the rows as maps, which is usually what you want to hand to out.Set.

MethodUse
Title() / IsTitle(phrase)The document title
Headings() / IsHeading(phrase) / HeadingCount()Heading structure
IsBigText(phrase)Whether text is visually prominent
FontSize(phrase) / BodyFontSize()Type sizes
LineWith(phrase) / FirstLine() / LineCount()Lines
PageCount() / TableCount()Counts
AllText()Everything, as one value

Typography signals are how you distinguish a heading that happens to read like body text from one that is actually set as a heading.

The same classification rule, before and after:

// Previously — six lines, twelve magic numbers, two presence idioms
bbox.Get("page").InRegionPct(0.0, 0.0, 60.0, 10.0)
.ContainingIPhraseFuzzy("Closing Disclosure", 2, 4).Count() > 0 &&
bbox.Get("page").FindIPhraseFuzzy("Loan Terms", 2, 4, 0, 0).Len() > 0 &&
bbox.Get("page").FindIPhraseFuzzy("Projected Payments", 2, 4, 0, 0).Len() > 0 &&
bbox.Get("page").FindIPhraseFuzzy("SELLER'S TRANSACTION", 2, 4, 0, 0).Len() == 0 &&
bbox.Get("page").FindIPhraseFuzzy("SELLERS TRANSACTION", 2, 4, 0, 0).Len() == 0
// Now
bbox.Doc().Top(10).Has("Closing Disclosure") &&
bbox.Doc().HasAll("Loan Terms", "Projected Payments") &&
!bbox.Doc().Has("SELLER'S TRANSACTION")

The second negative check disappears entirely: loose matching absorbs the apostrophe variant, so there is nothing to enumerate.

The tuning numbers are gone too, and that is the deeper point. 2, 4, 0, 0 were fuzz thresholds an author had to tune; the facade derives them. A surface you choose from is one both a person and a small model can use correctly — a surface you tune is not.

Instead ofWrite
bbox.Get("page")bbox.Doc()
.InRegionPct(0,0,100,20).Top(20)
.InRegionPct(0,0,100,100)nothing — it was a no-op
.ContainingIPhraseFuzzy(p, 2, 4).Count() > 0.Has(p)
.FindIPhraseFuzzy(p, 2, 4, 0, 0).Len() > 0.Has(p)
.FuzzyTextNear(l, "right", …).Right(l)
.FuzzyTextNearCapped(l, …).Right(l)
.GetOCRTextSimple("A |> B").Below("A", "B")
"A<below>|B" chains.Below("A", "B")
.IsCheckboxChecked(l).Checked(l)
.ExtractColumnAlignedSection(…).Rows(start, end)
.SafeExtractForm(n) / .HasForm(n).Form(n) / .HasForm(n)

Existing rules do not need rewriting to keep working. Migrate when you touch a rule for another reason.

The facade fixes what a rule reads. The runtime fixed what a rule writes over the same period — Retract became automatic, Changed left the rule surface, and dotted output keys nest. Together they are why the example above is three lines rather than fourteen. See Writing rules.