Working with results
Working with results
Section titled “Working with results”Every Orchestrator recipe returns a
DocumentResult. This page is the catalogue of what you can do with
one before you reach for a custom export step.
The shape:
class DocumentResult: pages: list[PageResult] schema: type[BaseModel] | None # set when a structured prompt was used
# Typed views (lazy) fields: FieldView # for schema-driven extraction tables: TableView # for grid / tabular content regions_with_text: Iterator[Region] # everything that has a recognition
# Scalars mean_confidence: float page_count: int
# Serialisation def render(self) -> str: ... def to_dict(self) -> dict: ... def to_json(self, path: str | Path) -> Path: ... def to_dataframe(self) -> pandas.DataFrame: ... def to_excel(self, path, **kwargs) -> Path: ... def to_markdown(self, path: str | Path | None = None) -> str | Path: ... def show(self, *, page: int = 0): ...
# Composition def merge(self, other: "DocumentResult", strategy: MergeStrategy = "highest_confidence") -> "DocumentResult": ... def diff(self, other: "DocumentResult") -> "ResultDiff": ... def filter(self, where: Callable[[Region], bool]) -> "DocumentResult": ...
# Filtering def low_confidence(self, threshold: float = 0.6) -> list[Region]: ...PageResult is the same shape, scoped to one page.
Schema-first: .fields
Section titled “Schema-first: .fields”When the recipe ran with a schema=, result.fields is the typed
instance:
import scrivafrom scriva.schemas import Invoice
result = scriva.extract("acme.pdf", schema=Invoice, return_="result")
# Or, when you built the recipe yourself:result = recipe("acme.pdf")invoice = result.fields # -> Invoiceprint(invoice.total_amount, invoice.vendor.name)result.fields is the parsed pydantic model — no dict diving. Each
field exposes a .region reference for click-to-source:
result.fields.total_amount # -> Decimal("1240.00")result.fields_with_provenance["total_amount"].region.bbox # -> BBox(...)result.fields_with_provenance["total_amount"].confidence # -> 0.92fields_with_provenance is the same data wrapped with region,
page_index, and per-field confidence. Use it when you need to draw
overlays or feed a human-review UI.
Grids and tables: .tables
Section titled “Grids and tables: .tables”result.tables is a list of detected tables, each a thin wrapper around
a pandas DataFrame:
result.tables # -> list[Table]len(result.tables) # -> 2result.tables[0].to_dataframe() # -> pandas.DataFrameresult.tables[0].caption # -> "Line items" (when the Worker names it)result.tables[0].page_index # -> 0result.tables[0].bbox # -> BBox enclosing the whole tableto_dataframe() returns headers + rows. Multi-page tables are stitched
automatically when the splitter emits a continued_from annotation;
pass stitch=False to keep them split.
Reading-order text
Section titled “Reading-order text”result.render() # full document as text, reading orderresult.pages[0].render() # one pageresult.render(separator="\n\n---\n\n") # between pagesrender() walks regions in reading order (top-to-bottom, left-to-right
for LTR locales; the splitter can override the iteration order for RTL
or vertical scripts).
Raw regions: .regions_with_text
Section titled “Raw regions: .regions_with_text”The escape hatch when none of the typed views fit:
for region in result.regions_with_text(): print(region.id, region.role, region.text, region.confidence).regions_with_text() skips blanks and merged-source regions; iterate
result.pages[0].layout.regions if you need everything.
Confidence
Section titled “Confidence”result.mean_confidence # mean over recognitions that report itresult.confidence_histogram(bins=10) # -> list[int]result.low_confidence(threshold=0.6) # -> list[Region]low_confidence is the input to confidence-driven re-OCR (see
Orchestrator › Confidence-driven re-OCR).
Serialisation
Section titled “Serialisation”result.render() # str — reading-order textresult.to_dict() # dict; same schema as .export.jsonresult.to_json("out.json") # writes file; returns Pathresult.to_excel("out.xlsx") # MS Gothic default; honours confidence colouringresult.to_dataframe() # flat regions tableresult.to_markdown("out.md") # render() but with table syntax for .tablesresult.show(page=0) # matplotlib viz of boxes (optional extra)to_dict() returns an in-memory dict; to_json(path) writes a file.
Use the in-recipe .export.xlsx(...) / .export.json(...) steps when
you want the write to happen during the run (they emit events and
respect page_concurrency); use result.to_excel(...) for ad-hoc
serialisation.
Composition
Section titled “Composition”Merge two runs
Section titled “Merge two runs”a = primary_recipe("scan.png")b = refined_recipe("scan.png")final = a.merge(b, strategy="highest_confidence")Merge strategies:
| Strategy | Behaviour |
|---|---|
"highest_confidence" | Per region, keep the side with the higher .confidence (default). |
"right_wins" | Right’s recognition replaces left’s for every region in other; missing left regions are appended. |
"left_wins" | Left’s recognition is kept; other only contributes missing regions. |
"consensus" | Same text → keep; different text → fall back to tiebreaker=.... |
| callable | (left, right) -> Recognition. Custom resolution. |
Regions match by id (which is stable across re-OCR within the same
splitter configuration). Layout is taken from self unchanged.
Diff two runs
Section titled “Diff two runs”diff = a.diff(b)diff.changed # list[(region_id, left_text, right_text)]diff.added # regions only in bdiff.removed # regions only in adiff.to_markdown("diff.md")diff is the right shape for “did my prompt change actually move
anything?” — pair it with scriva.eval when you care
about the ground-truth direction of the move.
Filter
Section titled “Filter”high_conf = result.filter(lambda r: (r.confidence or 0) >= 0.8)high_conf.to_excel("clean.xlsx")filter returns a view, not a copy — region objects are shared.
Iteration helpers
Section titled “Iteration helpers”for page in result.pages: for region in page.layout.regions: ...
for region in result.iter_regions(): # all pages, flat ...
result.find(role="title") # first matching region or Noneresult.find_all(role="title") # listEquality and hashing
Section titled “Equality and hashing”DocumentResult is not hashable. It implements __eq__ based on
the serialised dict form, which is what you want for snapshot tests:
def test_invoice_extraction(snapshot): result = recipe("fixture.pdf") assert result.to_dict() == snapshotCaveats
Section titled “Caveats”result.fieldsraisesAttributeErrorif the recipe did not run a structured-output Worker. Checkresult.schema is not Nonefirst when you don’t control the recipe.result.tablesis empty when the splitter did not declareCapability.GRID. Use a grid splitter (.split.grid(),.split.whole_page()with a table-aware Worker) to populate it.mergeonly works when both sides share the same splitter configuration; mismatching region IDs raiseResultMergeError. Re-run through the same splitter first, or pass a callable strategy that doesn’t rely on ID alignment.
What to read next
Section titled “What to read next”scriva.extract— the one-liner that returnsresult.fieldsdirectly.- Exporters — file-format adapters used by
.export.*. - Orchestrator › Confidence-driven re-OCR — the canonical use of
.low_confidence.