> This page is for version v2026-02-09 (default).
> For other versions, use one of these documentation indexes:
> - v2026-02-09 (default): https://docs.extend.ai/2026-02-09/llms.txt
> - v2025-04-21: https://docs.extend.ai/2025-04-21/llms.txt
> - v2024-12-23: https://docs.extend.ai/2024-12-23/llms.txt

> ## Documentation Index
> Fetch the complete documentation index at: https://docs.extend.ai/llms.txt
> Use this file to discover all available pages before exploring further.
>
> ## API version
> The current API version is `2026-02-09`, served at the site root (no version prefix in URLs).
> If this page URL contains `/2025-04-21/` or `/2024-12-23/`, you are reading an older API version.
> Prefer the current docs at https://docs.extend.ai/llms.txt unless the user explicitly needs that older version.
> Do not treat older-version pages as the source of truth for new integrations.

# Form Structured Output

> Return every form region as a typed tree of keys, filled values, empty fields, signatures, checkboxes, and tables, each with a bounding box, by setting blockOptions.keyValue.structuredOutputEnabled on client.parse (Python, TypeScript, Java, Go). Covers reading details.form from key_value blocks and flattening it into key-value pairs.

**Form structured output** returns each form region in a document as a tree you can walk in code. Our form and form-controls models work together to pair every printed label with its answer, find each input, checkbox, and signature line (including fields left blank), and locate every one of them on the page. Turn it on with one option. The markdown `content` of each block doesn't change; every `key_value` block gains a `details.form` tree alongside it.

Use it when you need label/value pairs, checkbox states, or per-field coordinates, for example to map tax or intake forms into your own data model, feed an LLM typed fields instead of markdown, or draw highlights over individual answers.

> **Note**
>
> Requires `parse_performance` >= `2.0.0`. Structured output applies to regions detected as `key_value` blocks; a box grid that the layout model detects as a `table` keeps its values in that table block's `content`.

## Via the API

### Enable structured output

Set `config.blockOptions.keyValue.structuredOutputEnabled` to `true`. It works on the synchronous `parse` call shown here and on async [parse runs](/api-reference/endpoints/parse/create-parse-run).

#### Python

```python
from extend_ai import Extend

client = Extend()

response = client.parse(
    file={"url": "https://example.com/patient-intake.pdf"},
    config={
        "blockOptions": {
            "keyValue": {"structuredOutputEnabled": True},
        },
    },
)
```

#### TypeScript

```typescript
import { ExtendClient } from "extend-ai";

const client = new ExtendClient();

const response = await client.parse({
  file: { url: "https://example.com/patient-intake.pdf" },
  config: {
    blockOptions: {
      keyValue: { structuredOutputEnabled: true },
    },
  },
});
```

#### Java

```java
import ai.extend.ExtendClient;
import ai.extend.requests.ParseRequest;
import ai.extend.types.*;

ExtendClient client = ExtendClient.builder().build();

ParseRun response = client.parse(ParseRequest.builder()
    .file(ParseRequestFile.of(FileFromUrl.builder()
        .url("https://example.com/patient-intake.pdf")
        .build()))
    .config(ParseConfig.builder()
        .blockOptions(ParseConfigBlockOptions.builder()
            .keyValue(ParseConfigBlockOptionsKeyValue.builder()
                .structuredOutputEnabled(true)
                .build())
            .build())
        .build())
    .build());
```

#### Go

```go
package main

import (
	"context"
	"fmt"
	"log"
	"strings"

	extend "github.com/extend-hq/extend-go-sdk"
	client "github.com/extend-hq/extend-go-sdk/client"
)

func main() {
	c := client.NewClient()

	response, err := c.Parse(context.TODO(), &extend.ParseRequest{
		File: &extend.ParseRequestFile{
			FileFromURL: &extend.FileFromURL{URL: "https://example.com/patient-intake.pdf"},
		},
		Config: &extend.ParseConfig{
			BlockOptions: &extend.ParseConfigBlockOptions{
				KeyValue: &extend.ParseConfigBlockOptionsKeyValue{
					StructuredOutputEnabled: extend.Bool(true),
				},
			},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	// Read the form tree (next step).
}
```

