Textract's Structured-Extraction Model
Amazon Textract is often introduced as "OCR for AWS," and that undersells it.
Search across all documentation pages
Amazon Textract is often introduced as "OCR for AWS," and that undersells it.
Reading the characters off a page is the easy part - Tesseract and a dozen other libraries have done that for years. What Textract actually sells is structure: knowing that a string of digits sitting next to the word "Total" is a value, that a value belongs to a specific label, that a grid of numbers is a table with rows and columns, not just text that happens to be aligned.
That structure does not arrive as a ready-made object you can immediately loop over. It arrives as a graph, and understanding that graph is the single most important thing to learn before writing any Textract integration.
DetectDocumentText for raw OCR, and AnalyzeDocument for structured extraction via FeatureTypes.Blocks array - structure is not a different response format, it's more block types linked by more relationships in the same array.BlockType (PAGE, LINE, WORD, TABLE, CELL, KEY_VALUE_SET, QUERY, QUERY_RESULT), Relationships (CHILD, VALUE, ANSWER), FeatureTypes (TABLES, FORMS, QUERIES, SIGNATURES).DetectDocumentText when you only need reading order; AnalyzeDocument the moment you need a form field, a table cell, or a targeted answer.DetectDocumentText is Textract's plain-OCR operation. Send it an image or a single-page PDF and it returns PAGE, LINE, and WORD blocks - text and its position, nothing more. It has no concept of a form field or a table; a table's cell contents come back as ordinary lines in reading order, indistinguishable from surrounding paragraph text.
AnalyzeDocument is the same shape of call with one addition: a FeatureTypes list. Pass ["TABLES"] and the response gains TABLE and CELL blocks. Pass ["FORMS"] and it gains KEY_VALUE_SET blocks representing key-value pairs. Pass ["QUERIES"] alongside a QueriesConfig and it gains QUERY and QUERY_RESULT blocks answering the specific questions you asked. Pass ["SIGNATURES"] and it flags detected signatures. These are additive - a single call can request ["TABLES", "FORMS", "QUERIES", "SIGNATURES"] together and get all four kinds of structure in one response.
What doesn't change between the two operations is the container. Both return a Blocks list, and every element in it - whether it's a LINE, a TABLE, or a QUERY_RESULT - is a Block object with an Id, a BlockType, a Confidence, geometry, and (for most types) a list of Relationships pointing at other blocks by Id.
Because everything lives in one flat array, working with structured output means finding the blocks you care about and following their relationships to the blocks that hold the data you actually want.
# --- Python (boto3) ---
import boto3
textract = boto3.client("textract")
with open("invoice.png", "rb") as f:
response = textract.analyze_document(
Document={"Bytes": f.read()},
FeatureTypes=["FORMS", "TABLES"],
)
blocks = response["Blocks"]
by_id = {block["Id"]: block for block in blocks}
# A KEY block's "VALUE" relationship points at its paired VALUE block's Id.
key_blocks = [b for b in blocks if b["BlockType"] == "KEY_VALUE_SET" and "KEY" in b.get("EntityTypes", [])]
print(f"found {len(key_blocks)} form keys, {len([b for b in blocks if b['BlockType'] == 'TABLE'])} tables")// --- TypeScript (AWS SDK v3) ---
import { TextractClient, AnalyzeDocumentCommand, Block } from "@aws-sdk/client-textract";
import { readFileSync } from "node:fs";
const textract = new TextractClient({});
const response = await textract.send(new AnalyzeDocumentCommand({
Document: { Bytes: readFileSync("invoice.png") },
FeatureTypes: ["FORMS", "TABLES"],
}));
const blocks: Block[] = response.Blocks ?? [];
const byId = new Map(blocks.map((b) => [b.Id, b]));
// A KEY block's "VALUE" relationship points at its paired VALUE block's Id.
const keyBlocks = blocks.filter((b) => b.BlockType === "KEY_VALUE_SET" && b.EntityTypes?.includes("KEY"));
console.log(`found ${keyBlocks.length} form keys, ${blocks.filter((b) => b.BlockType === "TABLE").length} tables`);Two things matter about this snippet. First, FeatureTypes is just a list on the same AnalyzeDocument call - there's no separate "forms API" and "tables API." Second, the useful work hasn't happened yet: knowing a KEY_VALUE_SET block exists tells you nothing about its text until you follow its relationships. A KEY block's VALUE relationship gives you the Id of its paired VALUE block, and both the KEY and VALUE blocks each carry a CHILD relationship pointing at the WORD blocks that make up their actual text. Getting from "there is a key" to "the key text is Invoice Date" means walking that chain - an Id-keyed lookup dictionary, like by_id / byId above, is the standard way to do it without repeated linear scans.
Tables follow the same pattern one level deeper: a TABLE block's CHILD relationship lists CELL block Ids, each CELL carries a RowIndex and ColumnIndex, and each cell's own CHILD relationship points at the WORD (or SELECTION_ELEMENT) blocks holding its text. Queries are simpler - a QUERY block's ANSWER relationship points directly at one or more QUERY_RESULT blocks, each already holding the answer text and a confidence score, no further graph-walking required.
| Need | Operation | FeatureTypes | Blocks added |
|---|---|---|---|
| Raw text, reading order only | DetectDocumentText | none | PAGE, LINE, WORD |
| Key-value form fields | AnalyzeDocument | FORMS | KEY_VALUE_SET |
| Table rows and columns | AnalyzeDocument | TABLES | TABLE, CELL |
| A specific question answered | AnalyzeDocument | QUERIES | QUERY, QUERY_RESULT |
| Signature presence | AnalyzeDocument | SIGNATURES | SIGNATURE |
The practical implication of this model is that Textract's job ends at "here is the graph," and your code's job is turning that graph into whatever shape your application actually wants - a dict of form fields, a list of table rows, a single extracted date. Libraries like amazon-textract-response-parser exist precisely because this graph-walking is repetitive enough to be worth wrapping, but understanding the raw block model matters even when you use one, since every wrapper is doing the same Id-relationship traversal underneath.
This also explains why combining FeatureTypes is cheap conceptually but not free in practice: each additional feature type adds its own block types to the same array, so a call requesting FORMS, TABLES, and QUERIES together returns a larger, denser graph than any one feature alone - plan your parsing code around the specific block types you requested, not "whatever comes back."
Block entries in one flat array, linked only by Id references in Relationships.DetectDocumentText is cheaper and sufficient when you only need raw text and reading order; AnalyzeDocument is for when you need forms, tables, queries, or signatures specifically.FeatureTypes is a list; TABLES, FORMS, QUERIES, and SIGNATURES can all be requested in a single call.CELL blocks are found via the TABLE block's CHILD relationship and carry their own RowIndex/ColumnIndex - array order is not table order.QUERY_RESULT blocks already contain the extracted answer text and a confidence score; you only need to follow the ANSWER relationship to find them.DetectDocumentText only returns PAGE, LINE, and WORD blocks - raw OCR with no structure. AnalyzeDocument returns the same plus whatever FeatureTypes you request (TABLES, FORMS, QUERIES, SIGNATURES), each adding its own block types to the response.
Flat. Every block - PAGE, LINE, WORD, TABLE, CELL, KEY_VALUE_SET, whatever - sits at the same level in one list. Structure comes entirely from Relationships entries that reference other blocks by Id.
A KEY block's VALUE relationship gives you its paired VALUE block's Id. Both the KEY and VALUE blocks separately have a CHILD relationship pointing at the WORD blocks that spell out their text - you concatenate those to get the readable string.
Yes. FeatureTypes accepts any combination of TABLES, FORMS, QUERIES, and SIGNATURES in one AnalyzeDocument request - you get every requested block type back in the same Blocks array.
You can, and this section shows how, but AWS also publishes amazon-textract-response-parser in both Python and JavaScript specifically to wrap this traversal into helper objects - useful once you're comfortable with what it's doing underneath.
No. Once you follow the QUERY block's ANSWER relationship to its QUERY_RESULT block, the answer text and confidence score are already there as fields - no further child traversal is needed, unlike forms or tables.
The same flat block model has to represent everything from a single word to a signature flag without a different response schema per feature. A generic Id-and-relationship graph lets one response shape carry arbitrarily different structures depending on which FeatureTypes you asked for.
DetectDocumentText call and raw block output.KEY_VALUE_SET and TABLE/CELL blocks in full.QUERY/QUERY_RESULT shortcut around graph-walking.Stack versions: This page was written for boto3 1.43.x (Python 3.10+) and the AWS SDK for JavaScript v3 (Node.js 18+).
Reviewed by Chris St. John·Last updated Jul 24, 2026