Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .fern/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"enum_type": "forward_compatible_python_enums"
}
},
"originGitCommit": "094c36681badc82afeb39a2c91a716461a5a0bd2",
"sdkVersion": "1.16.0"
"originGitCommit": "a5c1143a7b724689db7c5e4cee897834882c9d69",
"sdkVersion": "1.17.0"
}
12 changes: 9 additions & 3 deletions .fern/replay.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,77 @@ edit_run = client.edit(

> **Note:** The synchronous methods above have a 5-minute timeout and are best suited for onboarding and testing. For production workloads, use [polling helpers](#polling-helpers) or [webhooks](#webhook-verification) instead.

## Typed extraction with Pydantic

The SDK supports [pydantic](https://docs.pydantic.dev/) models for fully typed extraction -- define your schema once and get end-to-end type safety from request to response:

```python
from typing import List, Optional

from pydantic import BaseModel, Field

from extend_ai import Extend, ExtendCurrency, ExtendDate

class LineItem(BaseModel):
description: Optional[str] = None
amount: Optional[ExtendCurrency] = None

class Invoice(BaseModel):
invoice_number: Optional[str] = Field(None, description="The invoice number")
invoice_date: ExtendDate = Field(None, description="The invoice date")
line_items: List[LineItem] = Field(default_factory=list, description="Line items on the invoice")
total: Optional[ExtendCurrency] = Field(None, description="Total amount due")

client = Extend(token="YOUR_API_KEY")

result = client.extract(
file={"url": "https://example.com/invoice.pdf"},
config={"schema": Invoice},
)

# output.value is a validated Invoice instance
if result.status == "PROCESSED" and result.output is not None:
invoice = result.output.value
print(invoice.invoice_number) # str | None
print(invoice.invoice_date) # datetime.date | None
if invoice.total is not None:
print(invoice.total.amount) # float | None
print(invoice.total.iso_4217_currency_code) # str | None
```

The model is converted to [Extend's JSON Schema format](https://docs.extend.ai/2026-02-09/extraction/schema) for the request, and the extraction output is validated back into model instances. Use `Field(description=...)` to guide the extraction.

Primitive, enum, and date fields must be declared `Optional` -- extraction can return `null` for any field, so a non-Optional field raises `SchemaConversionError` before any request is sent. In the unlikely event that a completed run's output fails model validation, the SDK raises `ExtractOutputValidationError`, which preserves the completed run (including its raw output) on the error's `run` attribute.

Pydantic model schemas are accepted everywhere an extraction schema can be provided:

```python
# Polling (see below), including extractor config overrides
result = client.extract_runs.create_and_poll(
file={"url": "https://example.com/invoice.pdf"},
config={"schema": Invoice},
)

# Creating and updating extractors
extractor = client.extractors.create(name="Invoice Extractor", config={"schema": Invoice})
client.extractors.update(extractor.id, config={"schema": Invoice})

# Publishing extractor versions
client.extractor_versions.create(extractor.id, release_type="major", config={"schema": Invoice})
```

### Custom field types

The SDK provides field types for Extend-specific extraction behavior:

| Type | Output type | Description |
|---|---|---|
| `ExtendDate` | `datetime.date \| None` | ISO date (plain `datetime.date` annotations work too) |
| `ExtendCurrency` | `ExtendCurrency(amount, iso_4217_currency_code)` | Currency with amount and code |
| `ExtendSignature` | `ExtendSignature(printed_name, signature_date, is_signed, title_or_role)` | Signature detection |

Supported field types: `Optional[str]`, `Optional[float]`, `Optional[int]`, `Optional[bool]`, `Optional[datetime.date]`, `Optional[Literal[...]]` / string enums (converted to nullable enums), nested models, and lists of these (list items are non-Optional, e.g. `List[str]`). Unsupported constructs (non-Optional unions, dicts, recursive models, field aliases, etc.) raise `SchemaConversionError`.

## Polling helpers

Every run resource exposes a `create_and_poll()` method that creates the run and automatically polls until it reaches a terminal state (`PROCESSED`, `FAILED`, or `CANCELLED`):
Expand Down
10 changes: 5 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ dynamic = ["version"]

[tool.poetry]
name = "extend_ai"
version = "1.16.0"
version = "1.17.0"
description = ""
readme = "README.md"
authors = []
Expand Down Expand Up @@ -40,7 +40,8 @@ python = "^3.8"
httpx = ">=0.21.2"
pydantic = ">= 1.9.2"
pydantic-core = ">=2.18.2"
typing_extensions = ">= 4.0.0"
# Generic TypedDicts (used by the typed extraction wrappers) require >= 4.3.0
typing_extensions = ">= 4.3.0"

[tool.poetry.group.dev.dependencies]
mypy = "==1.13.0"
Expand Down
24 changes: 20 additions & 4 deletions reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -7148,7 +7148,9 @@ Example: `"invoice"`
<dl>
<dd>

Run a workflow with a file. A workflow is a sequence of steps that process files and data in a specific order to achieve a desired outcome.
Run a workflow. A workflow is a sequence of steps that process files and data in a specific order to achieve a desired outcome.

Pass `file` for a single document, or `package` to process 2-50 files together as one package in a single run. Exactly one of `file` or `package` must be provided.

The request returns immediately with a `PROCESSING` status. Use webhooks or poll the Get Workflow Run endpoint for results.
</dd>
Expand All @@ -7172,7 +7174,13 @@ client = Extend(
)
client.workflow_runs.create(
workflow={"id": "wf_1234567890"},
file={"url": "https://example.com/invoice.pdf"},
package={
"files": [
{"url": "https://example.com/invoice.pdf"},
{"url": "https://example.com/bill-of-lading.pdf"},
{"id": "file_xK9mLPqRtN3vS8wF5hB2cQ"},
]
},
)

```
Expand All @@ -7197,15 +7205,23 @@ client.workflow_runs.create(
<dl>
<dd>

**file:** `WorkflowRunsCreateRequestFileParams` — The file to be processed. Supported file types can be found [here](https://docs.extend.ai/2026-02-09/general/supported-file-types). Files can be provided as a URL, an Extend file ID, or raw text. If you wish to process more at a time, consider using the [Batch Run Workflow](https://docs.extend.ai/2026-02-09/api-reference/endpoints/workflow/batch-create-workflow-runs) endpoint.
**file:** `typing.Optional[WorkflowRunsCreateRequestFileParams]` — The file to be processed. Supported file types can be found [here](https://docs.extend.ai/2026-02-09/general/supported-file-types). Files can be provided as a URL, an Extend file ID, or raw text. Mutually exclusive with `package` — provide one or the other. If you wish to process many files as independent runs, consider using the [Batch Run Workflow](https://docs.extend.ai/2026-02-09/api-reference/endpoints/workflow/batch-create-workflow-runs) endpoint.

</dd>
</dl>

<dl>
<dd>

**package:** `typing.Optional[WorkflowRunPackageParams]` — A set of 2–50 files to process together in a single workflow run. Mutually exclusive with `file` — provide one or the other.

</dd>
</dl>

<dl>
<dd>

**outputs:** `typing.Optional[typing.Sequence[WorkflowRunsCreateRequestOutputsItemParams]]` — Predetermined outputs to be used for the workflow run. Generally not recommended for most use cases, however, can be useful in cases of overriding a classification in a workflow, or a subset of extraction fields when data is known.
**outputs:** `typing.Optional[typing.Sequence[WorkflowRunsCreateRequestOutputsItemParams]]` — Predetermined outputs to be used for the workflow run. Generally not recommended for most use cases, however, can be useful in cases of overriding a classification in a workflow, or a subset of extraction fields when data is known. Not supported on package runs — a package run produces a single merged result across all files and cannot accept pre-supplied per-processor outputs.

</dd>
</dl>
Expand Down
35 changes: 34 additions & 1 deletion src/extend_ai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,22 @@
ClassifyRunsListResponse,
ClassifyRunsListResponseParams,
)
from .wrapper import AsyncExtend, Extend, Webhooks, PollingOptions, PollingTimeoutError
from .wrapper import (
AsyncExtend,
Extend,
ExtendCurrency,
ExtendDate,
ExtendSignature,
ExtractOutputValidationError,
PollingOptions,
PollingTimeoutError,
SchemaConversionError,
TypedExtractOutput,
TypedExtractRun,
Webhooks,
parse_extract_run,
pydantic_to_extend_schema,
)
from .edit_runs import (
EditRunsCreateRequestFile,
EditRunsCreateRequestFileParams,
Expand Down Expand Up @@ -1102,6 +1117,15 @@
"Webhooks": ".wrapper",
"PollingOptions": ".wrapper",
"PollingTimeoutError": ".wrapper",
"ExtendCurrency": ".wrapper",
"ExtendDate": ".wrapper",
"ExtendSignature": ".wrapper",
"ExtractOutputValidationError": ".wrapper",
"SchemaConversionError": ".wrapper",
"TypedExtractOutput": ".wrapper",
"TypedExtractRun": ".wrapper",
"parse_extract_run": ".wrapper",
"pydantic_to_extend_schema": ".wrapper",
"ExtendEnvironment": ".environment",
"ExternalDataValidationResult": ".types",
"ExternalDataValidationResultParams": ".requests",
Expand Down Expand Up @@ -1985,10 +2009,19 @@ def __dir__():
"ExcelSheetRange",
"ExcelSheetRangeParams",
"Extend",
"ExtendCurrency",
"ExtendDate",
"ExtendEnvironment",
"ExtendSignature",
"ExtractOutputValidationError",
"Webhooks",
"PollingOptions",
"PollingTimeoutError",
"SchemaConversionError",
"TypedExtractOutput",
"TypedExtractRun",
"parse_extract_run",
"pydantic_to_extend_schema",
"ExternalDataValidationResult",
"ExternalDataValidationResultParams",
"ExternalDataValidationResultResponse",
Expand Down
4 changes: 2 additions & 2 deletions src/extend_ai/core/client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ def get_headers(self) -> typing.Dict[str, str]:
import platform

headers: typing.Dict[str, str] = {
"User-Agent": "extend_ai/1.16.0",
"User-Agent": "extend_ai/1.17.0",
"X-Fern-Language": "Python",
"X-Fern-Runtime": f"python/{platform.python_version()}",
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
"X-Fern-SDK-Name": "extend_ai",
"X-Fern-SDK-Version": "1.16.0",
"X-Fern-SDK-Version": "1.17.0",
**(self.get_custom_headers() or {}),
}
headers["Authorization"] = f"Bearer {self._get_token()}"
Expand Down
Loading
Loading