#### cURL

```bash
curl -X POST "https://api.extend.ai/parse" \
  -H "x-extend-api-version: 2026-02-09" \
  -H "Authorization: Bearer $EXTEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "file": { "url": "https://example.com/patient-intake.pdf" },
    "config": {
      "blockOptions": { "keyValue": { "structuredOutputEnabled": true } }
    }
  }'
```

### Read the form tree

Each `key_value` block carries `details.form`, a list of [nodes](#node-reference) in reading order. To turn the tree into flat key-value pairs, walk it and join the labels on the way down: every `key` adds its `content` to the path, `selection_group` and `table` nodes pass through, and every other node is an answer for the current path.

#### Python

```python
def form_pairs(nodes, path=(), key_box=None):
    for node in nodes:
        if node.type == "key":
            yield from form_pairs(node.children, (*path, node.content), node.bounding_box)
        elif node.type in ("selection_group", "table"):
            yield from form_pairs(node.children, path, key_box)
        else:
            yield {
                "key": " > ".join(path),
                "type": node.type,
                "value": node.content,
                "selected": node.selected,
                "key_box": key_box,
                "value_box": node.bounding_box,
            }


for chunk in response.output.chunks:
    for block in chunk.blocks:
        if block.type == "key_value" and block.details.form:
            for pair in form_pairs(block.details.form):
                value = ("[x]" if pair["selected"] else "[ ]") if pair["type"] == "selection" else pair["value"]
                print(f"{pair['key']} ({pair['type']}) = {value}")
```

#### TypeScript

```typescript
import { Extend } from "extend-ai";

interface FormPair {
  key: string;
  type: Extend.KeyValueFormNodeType;
  value: string;
  selected: boolean | null;
  keyBox: Extend.BoundingBox | null;
  valueBox: Extend.BoundingBox | null;
}

function formPairs(
  nodes: Extend.KeyValueFormNode[],
  path: string[] = [],
  keyBox: Extend.BoundingBox | null = null,
): FormPair[] {
  return nodes.flatMap((node) => {
    if (node.type === "key") return formPairs(node.children, [...path, node.content], node.boundingBox);
    if (node.type === "selection_group" || node.type === "table") return formPairs(node.children, path, keyBox);
    return [
      {
        key: path.join(" > "),
        type: node.type,
        value: node.content,
        selected: node.selected,
        keyBox,
        valueBox: node.boundingBox,
      },
    ];
  });
}

for (const chunk of response.output?.chunks ?? []) {
  for (const block of chunk.blocks) {
    if (block.details.type === "key_value_details" && block.details.form) {
      for (const pair of formPairs(block.details.form)) {
        const value = pair.type === "selection" ? (pair.selected ? "[x]" : "[ ]") : pair.value;
        console.log(`${pair.key} (${pair.type}) = ${value}`);
      }
    }
  }
}
```

#### Java

```java
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

record FormPair(String key, KeyValueFormNodeType type, String value, Optional<Boolean> selected,
                Optional<BoundingBox> keyBox, Optional<BoundingBox> valueBox) {}

static void formPairs(List<KeyValueFormNode> nodes, List<String> path,
                      Optional<BoundingBox> keyBox, List<FormPair> out) {
    for (KeyValueFormNode node : nodes) {
        KeyValueFormNodeType type = node.getType();
        if (type.equals(KeyValueFormNodeType.KEY)) {
            List<String> next = new ArrayList<>(path);
            next.add(node.getContent());
            formPairs(node.getChildren(), next, node.getBoundingBox(), out);
        } else if (type.equals(KeyValueFormNodeType.SELECTION_GROUP) || type.equals(KeyValueFormNodeType.TABLE)) {
            formPairs(node.getChildren(), path, keyBox, out);
        } else {
            out.add(new FormPair(String.join(" > ", path), type, node.getContent(),
                node.getSelected(), keyBox, node.getBoundingBox()));
        }
    }
}

for (Chunk chunk : response.getOutput().get().getChunks()) {
    for (Block block : chunk.getBlocks()) {
        if (!block.getType().equals(BlockType.KEY_VALUE)) continue;
        KeyValueDetails details = (KeyValueDetails) block.getDetails().get();
        List<FormPair> pairs = new ArrayList<>();
        formPairs(details.getForm().orElse(List.of()), List.of(), Optional.empty(), pairs);
        for (FormPair pair : pairs) {
            String value = pair.type().equals(KeyValueFormNodeType.SELECTION)
                ? (pair.selected().orElse(false) ? "[x]" : "[ ]")
                : pair.value();
            System.out.println(pair.key() + " (" + pair.type() + ") = " + value);
        }
    }
}
```

#### Go

```go
type FormPair struct {
	Key      string
	Type     extend.KeyValueFormNodeType
	Value    string
	Selected *bool
	KeyBox   *extend.BoundingBox
	ValueBox *extend.BoundingBox
}

func formPairs(nodes []*extend.KeyValueFormNode, path []string, keyBox *extend.BoundingBox) []FormPair {
	var pairs []FormPair
	for _, node := range nodes {
		switch node.Type {
		case extend.KeyValueFormNodeTypeKey:
			next := append(append([]string{}, path...), node.Content)
			pairs = append(pairs, formPairs(node.Children, next, node.BoundingBox)...)
		case extend.KeyValueFormNodeTypeSelectionGroup, extend.KeyValueFormNodeTypeTable:
			pairs = append(pairs, formPairs(node.Children, path, keyBox)...)
		default:
			pairs = append(pairs, FormPair{
				Key:      strings.Join(path, " > "),
				Type:     node.Type,
				Value:    node.Content,
				Selected: node.Selected,
				KeyBox:   keyBox,
				ValueBox: node.BoundingBox,
			})
		}
	}
	return pairs
}

// Inside main, after the parse call:
for _, chunk := range response.Output.Chunks {
	for _, block := range chunk.Blocks {
		kv := block.Details.GetKeyValueDetails()
		if kv == nil {
			continue
		}
		for _, pair := range formPairs(kv.Form, nil, nil) {
			value := pair.Value
			if pair.Type == extend.KeyValueFormNodeTypeSelection {
				value = "[ ]"
				if pair.Selected != nil && *pair.Selected {
					value = "[x]"
				}
			}
			fmt.Printf("%s (%s) = %s\n", pair.Key, pair.Type, value)
		}
	}
}
```

For the [patient intake form](#patient-intake-form) below, this prints:

```text
Patient name (text_field) = Maria L. Gonzalez
Date of birth > Month (text_field) = 04
Date of birth > Day (text_field) = 17
Date of birth > Year (text_field) = 1986
Phone (text_field) = (510) 555-0142
Email (empty_field) = <blank />
1. Do you have any known allergies? > Yes (selection) = [ ]
1. Do you have any known allergies? > No (selection) = [x]
2. Are you currently taking any medications? > Yes (selection) = [x]
2. Are you currently taking any medications? > No (selection) = [ ]
2. Are you currently taking any medications? > If yes, list medications and dosage (text_field) = Lisinopril 10 mg daily
3. Preferred contact method > Phone (selection) = [ ]
3. Preferred contact method > Email (selection) = [ ]
3. Preferred contact method > Text message (selection) = [x]
Insurance provider (text_field) = Blue Shield of California
Member ID (text_field) = XGB884213307
Group number (empty_field) = <blank />
Patient signature (signature_field) =
Date (text_field) = 10/28/2026
```

## Via Extend Studio

In the [Parse playground](https://dashboard.extend.ai/studio), open the configuration panel and turn on **Structured key-value output** in the key-value block options. Hover a key-value line in the output to see the boxes for its label and answer. Use **View code** to copy the matching `config` JSON.

## Node reference

Every node has the same six fields, so you can branch on `type` without checking which keys are present.

| Field         | Type            | Description                                                                                                                           |
| ------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `id`          | string          | Deterministic per block and position: the same node in the same block always gets the same `id`.                                      |
| `type`        | string          | What the node is (see below).                                                                                                         |
| `content`     | string          | The node's text. Always set; `""` where a node has no text.                                                                           |
| `boundingBox` | object \| null  | `left`, `top`, `right`, `bottom` in the same coordinate space as the block's `boundingBox`. `null` when the node couldn't be located. |
| `selected`    | boolean \| null | `true` or `false` on `selection` nodes; `null` on every other type.                                                                   |
| `children`    | array           | Child nodes. Always an array; empty on leaves.                                                                                        |

| `type`            | What it is                                                                                                   | `content`                                                       | `boundingBox`                         |
| ----------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | ------------------------------------- |
| `key`             | Printed text: a label, heading, instruction, or option label. Its answers and sub-labels are its `children`. | The text                                                        | The text                              |
| `text_field`      | A filled-in answer                                                                                           | The answer, with printed units kept (`"25.0000 %"`)             | The answer                            |
| `empty_field`     | An answer slot left empty                                                                                    | `"<blank />"` plus any printed symbol or unit (`"$ <blank />"`) | The empty input area                  |
| `signature_field` | A signature                                                                                                  | The signer's name if printed, otherwise `""`                    | The signature                         |
| `selection_group` | The options that answer one key                                                                              | `""`                                                            | The area around its options and marks |
| `selection`       | A checkbox, radio button, or circled-option mark                                                             | `""`                                                            | The mark                              |
| `table`           | A grid inside the form                                                                                       | The grid as HTML                                                | The grid                              |

How the tree is shaped:

* **Answers nest under their label.** A value is a `text_field` child of its `key`. Labels nest too: a section heading or question is a `key` whose children are the field labels under it (`Date of birth > Month > 04`).
* **Options are labels with a mark.** Each printed option is a `key` (the option text) whose first child is its `selection` (the mark). The options for one question sit together in a `selection_group`. A "check here" box with no printed option text is a bare `selection` in the group. Multi-select questions have the same shape, with more than one `selected: true`.
* **Tables are row > column > cell.** A `table` node's `content` holds the HTML; its `children` are row-header `key` > column-header `key` > `text_field` or `empty_field`. A column header that's printed once is repeated under each row, with the same box.
* **Coordinates match the block.** Boxes use the same units as the block's `boundingBox` and `metadata.page.width` / `height`; divide by those for page fractions. See [Bounding box coordinates](/parsing/response-format#bounding-box-coordinates).

## Examples

The examples below are trimmed from real runs; `id`s are omitted for brevity.

### Patient intake form

![A one-page patient intake form with filled name, date of birth in Month, Day, and Year boxes, phone, an empty email line, three Yes/No and multiple-choice checkbox questions, insurance fields with an empty group number, and a handwritten signature with a date](/_fern-files/_img/7513a2f0127b209087016f88bcfd8f807858766f13f334a9271e7e8ae4592187.webp)

**Values, nested labels, and blanks.** The date of birth's Month, Day, and Year captions are keys under `Date of birth`, each with its own value. The unfilled email line is an `empty_field` with the box of the empty input.

```json
[
  { "type": "key", "content": "Patient name", "boundingBox": { "left": 111, "top": 228, "right": 238, "bottom": 249 }, "selected": null, "children": [
    { "type": "text_field", "content": "Maria L. Gonzalez", "boundingBox": { "left": 280, "top": 221, "right": 460, "bottom": 247 }, "selected": null, "children": [] }
  ] },
  { "type": "key", "content": "Date of birth", "boundingBox": { "left": 728, "top": 228, "right": 845, "bottom": 249 }, "selected": null, "children": [
    { "type": "key", "content": "Month", "boundingBox": { "left": 878, "top": 262, "right": 922, "bottom": 277 }, "selected": null, "children": [
      { "type": "text_field", "content": "04", "boundingBox": { "left": 883, "top": 226, "right": 910, "bottom": 245 }, "selected": null, "children": [] }
    ] },
    { "type": "key", "content": "Day", "boundingBox": { "left": 953, "top": 262, "right": 982, "bottom": 279 }, "selected": null, "children": [
      { "type": "text_field", "content": "17", "boundingBox": { "left": 961, "top": 225, "right": 986, "bottom": 246 }, "selected": null, "children": [] }
    ] },
    { "type": "key", "content": "Year", "boundingBox": { "left": 1028, "top": 262, "right": 1061, "bottom": 277 }, "selected": null, "children": [
      { "type": "text_field", "content": "1986", "boundingBox": { "left": 1034, "top": 225, "right": 1082, "bottom": 245 }, "selected": null, "children": [] }
    ] }
  ] },
  { "type": "key", "content": "Email", "boundingBox": { "left": 727, "top": 311, "right": 782, "bottom": 332 }, "selected": null, "children": [
    { "type": "empty_field", "content": "<blank />", "boundingBox": { "left": 813, "top": 298, "right": 1164, "bottom": 336 }, "selected": null, "children": [] }
  ] }
]
```

**A Yes/No question.** Each option label is a `key` with its mark as the first child; the `selection` box is the checkbox itself, not the label.

```json
{ "type": "key", "content": "1. Do you have any known allergies?", "boundingBox": { "left": 112, "top": 452, "right": 455, "bottom": 477 }, "selected": null, "children": [
  { "type": "selection_group", "content": "", "boundingBox": { "left": 687, "top": 453, "right": 846, "bottom": 474 }, "selected": null, "children": [
    { "type": "key", "content": "Yes", "boundingBox": { "left": 714, "top": 454, "right": 751, "bottom": 472 }, "selected": null, "children": [
      { "type": "selection", "content": "", "boundingBox": { "left": 687, "top": 453, "right": 707, "bottom": 474 }, "selected": false, "children": [] }
    ] },
    { "type": "key", "content": "No", "boundingBox": { "left": 817, "top": 454, "right": 846, "bottom": 473 }, "selected": null, "children": [
      { "type": "selection", "content": "", "boundingBox": { "left": 791, "top": 453, "right": 811, "bottom": 474 }, "selected": true, "children": [] }
    ] }
  ] }
] }
```

**A signature.** The signature has no printed name, so `content` is `""`; its box covers the handwritten mark above the caption.

```json
[
  { "type": "key", "content": "Patient signature", "boundingBox": { "left": 111, "top": 1031, "right": 240, "bottom": 1051 }, "selected": null, "children": [
    { "type": "signature_field", "content": "", "boundingBox": { "left": 112, "top": 950, "right": 626, "bottom": 1022 }, "selected": null, "children": [] }
  ] },
  { "type": "key", "content": "Date", "boundingBox": { "left": 706, "top": 1031, "right": 745, "bottom": 1048 }, "selected": null, "children": [
    { "type": "text_field", "content": "10/28/2026", "boundingBox": { "left": 720, "top": 994, "right": 827, "bottom": 1016 }, "selected": null, "children": [] }
  ] }
]
```

### Tax form: Schedule K-1 (Form 1065)

![Part II of a filled Schedule K-1 showing item G with Limited partner checked, H1 with Domestic partner checked, an unchecked I2 retirement plan box, and the item J table of profit, loss, and capital percentages](/_fern-files/_img/e134753dd32a6383e3bdb34697d7e60bcbe60b4d12135520f11810fd1f5d423c.webp)

**One of several options checked, and a lone "check here" box.** Item G's two options share one `selection_group` under the `G` key. Item I2 has no printed option text, so its `selection` sits directly in the group.

```json
[
  { "type": "key", "content": "G", "boundingBox": { "left": 83, "top": 707, "right": 97, "bottom": 720 }, "selected": null, "children": [
    { "type": "selection_group", "content": "", "boundingBox": { "left": 119, "top": 704, "right": 592, "bottom": 741 }, "selected": null, "children": [
      { "type": "key", "content": "General partner or LLC member-manager", "boundingBox": { "left": 148, "top": 706, "right": 302, "bottom": 741 }, "selected": null, "children": [
        { "type": "selection", "content": "", "boundingBox": { "left": 119, "top": 704, "right": 138, "bottom": 722 }, "selected": false, "children": [] }
      ] },
      { "type": "key", "content": "Limited partner or other LLC member", "boundingBox": { "left": 403, "top": 707, "right": 592, "bottom": 740 }, "selected": null, "children": [
        { "type": "selection", "content": "", "boundingBox": { "left": 374, "top": 704, "right": 392, "bottom": 722 }, "selected": true, "children": [] }
      ] }
    ] }
  ] },
  { "type": "key", "content": "I2 If this partner is a retirement plan (IRA/SEP/Keogh/etc.), check here .", "boundingBox": { "left": 82, "top": 858, "right": 559, "bottom": 875 }, "selected": null, "children": [
    { "type": "selection_group", "content": "", "boundingBox": { "left": 612, "top": 856, "right": 631, "bottom": 874 }, "selected": null, "children": [
      { "type": "selection", "content": "", "boundingBox": { "left": 612, "top": 856, "right": 631, "bottom": 874 }, "selected": false, "children": [] }
    ] }
  ] }
]
```

**A table inside the form.** Item J's grid is a `table` node. Its children are row > column > cell, so each percentage flattens to a path like `J … > Profit > Beginning = 25.0000 %`. The `Loss` and `Capital` rows (trimmed here) have the same shape.

```json
{ "type": "key", "content": "J Partner's share of profit, loss, and capital (see instructions):", "boundingBox": { "left": 84, "top": 883, "right": 507, "bottom": 900 }, "selected": null, "children": [
  { "type": "table", "content": "<table><tr><th></th><th>Beginning</th><th>Ending</th></tr><tr><td>Profit</td><td>25.0000 %</td><td>25.0000 %</td></tr><tr><td>Loss</td><td>25.0000 %</td><td>25.0000 %</td></tr><tr><td>Capital</td><td>25.0000 %</td><td>25.0000 %</td></tr></table>", "boundingBox": { "left": 118, "top": 903, "right": 597, "bottom": 999 }, "selected": null, "children": [
    { "type": "key", "content": "Profit", "boundingBox": { "left": 118, "top": 934, "right": 156, "bottom": 948 }, "selected": null, "children": [
      { "type": "key", "content": "Beginning", "boundingBox": { "left": 228, "top": 903, "right": 301, "bottom": 921 }, "selected": null, "children": [
        { "type": "text_field", "content": "25.0000 %", "boundingBox": { "left": 295, "top": 927, "right": 372, "bottom": 947 }, "selected": null, "children": [] }
      ] },
      { "type": "key", "content": "Ending", "boundingBox": { "left": 477, "top": 904, "right": 529, "bottom": 920 }, "selected": null, "children": [
        { "type": "text_field", "content": "25.0000 %", "boundingBox": { "left": 519, "top": 927, "right": 597, "bottom": 947 }, "selected": null, "children": [] }
      ] }
    ] }
  ] }
] }
```

Empty cells keep their printed units. Item K1's unfilled liability amounts come back as `{ "type": "empty_field", "content": "$ <blank />" }` under their row and column keys.

## Reference

* [Create Parse Run](/api-reference/endpoints/parse/create-parse-run) and [Parse File (sync)](/api-reference/endpoints/parse/parse-file): `config.blockOptions.keyValue.structuredOutputEnabled` and the `KeyValueFormNode` schema
* [`blockOptions.keyValue.structuredOutputEnabled`](/parsing/configuration#blockoptionskeyvaluestructuredoutputenabled) in the configuration reference
* [Response Format](/parsing/response-format): blocks, `details`, and bounding box coordinates