Skip to content

IDP — Ground

Type: idp-ground  ·  Package: IDP Activities v1.20.4  ·  Output: object

Since: v1.1.0

Takes what AI — Extract returned and looks each value back up in the document. Three things come out of it: the box where it is, a measured confidence (not whatever the model claims), and the decision of which fields a person needs to look at.

IDP — Extract already uses it internally; use it on its own if you build the pipeline by hand.

The schema built by IDP — Build Schema asked the model, for each field, for the value, the literal text it comes from and the line where it appears. This activity searches for that literal among the document’s words:

  • It finds it → there is a box, and the confidence is the OCR confidence of those words.
  • It doesn’t find it → the value cannot be verified. To review.

A field goes to review if its confidence falls below its minConfidence, if it could not be located, or if the model did not return it. It is a local calculation: it calls no service and uses nothing. The log sums up how many fields were accepted and how many need review.

ParameterTypeDefaultDescription
documentDictionary—The document from IDP — Load Document (or from IDP — Classify). Its words are where each value is located.
extractionDictionary | List—What AI — Extract returned: one answer, or the list of answers when the taxonomy was split into batches (they are merged here).
taxonomystring | Dictionary—The same taxonomy the schema was built with (path to a .json or object). Its per-field minConfidence decides what goes to review.

None.

FieldTypeWhat it is
documentTypestringThe taxonomy’s document type.
needsReviewbooltrue if any field needs review.
reviewFieldsListNames of the fields that need it.
valuesDictionaryName → value. To use the data directly. A table comes out as a list of rows; a table with no rows, as an empty list.
fieldsListOne object per field, with full detail.

Each entry in fields carries: name, type, value, valueRaw, grounded, confidence, needsReview, reason, page, x, y, width, height, rows and rowCount. reason explains why a field goes to review (for example, that the literal does not appear in the document) and is null when it doesn’t need to.

A field of type table carries its rows in rows, and each cell has exactly the same shape as a scalar field — with its value, its confidence, its box and its reason. In values, the table comes out as a list of clean rows:

values["items"] = [
{ "description": "Letter paper ream", "quantity": 10, "amount": 185000 },
{ "description": "Black toner", "quantity": 2, "amount": 420000 }
]

The table needs review if any of its cells does. If the model did not return the table, it goes to review; if it returned it empty, it does not.

Walking a table needs a nested Foreach:

Foreach items = res["fields"] itemVariable = field
If condition = field["type"] == "table"
Foreach items = field["rows"] itemVariable = row
Set Variable name = cell value = row["amount"]
Log message = toString(cell["value"]) + " conf=" + toString(cell["confidence"])

Why the confidence isn’t asked of the model

Section titled “Why the confidence isn’t asked of the model”

Because the confidence an LLM assigns itself is not calibrated: it says “0.95” just as easily when it is right as when it makes something up.

Here something checkable is measured: the literal is written in the document or it isn’t. A value the model invented appears nowhere, so it is not found, so it goes to review. And as a bonus you get the box to highlight it.

That is why the schema asks for valueRaw besides value: value may come normalised (2026-07-15) while the document says July 15, 2026. The literal is what anchors; the value is what you use.

  • It compares ignoring spaces and case: OCR splits and joins words unpredictably, and where the spaces fall should not invalidate a hit. A literal can span several words and the box joins them.
  • Punctuation is kept: in a serial number, one dash more or less is a real difference that deserves review.
  • The confidence of a multi-word box is that of the weakest link: if one word of the serial number was read poorly, the serial number was read poorly.

Finding the literal proves it exists, not that that occurrence is the one that produced the value. An issue date, a due date and a delivery date can be identical: searching for the bare value would always match the first, and the box would point at the wrong field.

The solution is not to recover the position afterwards, but to have the model cite it while extracting. The document reaches it with every line numbered:

[11] Issue date: | 15/01/2024
[12] Due date: | 15/01/2024
[13] Delivery date: | 15/01/2024

And for each field, besides the value, the model returns the line number it took it from. Grounding then doesn’t search: it goes straight to that line’s box, and within it narrows down to the value. The date on line 12 and the one on line 13 stop colliding because the model said “12”, not “15/01/2024”. It is getting the position at reading time instead of guessing it afterwards — without training a model per layout.

  • The model cites a valid line → the box comes from there, direct and unambiguous.
  • The value doesn’t match within the cited line (the model cited it loosely, or the value was normalised) → the whole line’s box is used: the right line, with less precision.
  • The model doesn’t cite → it falls back to searching the literal in the text; a repeated value that cannot be told apart this way goes to review, instead of pointing at one at random.

In a table, each cell cites its own line, so repeated rows each land in their place without depending on order.

The full pipeline by hand, for a taxonomy that fits in one batch:

IDP — Load Document path = docPath → output: doc
IDP — Build Schema taxonomy = asset("taxonomies/supplier-invoice.json") → output: schemas
AI — Extract prompt = doc["text"]
schema = schemas[0]
→ output: extraction
IDP — Ground document = doc
extraction = extraction
taxonomy = asset("taxonomies/supplier-invoice.json")
→ output: res
If condition = res["needsReview"]
Queue Add Item ...to the human validation queue

Walk the fields to draw the boxes over the rendered page:

Foreach items = res["fields"] itemVariable = field
Log message = field["name"] + " = " + toString(field["value"]) + " (conf " + toString(field["confidence"]) + ")"

Use a specific value — it takes one hop because the engine does not chain indexers:

Set Variable name = vals value = res["values"]
Log message = "Invoice: " + toString(vals["number"])
MessageCause and fix
'document' must be the object returned by IDP Load Document.document is not an object (for example, you passed the path).
'document' has no 'words'. Pass the object returned by IDP Load Document.The object does not carry the positioned words.
'extraction' must be what AI Extract returned: either one answer, or the list of answers when the taxonomy was split into batches.extraction is neither an object nor a list of objects.
'taxonomy' is required. / The taxonomy must declare a 'documentType'. / …The taxonomy is invalid. See the errors in IDP — Build Schema.