# Delete Document Packet
Source: https://docs.anyformat.ai/api-reference-v3/document-packets/delete
DELETE /v3/document-packets/{document_packet_id}/
Delete a document packet, its files, and its extraction results
*Rate limit tier: **general** (600 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Deletes a [document packet](/concepts/document-packets) and every file in it, along with any extraction results. This action is irreversible.
Returns `204 No Content` on success. Unknown ids — including packets belonging to another organization — return `404`.
```bash curl theme={null}
curl -X DELETE 'https://api.anyformat.ai/v3/document-packets/069dcc2c-e14c-7606-8000-2ee4fb17b4e1/' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```python Python (requests) theme={null}
import requests
document_packet_id = "069dcc2c-e14c-7606-8000-2ee4fb17b4e1"
url = f"https://api.anyformat.ai/v3/document-packets/{document_packet_id}/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
response = requests.delete(url, headers=headers)
assert response.status_code == 204
```
# Get Document Packet
Source: https://docs.anyformat.ai/api-reference-v3/document-packets/get
GET /v3/document-packets/{document_packet_id}/
Retrieve one document packet: status, files, and its latest run id
*Rate limit tier: **general** (600 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Retrieves one [document packet](/concepts/document-packets) flat: extraction status, timestamps, the files it groups, and `latest_run_id` — the id of the packet's most recent [run](/concepts/runs-and-results), `null` until the packet has been run.
`latest_run_id` is the hop to results: follow it with [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get). Run history is not embedded — list a workflow's runs via [`GET /v3/workflows/{workflow_id}/runs/`](/api-reference-v3/runs/list).
Unknown ids — including packets belonging to another organization — return `404`.
```bash curl theme={null}
curl -X GET 'https://api.anyformat.ai/v3/document-packets/069dcc2c-e14c-7606-8000-2ee4fb17b4e1/' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```python Python (requests) theme={null}
import requests
document_packet_id = "069dcc2c-e14c-7606-8000-2ee4fb17b4e1"
url = f"https://api.anyformat.ai/v3/document-packets/{document_packet_id}/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
packet = requests.get(url, headers=headers).json()
print(packet["status"], packet["latest_run_id"])
```
```json Response (200 OK) theme={null}
{
"id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"workflow_id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"name": "contract-bundle",
"status": "processed",
"created_at": "2026-07-01T12:00:00.000Z",
"updated_at": "2026-07-01T12:05:00.000Z",
"files": [
{ "id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e2", "name": "contract.pdf" },
{ "id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e3", "name": "annex-a.pdf" }
],
"latest_run_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4f9"
}
```
# List Document Packets
Source: https://docs.anyformat.ai/api-reference-v3/document-packets/list
GET /v3/workflows/{workflow_id}/document-packets/
List a workflow's document packets, newest first
*Rate limit tier: **general** (600 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Returns one keyset page of a workflow's [document packets](/concepts/document-packets), newest first. Follow `next_cursor` until it is `null` (see [Pagination](/api-reference-v3/introduction#pagination)).
Items are slim summaries — id, name, `status`, timestamps. Fetch [`GET /v3/document-packets/{document_packet_id}/`](/api-reference-v3/document-packets/get) for the per-file breakdown and `latest_run_id`.
Packet `status` reflects the packet's most recent extraction: `not_started` (never run), `queued`, `in_progress`, `processed`, `error`, or `cancelled`.
```bash curl theme={null}
curl -X GET 'https://api.anyformat.ai/v3/workflows/0686bb97-8c30-70f0-8000-97669e000eb8/document-packets/?limit=20' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```python Python (requests) theme={null}
import requests
workflow_id = "0686bb97-8c30-70f0-8000-97669e000eb8"
url = f"https://api.anyformat.ai/v3/workflows/{workflow_id}/document-packets/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
page = requests.get(url, headers=headers, params={"limit": 20}).json()
for packet in page["items"]:
print(packet["id"], packet["status"])
```
```json Response (200 OK) theme={null}
{
"items": [
{
"id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"workflow_id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"name": "invoice-1043.pdf",
"status": "processed",
"created_at": "2026-07-01T12:00:00.000Z",
"updated_at": "2026-07-01T12:05:00.000Z"
},
{
"id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e4",
"workflow_id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"name": "invoice-1044.pdf",
"status": "not_started",
"created_at": "2026-07-01T11:00:00.000Z",
"updated_at": "2026-07-01T11:00:00.000Z"
}
],
"next_cursor": null
}
```
# Run Document Packet
Source: https://docs.anyformat.ai/api-reference-v3/document-packets/run
POST /v3/document-packets/{document_packet_id}/run/
Run (or re-run) a document packet on the latest version of its workflow
*Rate limit tier: **submission** (60 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Runs — or **re-runs** — a [document packet](/concepts/document-packets) on the latest version of its workflow. The path is flat: the packet already knows which workflow it belongs to, so no `workflow_id` appears in the URL.
**Every call creates a new run.** Re-running after editing the workflow is the intended flow — same document, another attempt — and earlier runs stay readable at [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get) with their own results. Re-runs are metered like fresh runs (see [How credits work](/concepts/how-credits-work)).
The only exception is an `Idempotency-Key` replay: retrying with the same key returns the **original** run instead of triggering (and billing) a second extraction. See [Idempotency](/api-reference-v3/introduction#idempotency).
Unknown ids — including packets belonging to another organization — return `404`. An organization without extraction credit receives `402 PAYMENT_REQUIRED`.
```bash curl theme={null}
curl -X POST 'https://api.anyformat.ai/v3/document-packets/069dcc2c-e14c-7606-8000-2ee4fb17b4e1/run/' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Idempotency-Key: 9e8d7c6b-5a4f-3e2d-1c0b-a9b8c7d6e5f4'
```
```python Python (requests) theme={null}
import requests
document_packet_id = "069dcc2c-e14c-7606-8000-2ee4fb17b4e1"
url = f"https://api.anyformat.ai/v3/document-packets/{document_packet_id}/run/"
headers = {
"Authorization": "Bearer YOUR_API_KEY",
"Idempotency-Key": "9e8d7c6b-5a4f-3e2d-1c0b-a9b8c7d6e5f4",
}
triggered = requests.post(url, headers=headers).json()
print(triggered["run_id"])
```
```typescript TypeScript theme={null}
interface RunTriggered {
run_id: string;
document_packet_id: string;
workflow_id: string;
status: 'queued' | 'in_progress' | 'processed' | 'error' | 'cancelled';
}
const packetId = '069dcc2c-e14c-7606-8000-2ee4fb17b4e1';
const response = await fetch(
`https://api.anyformat.ai/v3/document-packets/${packetId}/run/`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Idempotency-Key': crypto.randomUUID(),
},
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const triggered: RunTriggered = await response.json();
console.log(triggered.run_id);
```
```json Response (202 Accepted) theme={null}
{
"run_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4fa",
"document_packet_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"workflow_id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"status": "queued"
}
```
# Introduction
Source: https://docs.anyformat.ai/api-reference-v3/introduction
API v3 overview — base URL, resource model, keyset pagination, idempotency, response headers, and the error envelope.
anyformat API **v3** is the current stable major. It exposes the same extraction engine as v2 through a smaller, flatter surface built around three resources: [workflows](/concepts/workflows), [document packets](/concepts/document-packets), and [runs](/concepts/runs-and-results).
Coming from v2? The [migration guide](/api-reference-v3/migrating-from-v2) maps every v2 path to its v3 successor. v2 keeps serving traffic through its announced deprecation window, unchanged.
***
## Base URL
```
https://api.anyformat.ai/v3/
```
All endpoint paths require a trailing slash. Requests without one receive a `307 Temporary Redirect`, which preserves the request method and body.
***
## Authentication
All endpoints require an API key, passed as a Bearer token:
```bash theme={null}
curl -H "Authorization: Bearer YOUR_API_KEY" https://api.anyformat.ai/v3/workflows/
```
See [Authentication](/api-reference/authentication) for how to obtain and manage API keys — keys work identically across v2 and v3.
***
## Resource model
| Resource | What it is | Identifier |
| ------------------- | --------------------------------------------------------------------------------------------------- | -------------------- |
| **Workflow** | The extraction template — a typed graph of parse / classify / splitter / extract / validate nodes. | `workflow_id` |
| **Document packet** | The unit a workflow runs on — one or more files treated as a single document. Created by uploading. | `document_packet_id` |
| **Run** | One execution attempt of a workflow against a packet. One packet can have many runs. | `run_id` |
All identifiers are hyphenated UUIDs (e.g. `069dcc2c-e14c-7606-8000-2ee4fb17b4e1`). Path parameters are always snake\_case: `workflow_id`, `document_packet_id`, `run_id`.
The typical flow:
1. [Create a workflow](/api-reference-v3/workflows/create) (or build it visually in the [dashboard](https://app.anyformat.ai)).
2. [Upload and run](/api-reference-v3/workflows/upload-and-run) a document in one call — it returns a `run_id`.
3. [Get the run](/api-reference-v3/runs/get) until `status` is `processed`; the results envelope is inline on the same response.
***
## Pagination
Every list endpoint is **keyset-paginated** and returns the same page shape:
```json theme={null}
{
"items": [ ... ],
"next_cursor": "eyJjcmVhdGVkX2F0Ijoi..."
}
```
* `?limit=` — page size, default `20`, maximum `100`. Values above 100 are rejected with `400`, not clamped.
* `?cursor=` — the opaque `next_cursor` token from the previous page.
* Iterate by following `next_cursor` until it is `null`.
* Sort order is fixed at newest-first (`-created_at, -id`).
There are **no totals, counts, or page numbers** — a count over a growing collection is stale the moment it is computed, so v3 doesn't pretend otherwise. List endpoints also reject unknown query parameters with `400` instead of silently ignoring them, so a typo like `?pagesize=` fails loudly.
***
## Idempotency
The three POSTs that create packets or trigger runs accept an optional `Idempotency-Key` header (Stripe convention — any unique string you choose, e.g. a UUID):
* [`POST /v3/workflows/{workflow_id}/upload/`](/api-reference-v3/workflows/upload)
* [`POST /v3/workflows/{workflow_id}/upload/run/`](/api-reference-v3/workflows/upload-and-run)
* [`POST /v3/document-packets/{document_packet_id}/run/`](/api-reference-v3/document-packets/run)
Retrying a request with the same key **replays the original response** — no duplicate packet is created, no second extraction is triggered or billed. Use it to make network-timeout retries safe.
```bash theme={null}
curl -X POST 'https://api.anyformat.ai/v3/document-packets/069dcc2c-e14c-7606-8000-2ee4fb17b4e1/run/' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Idempotency-Key: 5f2b6c3e-8a1d-4f7e-9c0b-1a2b3c4d5e6f'
```
***
## Rate limits
Same two-tier model as v2 — submission endpoints have a stricter limit, everything else shares a higher general limit:
| Tier | v3 endpoints | Limit |
| -------------- | -------------------------------------------------------------------------------------------------------------- | ---------------- |
| **Submission** | `POST .../upload/`, `POST .../upload/run/`, `POST .../upload/from-url/`, `POST /v3/document-packets/{id}/run/` | 60 requests/min |
| **General** | All other authenticated endpoints | 600 requests/min |
Exceeding a limit returns `429 Too Many Requests`; wait for the number of seconds in the `Retry-After` header. Every response carries `x-ratelimit-limit`, `x-ratelimit-remaining`, and `x-ratelimit-reset` for the tier that applies.
***
## Response headers
Every `/v3/` response is stamped with the API version:
```http theme={null}
X-API-Version: 3.0.0
```
v3 responses carry **no** `Deprecation` or `Sunset` headers — those appear only on versions that are on the retirement path (as `/v2/` responses do today). See [Versioning & deprecation](/api-reference/introduction#versioning--deprecation) for the full header reference and how to alert on sunset signals.
***
## Errors
Error responses use the same structured envelope as v2:
```json theme={null}
{
"error": "Brief, human-readable error description",
"detail": "Detailed explanation of what went wrong",
"error_code": "MACHINE_READABLE_ERROR_CODE",
"retryable": false,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```
`retryable: true` means the same request may succeed on retry (with backoff); `retryable: false` means you must change the request first. See [Errors](/api-reference/errors) for the complete code reference. Two codes worth knowing up front:
* `402 PAYMENT_REQUIRED` — a run trigger was rejected because the organization is out of extraction credit.
* `400 TOPOLOGY_INVALID` — a workflow create/update body failed graph validation; `detail.violations` lists each broken rule with the offending node ids.
v3 never returns `412 Precondition Failed`. Where v2 used 412 to signal "results not ready yet", v3 returns `200` with the run's `status` field — see [Get run](/api-reference-v3/runs/get).
***
## Endpoints at a glance
### Workflows
| Method | Endpoint | Description |
| ------ | ------------------------------ | --------------------------------------------------------------------------- |
| POST | `/v3/workflows/` | [Create a workflow](/api-reference-v3/workflows/create) |
| GET | `/v3/workflows/` | [List workflows](/api-reference-v3/workflows/list) |
| GET | `/v3/workflows/{workflow_id}/` | [Get a workflow](/api-reference-v3/workflows/get) — full typed graph inline |
| PATCH | `/v3/workflows/{workflow_id}/` | [Update a workflow](/api-reference-v3/workflows/update) |
| DELETE | `/v3/workflows/{workflow_id}/` | [Delete a workflow](/api-reference-v3/workflows/delete) |
### Upload
| Method | Endpoint | Description |
| ------ | ---------------------------------------------- | ------------------------------------------------------------------------------ |
| POST | `/v3/workflows/{workflow_id}/upload/` | [Upload a document packet](/api-reference-v3/workflows/upload) (no processing) |
| POST | `/v3/workflows/{workflow_id}/upload/run/` | [Upload and run](/api-reference-v3/workflows/upload-and-run) in one call |
| POST | `/v3/workflows/{workflow_id}/upload/from-url/` | [Create a packet from URLs](/api-reference-v3/workflows/upload-from-url) |
### Document packets
| Method | Endpoint | Description |
| ------ | ------------------------------------------------ | -------------------------------------------------------------------------------- |
| GET | `/v3/workflows/{workflow_id}/document-packets/` | [List a workflow's packets](/api-reference-v3/document-packets/list) |
| GET | `/v3/document-packets/{document_packet_id}/` | [Get a packet](/api-reference-v3/document-packets/get) — files + `latest_run_id` |
| POST | `/v3/document-packets/{document_packet_id}/run/` | [Run a packet](/api-reference-v3/document-packets/run) — returns a new `run_id` |
| DELETE | `/v3/document-packets/{document_packet_id}/` | [Delete a packet](/api-reference-v3/document-packets/delete) |
### Runs
| Method | Endpoint | Description |
| ------ | ----------------------------------- | ----------------------------------------------------------------- |
| GET | `/v3/workflows/{workflow_id}/runs/` | [List a workflow's runs](/api-reference-v3/runs/list) |
| GET | `/v3/runs/{run_id}/` | [Get a run](/api-reference-v3/runs/get) — status + results inline |
***
## OpenAPI schema
The full OpenAPI specification (v2 + v3) is available at:
* **JSON**: [https://api.anyformat.ai/schema/?format=json](https://api.anyformat.ai/schema/?format=json)
* **Swagger UI**: [https://api.anyformat.ai/docs/](https://api.anyformat.ai/docs/)
# Check API Key
Source: https://docs.anyformat.ai/api-reference-v3/key-check
GET /key-check/
Verify an API key without triggering a billable operation
Confirms the caller's API key is active and names its owning organization. Unversioned — the same endpoint works for v2 and v3 keys.
* **200** — the key is valid; response body carries `organization_id`, plus `organization_name` and `scopes` (list of granted scope names, `null` when not reported) when available.
* **401** — standard [error envelope](/api-reference/errors) with `error_code` `MISSING_API_KEY` (no key sent) or `INVALID_API_KEY` (key not recognised).
Nothing is billed and no run is created, so it's cheap to call as a probe before the first real request. It hits the same `/me/organization/` round-trip every authenticated request runs, so a successful check also warms the org cache.
```bash curl theme={null}
curl -X GET 'https://api.anyformat.ai/key-check/' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```python Python (requests) theme={null}
import requests
response = requests.get(
"https://api.anyformat.ai/key-check/",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
response.raise_for_status()
print(response.json()["organization_id"])
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.anyformat.ai/key-check/', {
headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
});
if (!response.ok) throw new Error(`Invalid API key: ${response.status}`);
const { organization_id, organization_name } = await response.json();
```
```json Response (200 OK) theme={null}
{
"valid": true,
"organization_id": "7f4a1c2e-0000-4000-8000-000000000001",
"organization_name": "Acme Corp",
"scopes": ["read", "write"]
}
```
```json Response (401 Unauthorized) theme={null}
{
"error": "Invalid API key",
"detail": "The provided API key is not recognised.",
"error_code": "INVALID_API_KEY",
"retryable": false,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```
# Migrating from v2
Source: https://docs.anyformat.ai/api-reference-v3/migrating-from-v2
Path-by-path mapping from the v2 surface to v3, plus the semantic changes: keyset pagination, inline run status, persistent_id round-tripping, and idempotency.
v3 replaces v2's file-centric surface with three first-class resources — [workflows](/concepts/workflows), [document packets](/concepts/document-packets), and [runs](/concepts/runs-and-results). The extraction engine, the results envelope, authentication, and the error envelope are unchanged; what moved are the paths, the identifiers, and the read model.
**v2 keeps working through its deprecation window.** Every `/v2/` response now carries `Deprecation` and `Sunset` headers ([RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594)) announcing the retirement date. Nothing breaks today — but new integrations should start on v3, and existing ones should plan the move. See [Versioning & deprecation](/api-reference/introduction#versioning--deprecation) for how to alert on these headers automatically. The [v2 deprecation timeline](/api-reference/v2-migration) has the exact window dates and the per-route successor `Link` headers.
***
## The one-sentence version
Where v2 said *file* or *collection*, v3 says **document packet**; where v2 made you poll a nested `/results/` sub-path, v3 gives every execution its own **run** you read flat — status and results in one `200`.
***
## Path-by-path mapping
### Workflows
| v2 | v3 | What changed |
| ------------------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `POST /v2/workflows/` | [`POST /v3/workflows/`](/api-reference-v3/workflows/create) | Same typed-graph body. |
| `GET /v2/workflows/` | [`GET /v3/workflows/`](/api-reference-v3/workflows/list) | [Keyset pagination](#pagination-offset-to-keyset). |
| `GET /v2/workflows/{id}/` | [`GET /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/get) | Now returns the **full typed graph** inline, with a `persistent_id` on every field. |
| `GET /v2/workflows/{id}/definition/` | folded into [`GET /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/get) | There is no separate `/definition/` endpoint in v3 — the workflow read *is* the definition. |
| `PUT /v2/workflows/{id}/` | [`PATCH /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/update) | [PUT → PATCH with `persistent_id` echo](#put-to-patch-field-identity-round-trips). |
| `DELETE /v2/workflows/{id}/` | [`DELETE /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/delete) | Unchanged semantics. |
### Uploading documents
| v2 | v3 | What changed |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v2/workflows/{id}/files/` | [`POST /v3/workflows/{workflow_id}/upload/`](/api-reference-v3/workflows/upload) | Returns `document_packet_id` instead of `file_collection_id`. Accepts 1–10 files as **one packet**. Supports [`Idempotency-Key`](#idempotency-new). |
| `POST /v2/workflows/{id}/run/` | [`POST /v3/workflows/{workflow_id}/upload/run/`](/api-reference-v3/workflows/upload-and-run) | Returns a `run_id` — the handle you poll — instead of a collection id. |
| `POST /v2/workflows/{id}/files/from-url/` | [`POST /v3/workflows/{workflow_id}/upload/from-url/`](/api-reference-v3/workflows/upload-from-url) | Takes 1–10 HTTPS URLs; the import is **all-or-nothing** and the response is final (no pending fetch state to reason about). |
| `POST /v2/workflows/{id}/files/{file_id}/run/` | [`POST /v3/document-packets/{document_packet_id}/run/`](/api-reference-v3/document-packets/run) | Flat path — the packet knows its workflow, so no `workflow_id` needed. Every call creates a **new run**. |
v2's `text` form field (plain text instead of a file) has no dedicated v3 counterpart — upload the text as a small `.txt` file in the multipart `files` field instead.
### Reading packets and results
| v2 | v3 | What changed |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `GET /v2/workflows/{id}/files/` | [`GET /v3/workflows/{workflow_id}/document-packets/`](/api-reference-v3/document-packets/list) | Keyset-paginated; the single-packet read carries `latest_run_id`. |
| `GET /v2/workflows/{id}/files/{collection_id}/results/` | [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get) | [No more 412-polling](#results-polling-412-to-inline-status) — status and results come back inline on a flat run read. |
| `GET /v2/workflows/{id}/runs/` | [`GET /v3/workflows/{workflow_id}/runs/`](/api-reference-v3/runs/list) | Keyset-paginated; items reference `document_packet_id` and are followed up via `GET /v3/runs/{run_id}/`. |
| `DELETE /v2/files/{collection_id}/` | [`DELETE /v3/document-packets/{document_packet_id}/`](/api-reference-v3/document-packets/delete) | Same semantics: deletes the packet, its files, and its results. |
***
## Identifier renames
v2's `file_id` / `collection_id` / `file_collection_id` all collapse into one identifier:
| v2 name | v3 name |
| ------------------------------------------------------------------------- | -------------------- |
| `file_collection_id`, `collection_id`, `file_id` (as the runnable handle) | `document_packet_id` |
A v2 collection **is** a v3 document packet — same underlying object, one name. Per-file ids still exist inside a packet (`files[].id` on packet responses) for the rare case where you need to address one file of a multi-file packet. Path parameters are always snake\_case: `workflow_id`, `document_packet_id`, `run_id`.
***
## Semantic changes
### Results polling: 412 to inline status
v2's results read returned `412 Precondition Failed` while the extraction was in flight, so clients had to treat an error status as "not yet". In v3, [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get) **always returns `200`** while the run exists:
* `status` tells you where the run is: `queued`, `in_progress`, `processed`, `error`, or `cancelled`.
* `results` is `null` until `status` reaches `processed`, then carries the results envelope inline.
* A run that ended in `error` or `cancelled` stays readable with `results: null`.
The results envelope itself — `parse`, `classifications`, `splits`, `extractions` — is **byte-compatible** with v2's; see [Response formats](/api-reference/response-formats). Only the read path moved.
```python theme={null}
# v2: poll /results/, treat 412 as "not ready"
r = requests.get(f"{BASE}/v2/workflows/{wf}/files/{col}/results/", headers=h)
if r.status_code == 412:
...retry...
# v3: poll the run, branch on status
r = requests.get(f"{BASE}/v3/runs/{run_id}/", headers=h)
run = r.json()
if run["status"] == "processed":
results = run["results"]
```
### PUT to PATCH: field identity round-trips
v2 edited workflows with `PUT /v2/workflows/{id}/`, and the round-trippable graph lived on a separate `GET .../definition/`. v3 folds both into the workflow resource:
1. `GET /v3/workflows/{workflow_id}/` returns the workflow with its graph inline — every field carries its server-assigned `persistent_id`.
2. Mutate the graph, keep only `{name, description, nodes, edges}` (the read-only `id`, `created_at`, `updated_at` are rejected with `422`), and send it back via `PATCH /v3/workflows/{workflow_id}/`.
3. **Echo each field's `persistent_id` unchanged — including across renames** — and the field keeps its identity: analytics history, ground truth, and quality metrics stay attached.
4. The PATCH response echoes the stored graph back in the exact GET shape (new fields included, with their freshly assigned `persistent_id`s), so you can edit and PATCH again.
Omit `persistent_id` only for genuinely new fields — the server assigns one. A field without `persistent_id` is always treated as new, so omitting it on an existing field replaces that field with a fresh one and detaches its history. An echoed `persistent_id` that doesn't match any field in the current version is rejected with `400`. There is no `PUT` in v3.
### Pagination: offset to keyset
v2 lists used `?page=` / `?page_size=` with a `count` total. v3 lists return `{items, next_cursor}`:
* Pass `?cursor=` to fetch the next page; stop when `next_cursor` is `null`.
* `?limit=` caps at 100 — values above are rejected with `400`, not clamped.
* There are **no totals or page numbers**. If you rendered `count` in a UI, switch to cursor-driven "load more" semantics.
See [Pagination](/api-reference-v3/introduction#pagination).
### Idempotency (new)
The packet-creating and run-triggering POSTs accept an `Idempotency-Key` header. Retrying with the same key replays the original response instead of creating a duplicate packet or billing a second extraction. v2 had no equivalent — if you built retry-dedup logic client-side, you can delete it. See [Idempotency](/api-reference-v3/introduction#idempotency).
### Version header
Every `/v3/` response is stamped `X-API-Version: 3.0.0` (v2 responses say `2.0.0` and additionally carry `Deprecation` / `Sunset`). If you tag metrics by API version, the header is the reliable source.
***
## What did *not* change
* **Authentication** — same API keys, same `Authorization: Bearer` header.
* **Error envelope** — same `{error, detail, error_code, retryable, request_id}` shape ([Errors](/api-reference/errors)).
* **Results envelope** — same `parse` / `classifications` / `splits` / `extractions` sections ([Response formats](/api-reference/response-formats)).
* **Rate limiting** — same two-tier limits and `x-ratelimit-*` headers.
* **Webhooks** — existing webhook subscriptions keep firing; management stays on the current endpoints ([Webhooks](/api-reference/webhooks/overview)).
* **The typed-graph workflow body** — `POST /v3/workflows/` accepts the same `{name, description, nodes, edges}` shape as `POST /v2/workflows/`.
***
## Migration checklist
1. Swap base paths `/v2/` → `/v3/` per the tables above; rename `file_id` / `collection_id` variables to `document_packet_id`.
2. Replace `/results/` polling (412-tolerant) with `GET /v3/runs/{run_id}/` polling on `status`.
3. Replace `PUT` workflow edits with GET → mutate → `PATCH`, preserving `persistent_id`s.
4. Replace `page`/`page_size` pagination with `cursor`/`limit`; drop any use of `count`.
5. Add an `Idempotency-Key` to upload and run-trigger POSTs if you retry on timeouts.
6. Wire an alert on the `Sunset` header so your v2 traffic can't outlive the window unnoticed — see [the header reference](/api-reference/introduction#versioning--deprecation).
# Get Run
Source: https://docs.anyformat.ai/api-reference-v3/runs/get
GET /v3/runs/{run_id}/
Fetch a single run flat: lifecycle status plus the results envelope inline
*Rate limit tier: **general** (600 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Fetches a [run](/concepts/runs-and-results) by its `run_id`. The path is flat — no workflow id, no `/results/` sub-path — and the response carries the run's status **and** its results envelope in one shot, so a completed run is a single round-trip.
## Status model
The endpoint **always returns `200` while the run exists** — there are no precondition errors while the extraction is in flight.
| `status` | Meaning | `results` |
| ------------- | ------------------------------- | -------------------- |
| `queued` | Waiting for a processing slot | `null` |
| `in_progress` | Extraction actively running | `null` |
| `processed` | Success — terminal | the results envelope |
| `error` | Extraction failed — terminal | `null` |
| `cancelled` | Extraction cancelled — terminal | `null` |
Poll until `status` is terminal; a run that ended in `error` or `cancelled` stays readable with `results: null`. For production integrations, prefer [webhooks](/api-reference/webhooks/overview) over polling.
The `results` envelope matches the shape documented at [Response formats](/api-reference/response-formats) — the same `parse`, `classifications`, `splits`, and `extractions` sections v2 returned from its `/results/` route. It's the read path that changed, not the envelope.
Unknown ids — including runs belonging to another organization — return `404`.
```bash curl theme={null}
curl -X GET 'https://api.anyformat.ai/v3/runs/069dcc2c-e14c-7606-8000-2ee4fb17b4f9/' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```python Python (requests) theme={null}
import requests
import time
run_id = "069dcc2c-e14c-7606-8000-2ee4fb17b4f9"
url = f"https://api.anyformat.ai/v3/runs/{run_id}/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
for _ in range(100):
run = requests.get(url, headers=headers).json()
if run["status"] == "processed":
print(run["results"])
break
if run["status"] in ("error", "cancelled"):
print(f"Terminal: {run['status']}")
break
time.sleep(3)
```
```typescript TypeScript theme={null}
interface RunDetail {
id: string;
workflow_id: string;
document_packet_id: string;
status: 'queued' | 'in_progress' | 'processed' | 'error' | 'cancelled';
created_at: string | null;
updated_at: string | null;
results: Record | null;
}
const runId = '069dcc2c-e14c-7606-8000-2ee4fb17b4f9';
const response = await fetch(`https://api.anyformat.ai/v3/runs/${runId}/`, {
headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const run: RunDetail = await response.json();
if (run.status === 'processed') {
console.log(run.results);
}
```
```json Response (200 OK — processed) theme={null}
{
"id": "069dcc2c-e14c-7606-8000-2ee4fb17b4f9",
"workflow_id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"document_packet_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"status": "processed",
"created_at": "2026-07-01T12:00:00.000Z",
"updated_at": "2026-07-01T12:02:30.000Z",
"results": {
"document_packet_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"verification_url": "https://app.anyformat.ai/review/069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"parse": {
"markdown": "..."
},
"classifications": [],
"splits": [],
"extractions": [
{
"split_name": null,
"partition": null,
"fields": {
"invoice_number": {
"value": "INV-001",
"value_override": null,
"verification_status": "not_verified",
"confidence": 95.0,
"evidence": [{ "text": "Invoice #INV-001", "page_number": 1 }]
}
}
}
]
}
}
```
```json Response (200 OK — still processing) theme={null}
{
"id": "069dcc2c-e14c-7606-8000-2ee4fb17b4f9",
"workflow_id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"document_packet_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"status": "in_progress",
"created_at": "2026-07-01T12:00:00.000Z",
"updated_at": "2026-07-01T12:00:15.000Z",
"results": null
}
```
```json Response (200 OK — terminal, no results) theme={null}
{
"id": "069dcc2c-e14c-7606-8000-2ee4fb17b4f9",
"workflow_id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"document_packet_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"status": "error",
"created_at": "2026-07-01T12:00:00.000Z",
"updated_at": "2026-07-01T12:02:30.000Z",
"results": null
}
```
# List Workflow Runs
Source: https://docs.anyformat.ai/api-reference-v3/runs/list
GET /v3/workflows/{workflow_id}/runs/
List a workflow's runs, newest first
*Rate limit tier: **general** (600 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Returns one keyset page of a workflow's [runs](/concepts/runs-and-results), newest first. Follow `next_cursor` until it is `null` (see [Pagination](/api-reference-v3/introduction#pagination)).
Items are slim — id, `document_packet_id`, `status`, timestamps. Fetch [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get) for the results envelope.
```bash curl theme={null}
curl -X GET 'https://api.anyformat.ai/v3/workflows/0686bb97-8c30-70f0-8000-97669e000eb8/runs/?limit=20' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```python Python (requests) theme={null}
import requests
workflow_id = "0686bb97-8c30-70f0-8000-97669e000eb8"
url = f"https://api.anyformat.ai/v3/workflows/{workflow_id}/runs/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
page = requests.get(url, headers=headers, params={"limit": 20}).json()
for run in page["items"]:
print(run["id"], run["status"])
```
```json Response (200 OK) theme={null}
{
"items": [
{
"id": "069dcc2c-e14c-7606-8000-2ee4fb17b4fa",
"document_packet_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"status": "processed",
"created_at": "2026-07-02T09:00:00.000Z",
"updated_at": "2026-07-02T09:02:30.000Z"
},
{
"id": "069dcc2c-e14c-7606-8000-2ee4fb17b4f9",
"document_packet_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"status": "error",
"created_at": "2026-07-01T12:00:00.000Z",
"updated_at": "2026-07-01T12:01:10.000Z"
}
],
"next_cursor": null
}
```
# SDKs
Source: https://docs.anyformat.ai/api-reference-v3/sdks
Official client libraries for API v3 — new SDK majors are in progress.
**New SDK majors targeting v3 are in development.** The pages below will link the v3 releases of the TypeScript and Python SDKs as soon as they publish. Until then, the current SDKs — which speak v2 under the hood — keep working unchanged through v2's deprecation window.
The upcoming SDK majors keep the same fluent builder over the [typed-graph workflow definition](/concepts/workflows) and swap the transport to the v3 surface: flat run reads instead of `/results/` polling, `document_packet_id` handles, keyset pagination, and automatic `Idempotency-Key` on retries.
## Today
* **TypeScript** — [`@anyformat/sdk`](https://www.npmjs.com/package/@anyformat/sdk) (v2 transport). See the [TypeScript SDK guide](/api-reference/sdks/typescript).
* **Python** — [`anyformat`](https://pypi.org/project/anyformat/) (v2 transport). See the [Python SDK guide](/api-reference/sdks/python).
* **Raw HTTP** — every v3 endpoint page in this reference carries copy-paste `curl` / Python / TypeScript examples that work right now, no SDK required. Start at [Upload and run](/api-reference-v3/workflows/upload-and-run).
## Coding assistant
If you use [Claude Code](https://claude.com/claude-code) (or any compatible AI coding agent), install the **anyformat Claude Code skill** so your agent knows the right endpoints, payloads, and gotchas out of the box:
```bash theme={null}
npx @anyformat/skill # installs for all projects (~/.claude/skills/anyformat)
npx @anyformat/skill --project # installs for the current project only (./.claude/skills/anyformat)
```
See the [Coding assistant](/guides/coding-assistant) guide for installation, configuration, and example prompts.
# Create Workflow
Source: https://docs.anyformat.ai/api-reference-v3/workflows/create
POST /v3/workflows/
Create a workflow from a typed graph of parse / classify / splitter / extract / validate nodes in a single atomic transaction
*Rate limit tier: **general** (600 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
We recommend creating workflows in the [anyformat platform](https://app.anyformat.ai), where you can visually configure fields, test with sample documents, and iterate faster. Once your workflow is ready, copy its ID and use it with the API to run documents programmatically.
Creates a workflow from a strongly-typed graph, atomically. The body is the same `{name, description, nodes, edges}` shape `POST /v2/workflows/` accepts — node types, field types, and validation rules are documented in depth on the [v2 create page](/api-reference/workflows/create) and in [Field types](/concepts/field-types); the graph model itself did not change in v3.
A body that violates graph topology (e.g. no parse node, an orphaned extract node) is rejected with `400 TOPOLOGY_INVALID`; `detail.violations` lists each broken rule with the offending node ids.
Fetch the stored graph back — with server-assigned `persistent_id`s on every field — via [`GET /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/get).
```bash curl theme={null}
curl -X POST 'https://api.anyformat.ai/v3/workflows/' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "Invoice Processing",
"description": "Extract data from invoice documents",
"nodes": [
{ "id": "parse_1", "type": "parse" },
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{ "name": "invoice_number", "description": "The unique invoice identifier", "data_type": "string" },
{ "name": "total_amount", "description": "Total invoice amount including tax", "data_type": "float" }
]
}
}
],
"edges": [ { "source": "parse_1", "target": "extract_1" } ]
}'
```
```python Python (requests) theme={null}
import requests
url = "https://api.anyformat.ai/v3/workflows/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
body = {
"name": "Invoice Processing",
"description": "Extract data from invoice documents",
"nodes": [
{"id": "parse_1", "type": "parse"},
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{"name": "invoice_number", "description": "The unique invoice identifier", "data_type": "string"},
{"name": "total_amount", "description": "Total invoice amount including tax", "data_type": "float"},
]
},
},
],
"edges": [{"source": "parse_1", "target": "extract_1"}],
}
workflow = requests.post(url, headers=headers, json=body).json()
print(workflow["id"])
```
```json Response (201 Created) theme={null}
{
"id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"name": "Invoice Processing",
"description": "Extract data from invoice documents",
"created_at": "2026-07-01T12:00:00.000Z",
"updated_at": "2026-07-01T12:00:00.000Z"
}
```
```json Response (400 — invalid topology) theme={null}
{
"error": "Validation failed",
"detail": {
"message": "Workflow graph has 1 violation",
"violations": [
{
"rule": "single_parse_entrypoint",
"message": "Exactly one parse node is required",
"node_ids": []
}
]
},
"error_code": "TOPOLOGY_INVALID",
"retryable": false,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```
# Delete Workflow
Source: https://docs.anyformat.ai/api-reference-v3/workflows/delete
DELETE /v3/workflows/{workflow_id}/
Delete a workflow and all associated document packets and runs
*Rate limit tier: **general** (600 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Deletes a workflow along with **all of its document packets and runs**. This action is irreversible.
Returns `204 No Content` on success. Unknown ids — including workflows belonging to another organization — return `404`.
```bash curl theme={null}
curl -X DELETE 'https://api.anyformat.ai/v3/workflows/0686bb97-8c30-70f0-8000-97669e000eb8/' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```python Python (requests) theme={null}
import requests
workflow_id = "0686bb97-8c30-70f0-8000-97669e000eb8"
url = f"https://api.anyformat.ai/v3/workflows/{workflow_id}/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
response = requests.delete(url, headers=headers)
assert response.status_code == 204
```
# Get Workflow
Source: https://docs.anyformat.ai/api-reference-v3/workflows/get
GET /v3/workflows/{workflow_id}/
Retrieve a workflow with its complete typed graph inline
*Rate limit tier: **general** (600 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Retrieves a workflow with its **complete typed graph inline**. The `{name, description, nodes, edges}` subset is the exact shape [`POST /v3/workflows/`](/api-reference-v3/workflows/create) accepts — strip the read-only `id`, `created_at`, and `updated_at` (unknown fields are rejected with `422`) and feed the remainder to [`PATCH /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/update); its Python example shows the one-liner.
Every field in an extract node carries its server-assigned `persistent_id` — the stable identity that survives renames. Echo it unchanged when you PATCH to keep analytics, ground truth, and quality metrics attached to the field (see [the migration guide](/api-reference-v3/migrating-from-v2#put-to-patch-field-identity-round-trips)).
There is no separate `/definition/` endpoint in v3 — the workflow read *is* the round-trippable definition.
```bash curl theme={null}
curl -X GET 'https://api.anyformat.ai/v3/workflows/0686bb97-8c30-70f0-8000-97669e000eb8/' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```python Python (requests) theme={null}
import requests
workflow_id = "0686bb97-8c30-70f0-8000-97669e000eb8"
url = f"https://api.anyformat.ai/v3/workflows/{workflow_id}/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
workflow = requests.get(url, headers=headers).json()
for node in workflow["nodes"]:
print(node["id"], node["type"])
```
```json Response (200 OK) theme={null}
{
"id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"name": "Invoice Processing",
"description": "Extract data from invoice documents",
"created_at": "2026-07-01T12:00:00.000Z",
"updated_at": "2026-07-01T12:00:00.000Z",
"nodes": [
{
"id": "parse_1",
"type": "parse",
"mode": "standard",
"prompt_hint": null,
"figure_enhancement": false,
"cache": true,
"effort": "mid",
"ocr_effort": "high",
"skip_routing_review": true,
"figures": false,
"text_formatting": false,
"routing_effort": null
},
{
"id": "extract_1",
"type": "extract",
"mode": "standard",
"extraction_schema": {
"fields": [
{
"name": "invoice_number",
"description": "The unique invoice identifier",
"data_type": "string",
"persistent_id": "8f14e45f-ceea-467f-a34e-9b6e2f7d3c1a"
},
{
"name": "total_amount",
"description": "Total invoice amount including tax",
"data_type": "float",
"persistent_id": "45c48cce-2e2d-4fbd-aa47-1a8a5d8f6b2b"
}
]
},
"lookup_files": [],
"lookup_suggestion": null,
"lookup_reasoning_effort": null,
"lookup_file_uploads": [],
"use_images": false
}
],
"edges": [
{ "source": "parse_1", "target": "extract_1" }
]
}
```
# List Workflows
Source: https://docs.anyformat.ai/api-reference-v3/workflows/list
GET /v3/workflows/
List your organization's workflows, newest first
*Rate limit tier: **general** (600 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Returns one keyset page of your organization's workflows, newest first. Follow `next_cursor` until it is `null` — there are no totals or page numbers (see [Pagination](/api-reference-v3/introduction#pagination)).
Items are slim summaries; fetch [`GET /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/get) for the full typed graph.
```bash curl theme={null}
curl -X GET 'https://api.anyformat.ai/v3/workflows/?limit=20' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
```python Python (requests) theme={null}
import requests
url = "https://api.anyformat.ai/v3/workflows/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
cursor = None
while True:
params = {"limit": 20, **({"cursor": cursor} if cursor else {})}
page = requests.get(url, headers=headers, params=params).json()
for workflow in page["items"]:
print(workflow["id"], workflow["name"])
cursor = page["next_cursor"]
if cursor is None:
break
```
```typescript TypeScript theme={null}
interface WorkflowSummary {
id: string;
name: string;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
interface WorkflowListPage {
items: WorkflowSummary[];
next_cursor: string | null;
}
let cursor: string | null = null;
do {
const params = new URLSearchParams({ limit: '20' });
if (cursor) params.set('cursor', cursor);
const response = await fetch(`https://api.anyformat.ai/v3/workflows/?${params}`, {
headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
});
const page: WorkflowListPage = await response.json();
page.items.forEach(w => console.log(w.id, w.name));
cursor = page.next_cursor;
} while (cursor !== null);
```
```json Response (200 OK) theme={null}
{
"items": [
{
"id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"name": "Invoice Processing",
"description": "Extract data from invoice documents",
"created_at": "2026-07-01T12:00:00.000Z",
"updated_at": "2026-07-01T12:00:00.000Z"
}
],
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNy0wMVQxMjowMDowMFoifQ"
}
```
# Update Workflow
Source: https://docs.anyformat.ai/api-reference-v3/workflows/update
PATCH /v3/workflows/{workflow_id}/
Replace a workflow's typed graph atomically — echo persistent_id to keep field identity across renames
*Rate limit tier: **general** (600 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Replaces a workflow's typed graph, atomically. The body is the `{name, description, nodes, edges}` sub-shape of [`GET /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/get) — the intended loop is **GET → mutate → PATCH**.
Each update mints a new workflow version; previous versions remain available for historical runs. There is no `PUT` in v3.
## Field identity: `persistent_id`
Every field in the GET response carries a server-assigned `persistent_id`. When you PATCH:
* **Echo `persistent_id` unchanged** — including when you rename the field — and the field keeps its identity: analytics history, ground truth, and quality metrics stay attached.
* **Omit it only for brand-new fields**; the server assigns one. A field without `persistent_id` is always treated as new — name coincidence with a prior field does **not** confer identity, so omitting it on an existing field replaces that field with a fresh one and detaches its history.
* An echoed `persistent_id` that doesn't match any field in the current version is rejected with `400`.
* Sending back an **unchanged graph** cuts no new version.
The response is the updated workflow in the exact GET shape — including the `persistent_id`s just assigned to new fields — so the result can be edited and PATCHed again. The echoed graph is this request's own write result, never the state left by a concurrent update.
An invalid graph is rejected with `400 TOPOLOGY_INVALID` (same contract as [Create workflow](/api-reference-v3/workflows/create)); the stored workflow is untouched.
```bash curl theme={null}
curl -X PATCH 'https://api.anyformat.ai/v3/workflows/0686bb97-8c30-70f0-8000-97669e000eb8/' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "Invoice Processing",
"description": "Extract data from invoice documents",
"nodes": [
{ "id": "parse_1", "type": "parse" },
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{
"name": "invoice_reference",
"description": "The unique invoice identifier",
"data_type": "string",
"persistent_id": "8f14e45f-ceea-467f-a34e-9b6e2f7d3c1a"
}
]
}
}
],
"edges": [ { "source": "parse_1", "target": "extract_1" } ]
}'
```
```python Python (requests) theme={null}
import requests
workflow_id = "0686bb97-8c30-70f0-8000-97669e000eb8"
url = f"https://api.anyformat.ai/v3/workflows/{workflow_id}/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
# GET → mutate → PATCH: rename a field, keep its persistent_id.
workflow = requests.get(url, headers=headers).json()
field = workflow["nodes"][1]["extraction_schema"]["fields"][0]
field["name"] = "invoice_reference" # persistent_id stays — identity survives the rename
body = {k: workflow[k] for k in ("name", "description", "nodes", "edges")}
updated = requests.patch(url, headers=headers, json=body).json()
print(updated["updated_at"])
```
```json Response (200 OK) theme={null}
{
"id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"name": "Invoice Processing",
"description": "Extract data from invoice documents",
"created_at": "2026-07-01T12:00:00.000Z",
"updated_at": "2026-07-05T09:30:00.000Z",
"nodes": [
{
"id": "parse_1",
"type": "parse",
"mode": "standard",
"prompt_hint": null,
"figure_enhancement": false,
"cache": true,
"effort": "mid",
"ocr_effort": "high",
"skip_routing_review": true,
"figures": false,
"text_formatting": false,
"routing_effort": null
},
{
"id": "extract_1",
"type": "extract",
"mode": "standard",
"extraction_schema": {
"fields": [
{
"name": "invoice_reference",
"description": "The unique invoice identifier",
"data_type": "string",
"persistent_id": "8f14e45f-ceea-467f-a34e-9b6e2f7d3c1a"
}
]
},
"lookup_files": [],
"lookup_suggestion": null,
"lookup_reasoning_effort": null,
"lookup_file_uploads": [],
"use_images": false
}
],
"edges": [ { "source": "parse_1", "target": "extract_1" } ]
}
```
# Upload Document Packet
Source: https://docs.anyformat.ai/api-reference-v3/workflows/upload
POST /v3/workflows/{workflow_id}/upload/
Upload one or more files as a single document packet, without running it
*Rate limit tier: **submission** (60 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Uploads 1–10 files as a single [document packet](/concepts/document-packets) — the unit that runs later address. All files are grouped into **one** packet; creation is all-or-nothing, so any rejected file (unsupported type, disguised bytes) fails the whole request and nothing is stored.
This endpoint uploads without processing. Trigger extraction afterwards via [`POST /v3/document-packets/{document_packet_id}/run/`](/api-reference-v3/document-packets/run) — or do both in one call with [Upload and Run](/api-reference-v3/workflows/upload-and-run).
Send the files as multipart form data under the `files` field (repeat the field for a multi-file packet). Files above the per-file size cap are rejected at slot mint (before any bytes reach S3); see [Files](/concepts/files) for the cap and supported formats. Bundle multiple files only when they belong together as one document (a contract and its annexes) — unrelated documents should be separate packets.
**Retries are safe with `Idempotency-Key`.** Pass any unique string; retrying the request with the same key replays the original upload, so no duplicate packet is created. See [Idempotency](/api-reference-v3/introduction#idempotency).
```bash curl theme={null}
curl -X POST 'https://api.anyformat.ai/v3/workflows/0686bb97-8c30-70f0-8000-97669e000eb8/upload/' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Idempotency-Key: 5f2b6c3e-8a1d-4f7e-9c0b-1a2b3c4d5e6f' \
-F 'files=@/path/to/contract.pdf' \
-F 'files=@/path/to/annex-a.pdf'
```
```python Python (requests) theme={null}
import requests
workflow_id = "0686bb97-8c30-70f0-8000-97669e000eb8"
url = f"https://api.anyformat.ai/v3/workflows/{workflow_id}/upload/"
headers = {
"Authorization": "Bearer YOUR_API_KEY",
"Idempotency-Key": "5f2b6c3e-8a1d-4f7e-9c0b-1a2b3c4d5e6f",
}
with open("contract.pdf", "rb") as f1, open("annex-a.pdf", "rb") as f2:
files = [("files", f1), ("files", f2)]
packet = requests.post(url, headers=headers, files=files).json()
print(packet["document_packet_id"])
```
```typescript TypeScript theme={null}
interface DocumentPacketCreated {
document_packet_id: string;
workflow_id: string;
files: { id: string; name: string }[];
}
const workflowId = '0686bb97-8c30-70f0-8000-97669e000eb8';
const formData = new FormData();
formData.append('files', fileInput.files[0]);
const response = await fetch(
`https://api.anyformat.ai/v3/workflows/${workflowId}/upload/`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Idempotency-Key': crypto.randomUUID(),
},
body: formData,
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const packet: DocumentPacketCreated = await response.json();
console.log(packet.document_packet_id);
```
```json Response (201 Created) theme={null}
{
"document_packet_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"workflow_id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"files": [
{ "id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e2", "name": "contract.pdf" },
{ "id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e3", "name": "annex-a.pdf" }
]
}
```
# Upload and Run
Source: https://docs.anyformat.ai/api-reference-v3/workflows/upload-and-run
POST /v3/workflows/{workflow_id}/upload/run/
Upload a document packet and start a run in a single call
*Rate limit tier: **submission** (60 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Uploads 1–10 files as a single [document packet](/concepts/document-packets) and immediately enqueues a [run](/concepts/runs-and-results) on the workflow's latest version — the one-shot composition of [Upload](/api-reference-v3/workflows/upload) and [Run packet](/api-reference-v3/document-packets/run). This is the shortest path from bytes to structured data.
The `202` response returns the new `run_id`. Poll [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get) until `status` is terminal — the results arrive inline on that same read. For production integrations, prefer [webhooks](/api-reference/webhooks/overview) over polling.
Send the files as multipart form data under the `files` field (repeat it for a multi-file packet); files above the per-file size cap are rejected at slot mint (before any bytes reach S3). Packet creation is all-or-nothing.
**Retries are safe with `Idempotency-Key`.** Retrying with the same key replays the original packet **and** run — no duplicate upload, no second extraction billed. See [Idempotency](/api-reference-v3/introduction#idempotency).
An organization without extraction credit receives `402 PAYMENT_REQUIRED`.
```bash curl theme={null}
curl -X POST 'https://api.anyformat.ai/v3/workflows/0686bb97-8c30-70f0-8000-97669e000eb8/upload/run/' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Idempotency-Key: 7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f' \
-F 'files=@/path/to/invoice.pdf'
```
```python Python (requests) theme={null}
import requests
import time
workflow_id = "0686bb97-8c30-70f0-8000-97669e000eb8"
base = "https://api.anyformat.ai"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
with open("invoice.pdf", "rb") as f:
triggered = requests.post(
f"{base}/v3/workflows/{workflow_id}/upload/run/",
headers={**headers, "Idempotency-Key": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"},
files=[("files", f)],
).json()
run_url = f"{base}/v3/runs/{triggered['run_id']}/"
for _ in range(100):
run = requests.get(run_url, headers=headers).json()
if run["status"] == "processed":
print(run["results"])
break
if run["status"] in ("error", "cancelled"):
print(f"Terminal: {run['status']}")
break
time.sleep(3)
```
```typescript TypeScript theme={null}
interface RunTriggered {
run_id: string;
document_packet_id: string;
workflow_id: string;
status: 'queued' | 'in_progress' | 'processed' | 'error' | 'cancelled';
}
const workflowId = '0686bb97-8c30-70f0-8000-97669e000eb8';
const formData = new FormData();
formData.append('files', fileInput.files[0]);
const response = await fetch(
`https://api.anyformat.ai/v3/workflows/${workflowId}/upload/run/`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Idempotency-Key': crypto.randomUUID(),
},
body: formData,
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const triggered: RunTriggered = await response.json();
console.log(triggered.run_id);
```
```json Response (202 Accepted) theme={null}
{
"run_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4f9",
"document_packet_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"workflow_id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"status": "queued"
}
```
# Upload from URL
Source: https://docs.anyformat.ai/api-reference-v3/workflows/upload-from-url
POST /v3/workflows/{workflow_id}/upload/from-url/
Create a document packet by importing 1–10 HTTPS URLs server-side, all-or-nothing
*Rate limit tier: **submission** (60 req/min) — see [Rate limits](/api-reference-v3/introduction#rate-limits).*
Creates a [document packet](/concepts/document-packets) by having anyformat fetch the bytes server-side — no need to stream documents through your own backend when they already live in object storage you can presign (S3, GCS, R2, …) or at a public HTTPS URL.
Provide 1–10 **HTTPS** URLs. All of them import into a **single packet, atomically**: the packet is registered only after every fetch succeeded, so any failure imports nothing — no partial packet, no orphan files. Filenames are derived from each URL's path.
Each fetch is bounded by a 10-second timeout and a 20 MB per-file cap. A failed, non-2xx, or timed-out fetch surfaces as `422` with the distinct failure reasons in `detail`. URLs resolving to non-globally-routable addresses (loopback, private ranges, link-local, cloud-metadata) are refused before any connection opens.
The response is final — the packet is fully imported when you receive `201`. If the server-side import takes longer than the gateway's 6-minute polling ceiling, the request fails with a `504` (`retryable: true`); it is safe to retry with the same request body. Trigger extraction with [`POST /v3/document-packets/{document_packet_id}/run/`](/api-reference-v3/document-packets/run).
```bash curl theme={null}
curl -X POST 'https://api.anyformat.ai/v3/workflows/0686bb97-8c30-70f0-8000-97669e000eb8/upload/from-url/' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"urls": [
"https://example.com/invoices/april.pdf",
"https://example.com/invoices/april-annex.pdf"
]
}'
```
```python Python (requests) theme={null}
import requests
workflow_id = "0686bb97-8c30-70f0-8000-97669e000eb8"
url = f"https://api.anyformat.ai/v3/workflows/{workflow_id}/upload/from-url/"
headers = {"Authorization": "Bearer YOUR_API_KEY"}
body = {
"urls": [
"https://example.com/invoices/april.pdf",
"https://example.com/invoices/april-annex.pdf",
]
}
packet = requests.post(url, headers=headers, json=body).json()
print(packet["document_packet_id"])
```
```json Response (201 Created) theme={null}
{
"document_packet_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"workflow_id": "0686bb97-8c30-70f0-8000-97669e000eb8",
"files": [
{ "id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e2", "name": "april.pdf" },
{ "id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e3", "name": "april-annex.pdf" }
]
}
```
```json Response (422 — a fetch failed, nothing imported) theme={null}
{
"error": "Remote fetch failed",
"detail": "Remote server returned 404",
"error_code": "VALIDATION_ERROR",
"retryable": false,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
```
# Changelog
Source: https://docs.anyformat.ai/changelog/overview
Stay up to date with the latest changes and improvements to the anyformat API.
## July 2026
### July 7, 2026
**\[New + API]** API v3 launched — document packets and runs as first-class resources
The public API's new stable major is live under `/v3/`, rebuilt around three resources: **workflows**, **[document packets](/concepts/document-packets)** (the runnable bundle of 1+ files — v2's "file collection", renamed and promoted), and **[runs](/concepts/runs-and-results)** (one execution attempt of a workflow against a packet, with its own id, status, and results). The full reference lives in the new [v3 API tree](/api-reference-v3/introduction); v2 users can follow the [path-by-path migration guide](/api-reference-v3/migrating-from-v2). Highlights:
* **Flat run reads, no more 412-polling** — [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get) always returns `200` with a `status` field; `results` carries the (unchanged) results envelope inline once `status` reaches `processed`. The `/results/` sub-path is gone.
* **One upload shape** — [`POST .../upload/`](/api-reference-v3/workflows/upload) groups 1–10 files into a single packet atomically, [`.../upload/run/`](/api-reference-v3/workflows/upload-and-run) does upload + run in one call and returns a `run_id`, and [`.../upload/from-url/`](/api-reference-v3/workflows/upload-from-url) imports 1–10 HTTPS URLs all-or-nothing.
* **Re-run without re-uploading** — [`POST /v3/document-packets/{id}/run/`](/api-reference-v3/document-packets/run) creates a fresh run each call; earlier runs stay readable.
* **The workflow read is the definition** — [`GET /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/get) returns the complete typed graph with a `persistent_id` per field, and [`PATCH`](/api-reference-v3/workflows/update) echoes that shape back: echo the `persistent_id` and a renamed field keeps its identity (analytics, ground truth, and quality metrics stay attached). There is no separate `/definition/` endpoint and no `PUT` in v3.
* **Keyset pagination everywhere** — every list returns `{items, next_cursor}` with `?limit=` (max 100) and `?cursor=`; no totals or page numbers.
* **Idempotent submission** — the upload and run-trigger POSTs accept an [`Idempotency-Key` header](/api-reference-v3/introduction#idempotency); a retry with the same key replays the original response instead of creating a duplicate packet or billing a second extraction.
* **`X-API-Version: 3.0.0`** is stamped on every `/v3/` response.
v2 is unchanged and keeps serving traffic through its announced deprecation window (see the July 3 entry below). Authentication, the error envelope, the results envelope, webhooks, and rate limits are identical across both versions.
### July 6, 2026
**\[Changed + API]** Dataset membership moves to collection-keyed, idempotent endpoints
Test-set membership is being renamed to the product's **dataset** vocabulary and re-keyed onto the collection — the identity a document already carries everywhere else (the `id` in every document response). Membership now lives under the workflow: `PUT /api/v3/workflows/{workflow_id}/dataset/files/{collection_id}/` adds a document to the dataset (optionally snapshotting a chosen extraction as ground truth via `{ "extraction_id": … }`), `DELETE` the same path removes it, and `POST /api/v3/workflows/{workflow_id}/dataset/files/bulk/` adds a filtered grid selection. All three writes are **idempotent** — re-adding a member or removing a non-member is a `200`/`204` no-op instead of a `409`. The test-file list rows now include a `collection_id` so callers can address membership without a second lookup.
The previous file-scoped routes `POST /api/v3/files/{file_id}/test-set/` and `DELETE /api/v3/files/{file_id}/test-set/` still work but are **deprecated** and now idempotent too; migrate to the `dataset/` paths above.
### July 3, 2026
**\[Deprecated + API]** v2 enters its deprecation window — `Deprecation`/`Sunset` headers on every response
With v3 as the new stable major, `/v2/` is frozen: no new features, existing behavior untouched. Every `/v2/` response now carries the [RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594) deprecation signals — `Deprecation: Fri, 03 Jul 2026 00:00:00 GMT` and `Sunset: Sat, 03 Oct 2026 00:00:00 GMT`. Per-route `Link: <...>; rel="successor-version"` headers pointing at the v3 equivalent follow as the mappings are wired up. `/v3/` responses carry none of these headers.
**Action recommended:** wire an alert on the `Sunset` header so migration work can't be missed — [Versioning & deprecation](/api-reference/introduction#versioning--deprecation) shows a five-line client middleware that does it. Then plan the move with the [v2 → v3 migration guide](/api-reference-v3/migrating-from-v2). No client changes are required today; v2 keeps working until the sunset date.
## June 2026
### June 29, 2026
**\[New + API]** Deterministic validation rules — instant, free checks alongside AI rules
A **Validate** step can now mix two kinds of rules. **AI** rules stay as they were: you describe the condition in plain language and a model judges it. **Deterministic** rules are new — structured checks that run in plain code with no model call, so they're instant and **free**. Seven check types ship: **number range**, **date range** (with a relative `today` bound for not-expired checks), **arithmetic** (`subtotal + tax = total`, with a tolerance), **comparison** (a field vs a value or another field), **one-of** (value in an allowed set), **pattern** (regex), and **required** (present and non-empty). Build them in Studio with the new **AI / Deterministic** toggle on each rule, or author them via the API and SDKs — deterministic rules reference fields by name and carry a `check` payload, AI rules carry a `description`. Pattern checks run on a linear-time regex engine, so a user-supplied pattern can't hang an extraction. Existing workflows are unchanged: rules default to `kind: "ai"`. See [Validation rules](/guides/studio/validate-rules).
### June 22, 2026
**\[New]** Self-serve Business subscriptions, recurring monthly credits, and a billing incident banner
Business is now a self-serve plan. From the dashboard, an organisation owner or admin can click **Upgrade to Business** to open Stripe Checkout, enter a card, and become a paying Business customer in a single round-trip — no support ticket required. Once active, the organisation receives a fresh **100,000-credit monthly grant** on each successful renewal, replacing the previous month's grant rather than stacking on top of it. Enterprise is unchanged in spirit (still sales-led) but rides the same plumbing: support agrees on a custom monthly grant and price, mints a dedicated Stripe price, and pins both to the org on a `BillingPlan` row. Top-ups still work on top of the recurring grant for one-off credit additions.
Subscription customers get a **Customer Portal** link from the **Usage** page to manage their payment method and cancel the subscription themselves. When Stripe reports a failed renewal, a **billing incident banner** appears on every authenticated page describing the current state (`PastDue` during the grace window, `Suspended` if grace expires, `Canceled` after cancellation) with a deep-link to the action that resolves it — update the card, reactivate, or restart the subscription. Late-payment recovery is exact: if a card is fixed after the grace window has zeroed the grant, the next successful invoice restores `cycle_monthly_grant − consumed_during_unpaid_cycle` credits (floored at zero), so a customer never pays for and then loses what they already used.
This rollout is gated behind `BILLING_SUBSCRIPTIONS_ENABLED`. Existing Business and Enterprise organisations whose tier was set by the legacy one-shot upgrade keep their tier through a fallback path until their first real subscription invoice clears.
### June 18, 2026
**\[New + API]** `GET /v2/workflows/{workflow_id}/definition/` — round-trippable workflow definition
A new sub-resource returns the workflow's typed graph in exactly the shape `PUT /v2/workflows/{workflow_id}/` accepts. SDK callers can now GET, mutate one field, and PUT the result back without re-authoring the rest of the schema. The PATCH side preserves `persistent_id` per `(node_id, field_name)` so an unchanged echo cuts no new version (`ChangeKind.NONE` modulo edge order), and genuine edits keep `persistent_id` stable for unchanged-by-id fields — analytics, ground-truth, and monitoring continuity all hold across edits.
**\[Changed + API]** Field `description` is no longer required to be non-empty
The typed surface (`AnyField.description`) previously enforced `min_length=1`, but Path A persistence (the Studio frontend's save path) never enforced it — so legacy workflows can carry fields with empty descriptions that the new GET-definition endpoint needs to round-trip cleanly. Empty descriptions are now accepted on `POST /v2/workflows/`, `PUT /v2/workflows/{id}/`, and the generated SDKs. SDKs and docs still encourage non-empty descriptions — an empty value degrades extraction quality, it isn't structurally invalid.
**\[New + API]** `POST /v2/workflows/{workflow_id}/files/from-url/` — upload by URL instead of multipart
A new route accepts an HTTPS URL and tells anyformat to fetch the bytes server-side, returning the same `CreateCollectionResponse` shape as the multipart `POST /v2/workflows/{workflow_id}/files/` upload. Use it when the source document already lives in object storage you can presign (S3, GCS, R2, …) or at a public HTTPS URL — no need to stream bytes through your own backend. The fetch is bounded by a 10-second timeout and the same 20 MB cap as multipart upload, and runs through the outbound-URL SSRF guard so URLs resolving to non-globally-routable IPs (loopback, RFC1918, link-local, cloud-metadata, IPv4-mapped IPv6) are refused before any connection opens. HTTPS-only at the gateway; non-2xx upstream, DNS failure, or timeout surfaces as `422`. Documented under [Upload File from URL](/api-reference/files/create-from-url) and in [Files](/concepts/files#how-to-provide-a-file).
### June 15, 2026
**\[Fixed + API]** SDK consumers can pull per-file results through `/v2/workflows/{wf}/results/?file_id=...` again
The v3 route `GET /api/v3/files/{file_id}/results/` declared its `workflow_version_id` query parameter as `int`, but the v2 gateway forwards the 10-character `WorkflowVersion.external_id` string returned by `/api/v2/.../latest_version/`. Ninja's int coercion rejected the string with `422`, breaking any SDK call that chained `latest_version` into the per-file-results lookup. The route now accepts `workflow_version_id` as a string, resolves the `external_id` to the internal PK in-route, and returns `404` (not `422`) for an unknown id — matching every other v3 endpoint's "the public `external_id` is the single public identifier" contract.
### June 11, 2026
**\[Breaking + API]** Object fields support only one level of nesting
Object fields have always been limited to **one level of nesting** — children of an object must be non-object types — but the API surface advertised the opposite: `ObjectField.nested_fields` was typed recursively, so the generated OpenAPI spec listed object-in-object schemas as valid. The contract is now enforced at the source: posting a schema with a nested object to `POST /v2/workflows/` returns `422`, and the same constraint is mirrored in the regenerated TypeScript SDK types and the Python SDK CLI (which now returns a clean "only one level of nesting is supported" error instead of a raw pydantic traceback).
**Action required:** schemas that declared an object inside an object must be flattened before submission. Single-level object fields are unaffected.
**\[Breaking + API]** `POST /api/v3/extractions/progress` removed; replaced by the workflow-progress digest
The batch progress endpoint `POST /api/v3/extractions/progress` is gone. Callers that want per-extraction progress alongside per-status file counts for a workflow version should switch to the new `POST /api/v3/extractions/workflow-progress-digest` (see the \[New + API] entry below). Single-extraction progress via `GET /api/v3/extractions/{uuid}/progress` is unchanged.
**\[New + API]** `POST /api/v3/extractions/workflow-progress-digest` — one round-trip for workflow polling
A new digest endpoint returns per-extraction progress for a caller-supplied set of `extraction_uuids` AND unfiltered per-status file counts for the surrounding `workflow_version_id` in a single response. The dashboard now polls this endpoint once every 4 seconds while files are processing, replacing three separate polls (`/extractions/progress` at 2.5s, and the v2 `/files/count/` and `/files/` polls at 5s). Polling stops automatically when no extraction is queued or in-progress. Counts come back `null` for unknown or cross-organisation workflow versions; cross-organisation extraction UUIDs are silently dropped, matching the existing single-extraction-progress semantics.
**\[New + API]** Cell-level grounding on extraction evidence
Table-row and scalar evidence now carries true cell provenance end-to-end: `evidence` and `mapped_locations` entries on extraction responses gain optional `cell` and `row` keys identifying the exact source cell that fed each value. Evidence without cell-level grounding (non-table datapoints, or table rows the model couldn't ground precisely) stays byte-identical to before — the new keys are emitted only when present. On the reference invoice that motivated this work, table rows with precise grounding went from 0% to 100% (626/626) with zero wrong-row assignments, and scalars whose value lives in a header table (invoice ids, dates) now resolve to one precise evidence on an LLM-cited page instead of a page-backfilled guess.
**\[New + API]** Test-set membership for files
Files can now be designated as part of a curated, locked test set. A new boolean `is_test_file` is returned on file responses, and two v3 routes manage membership: `POST /api/v3/files/{file_id}/test-set` to add and `DELETE /api/v3/files/{file_id}/test-set` to remove. Files in the test set are **frozen from re-verification**: every verification write (verify, unverify, edit, discard at the datapoint level; verify-extraction; add/delete/swap/verify-row/verify-all at the datarow level, across both v2 and v3) now returns `409 Conflict` with code `ground_truth_locked`, so an extraction designated as ground truth cannot be mutated out from under a downstream accuracy scorer. Ground-truth values and accuracy reporting land in a follow-up release.
**\[New]** Download lookup files from the Extract node UI
Lookup files (master files attached to an Extract node) were upload-only — once added, you couldn't get them back. The **Manage files** dialog in the Extract node now has a download button next to each lookup file, backed by a new presigned-URL endpoint `GET /api/v2/saas_manager/workflows/{id}/master_file/download/?uri=`. Org membership and the workflow-scoped S3 key prefix are both enforced; the presign is pinned to the canonical bucket so a crafted cross-bucket URI cannot redirect the signed GET.
**\[Improved]** Table extraction now always uses the refine engine
Table parsing previously branched upfront on a heuristic between a cheap `simple_table` call (one shot, no retry — silent data loss on empty/truncated output) and a much slower `dense_table` agent (\~10–20× the cost and latency). The classification was fuzzy and data integrity depended on getting that one-shot guess right, so the routing prompt was biased toward dense, over-escalating and burning cost. Every table now takes the same path — cheap parse → audit critic → in-place refinement with tools — with a best-draft-wins guarantee, so a misclassification can no longer lose data. On an 18-document Iberia benchmark refine was **27% cheaper** (cheaper on every doc) and substantially faster on the slow tail (worst single doc 1346s → 317s), with all four `full_content` quality metrics improving. As part of the change the routing prompt now requires ≥2 rows × ≥2 columns of tabular evidence before sending a block to the table pipeline, so label/value pairs, signatures and logos are no longer misrouted as tables.
**\[Improved + API]** Workflow analytics timeseries response is much smaller and faster
`GET /api/v3/workflows/{id}/analytics/timeseries` now serves the default monitoring view in **a few KB of gzipped JSON** instead of 1.7–3.9 MB. The "Overall" series across all fields is shipped pre-collapsed as a new `overall_rows` array (one entry per extraction), and an opt-in repeatable `field_ids` query parameter narrows the legacy `rows` payload to only the fields you want — present-but-empty (`?field_ids=`) returns counts and `overall_rows` only. Calls that omit `field_ids` still receive the full legacy rows, so existing integrations keep working unchanged. The cold-path recomputation now scopes the live aggregation to cold versions instead of the full workflow, self-heals to all-warm on the first full-universe read, and the response is gzip-compressed.
### June 10, 2026
**\[New]** Per-user timezone preference for API datetimes
User profiles now carry an IANA timezone preference, defaulting to `Europe/Madrid`. Datetime fields in API responses are serialized in the user's chosen zone, and the dashboard **Account** page gains a searchable timezone selector that persists through the existing profile-update flow. Storage stays UTC end-to-end — only the wire serialization (and transitively, what the dashboard renders) reflects the preference.
**\[Fixed + API]** Oversized request bodies now return `400` instead of `500`
Any v3 endpoint that reads a request body returned a `500` when the JSON payload exceeded the 20 MB limit (`DATA_UPLOAD_MAX_MEMORY_SIZE`): Django raised `RequestDataTooBig` while django-ninja read `request.body` during parameter resolution, before the view ever ran, so it could not be caught inside the endpoint. A global exception handler on the v3 API now returns `400` with `{"detail": "Request body is too large. Maximum allowed size is 20 MB."}`. The limit in the message is derived from the setting, so it stays accurate if the cap is tuned.
**\[API]** Rate limiting now enforced on the external API in production
The external API's rate limiter is now active in production. It had been silently disabled for roughly three months because `RATE_LIMIT_ENABLED` and `REDIS_URL` were added to the dev config but never propagated to prod. Callers exceeding the configured limits may now receive `429` responses.
**\[Breaking + API]** Deprecated v2 datapoint and file-verification endpoints removed
The following v2 endpoints are gone. They were already marked deprecated and verified unreachable from the dashboard, the external API, the SDKs, and the cronjobs before removal:
* `PATCH /api/v2/.../datapoints/{id}/` and `POST` / `DELETE /api/v2/.../datapoints/{id}/validation/`
* `POST /api/v2/.../files/{id}/verify/`
**Action required:** integrations that still target these paths must switch to the v3 replacements: `POST /api/v3/extractions/{uuid}/datapoints/{verify,unverify}` for verification toggles, `POST /api/v3/extractions/{uuid}/datapoints/{edit,discard}` for value edits and discards, and `POST /api/v3/extractions/{uuid}/verify` for whole-extraction verification.
**\[Fixed]** Verify Document button now works on split + extract workflows
The header **Verify Document** button used to be a permanent no-op on workflows that combine a Split node with an Extract: it targeted the file-level extraction, but extracted data on a split lives on per-split child extractions, so verifying the parent verified nothing. The button now targets the **active split's child extraction** (and is labelled **Verify Extraction** on a split tab to match), and is disabled on parse, split, and validation tabs where there is nothing to verify. A split file is reported as verified only once every non-empty child extraction is verified — empty category tabs no longer wedge the rollup. Shift+Enter advances through unverified splits before moving on to the next file.
**\[Improved + API]** New `verifier` API surface; `validator` deprecated
The "human-validation → verification" rename now reaches the file-level API. A new endpoint `POST` / `PATCH /api/v2/.../files/{id}/verifier/` assigns and updates the verifier for a file, a `verifier__in` filter narrows file lists by assigned verifier, and `?include=verifier` returns the assigned verifier in file responses. The legacy `/validator` action, `validator__in` filter, and `?include=validator` option remain live for backward compatibility but are now flagged `deprecated` in the OpenAPI spec; integrations should plan the move.
**\[Fixed + API]** Non-member reads of workflow analytics now return `404`
`GET /api/v3/workflows/{id}/analytics/summary` and `GET /api/v3/workflows/{id}/analytics/timeseries` previously returned `403 Forbidden` to a caller outside the workflow's organisation, leaking the existence of the workflow. They now return `404 Not Found`, matching `GET /api/v3/workflows/{id}`. Members continue to receive analytics as before.
**\[Fixed]** New free accounts now see their signup credits on the Usage page
The **Usage** page counted only purchased and granted credits, so a brand-new free account — whose only balance is its signup bonus — showed a zero balance and the empty-state screen instead of the credits it could actually spend. Signup credits are now included in both the headline total and the per-source breakdown.
### June 9, 2026
**\[API]** New v3 endpoints for row-level edits on object fields
The v3 API now exposes positional row operations on object-field tables: add, delete, swap, verify-row, and verify-all. Rows are addressed positionally by `(extraction_uuid, field_persistent_id, index)` instead of an opaque datarow id, and the dense `[0, n_live]` invariant is enforced at the boundary (negative indexes now return `422` instead of a silent `404`). The corresponding v2 actions on `DatarowViewSet` are now marked deprecated but remain live; integrations should plan to migrate.
**\[Improved]** Document parsing now retries transient LLM failures more reliably
The parse pipeline now retries `LengthFinishReasonError` (model hit max-tokens, often non-deterministic) and detects silent parse failures where the LLM returned a 200 but the response could not be coerced into the schema. Both previously skipped straight to the fallback model on the first attempt; they now go through the configured retry budget on the primary model first. The per-page parsing token budget was also raised from 8K to 32K to reduce length-truncation failures, and `ParseFailedError` now carries the underlying per-page error detail.
**\[Fixed]** Lookup + Validate workflows now complete end-to-end
Two independent bugs that broke the same Lookup + Validate workflow are fixed:
* A `Validate` node downstream of an `Extract` node with lookup fields previously returned every rule as *inconclusive ("No extraction data available to validate.")*, because the validate step resolved its upstream to a synthetic lookup node and read a state key nothing ever wrote. It now resolves through smart-lookup hops back to the real producing extract.
* The Gemini page-sum verification schema was missing a top-level `title`, which made model-build raise outside the per-task `try/except` and aborted the whole extraction whenever Phase-1 sum verification failed and Gemini verification was attempted.
**\[Improved]** Faster admin pages, file listings, and date-filtered results
Several database hotspots flagged by APM are now indexed and the corresponding queries rewritten to be sargable:
* `Extraction.uuid` and `File.name` get btree indexes (built `CONCURRENTLY`), eliminating sequential scans on the hundreds of lookups per day they served.
* A new composite `FileCollection(workflow_id, created_at DESC)` index backs the per-workflow collection listing.
* `created_at` date filters across the admin dashboard, billing workflow-usage queries, and the public `ResultsFilter` are rewritten from `(created_at AT TIME ZONE 'UTC')::date` casts (which disabled the index, costing up to \~2s on a single filter path) to half-open `created_at` ranges. Behavior is unchanged because the deployment timezone is UTC.
**\[Breaking + API]** Deprecated v2 datarow endpoints removed
The v2 `DatarowViewSet` actions marked deprecated in the same release as the v3 datarow rollout — `create_empty`, `delete_by_position`, `swap`, `validate_row`, `validate_all` — and their five `/api/v2/.../files/{file_id}/datarows/` routes are now removed.
**Action required:** integrations that still drive row-level edits through the v2 paths must migrate to the v3 surface: `POST /api/v3/extractions/{extraction_uuid}/datarows/{add,delete,swap,verify-row,verify-all}`.
**\[Improved]** Per-category grid restores split-granular column filtering
Workflows that include a Classify or Split node show their extracted data in the Per-Category grid in the data viewer. Column filters on that grid had been deliberately disabled because the previous file-paginated path compared against the wrong extraction. A new split-granular results endpoint backs the grid: a filter on an extracted field now narrows to exactly the matching split results, paginated and counted server-side, with filter and sort behavior matching the main file grid.
### June 5, 2026
**\[Improved]** Transient frontend request failures now auto-retry with backoff
The web app retries idempotent React Query requests that fail with transient connectivity errors or 5xx responses, using capped exponential backoff (up to \~10s) and up to 3 attempts. Deterministic 4xx responses and unknown errors still fail fast. Connectivity errors are now detected via the Fetch spec's `TypeError` instead of message matching, which cuts down on false-positive error reports during brief network blips.
**\[API]** Malformed `Authorization: Bearer` headers now return `401`
`Authorization` headers with malformed bearer tokens (for example, an empty token or whitespace inside the token value) now return `401 Unauthorized` instead of `500 Internal Server Error`. The successful auth path and well-formed token-rejection responses are unchanged.
**\[Fixed]** Newly created workflows lay out their nodes with wider default spacing
Newly created workflow nodes are placed with a larger default gap so the auto-layout reads more clearly out of the box and matches the spacing the rest of the editor assumes.
**\[Fixed]** Full-page loading spinner no longer jumps during initial load
The full-page spinner stays anchored in place while the app boots, instead of shifting position as the surrounding layout settles in.
**\[New]** How credits work — new pricing & credits docs page
A new Concepts → Account page, **How credits work**, documents per-operator credit costs (including Validate at 5 credits per rule per page), plan €/credit conversion, included credits, usage tracking, and tips for keeping cost down. The Usage & Billing page links to it in place of the previous "contact us for pricing" stub.
**\[Improved]** Monitoring over-time chart now plots on a true time axis
The accuracy and confidence over-time chart on the Monitoring tab positions each extraction at its real timestamp on a time-scaled axis, instead of forcing points onto an evenly-spaced grid. Time-bucketing now adapts to the visible span, so long, dense histories coarsen gracefully while short spans stay at full resolution. Version markers sit at their exact creation time and merge into a single ranged label (for example `v1.0–v1.2`) when several versions ship the same day, and the empty states now distinguish between no rows, no reviewed extractions, and no recorded confidence scores.
**\[Improved]** Large workflow versions no longer time out when loading file counts
Loading the file counts for a high-volume workflow version no longer tips over the request timeout. The count is now served by a single annotated aggregate backed by an index-only scan, replacing the per-file status scan that caused worker timeouts on workflows with many files.
**\[Fixed]** Filtering and sorting restored on smart-lookup columns
Columns backed by a smart-lookup field keep their sort and filter controls in the data viewer while still showing the lookup indicator. Previously the custom lookup header replaced the entire header cell, hiding the filter button on those columns even though filtering still worked underneath.
**\[Fixed]** Overlapping bounding-box highlights no longer wash out the PDF page
In the PDF visualizer, hovering a stack of overlapping bounding boxes now reads as a single translucent tint on both light and dark pages, instead of compounding toward opaque as more boxes overlapped. Boxes shared by many fields, such as table sections, also render more efficiently.
### June 2, 2026
**\[Breaking + API]** Monitoring summary `total_extractions` now counts processed extractions only
`GET /api/v3/workflows/{id}/analytics/summary` previously counted every queued extraction in `total_extractions`, including in-flight rows that had not yet finished processing. It now counts processed extractions only — matching the `verified`, `through`, and per-field counts already returned by the same endpoint, and matching what the cold path (no version filter) has always reported.
**Action required:** if your integration reads `total_extractions` from the summary endpoint and expects in-flight rows to be included, switch to a non-summary query or add the in-flight count from your own tracking. The Studio monitoring UI is unaffected.
**\[Improved]** Monitoring page now reveals data progressively
Each card on the Monitoring tab paints its title and controls immediately and shows a skeleton only for the body that is still loading, off the data source that feeds it. A fast endpoint can paint without waiting on a slower sibling, instead of the whole page sitting behind a single page-level skeleton. The time-series chart also now keeps daily resolution on 30-day ranges instead of collapsing into \~5 weekly points.
**\[Improved]** Faster workflow version analytics summary
`GET /api/v3/workflows/{id}/analytics/summary` is significantly faster on high-volume workflow versions. Overall metrics, per-field metrics, verified counts, and throughput are now computed in two table scans instead of five, with no change to the response shape.
The endpoint previously routed through the paginated read path and ran the per-extraction aggregate twice plus a discarded pagination subquery. It now serves the summary from a dedicated read that combines the by-field `GROUP BY` and a single per-extraction `COUNT(...) FILTER(...)` aggregate, with the paginated read path unchanged for the timeseries and detail endpoints.
**\[Improved]** Faster workflow version analytics timeseries
`GET /api/v3/workflows/{id}/analytics/timeseries` is now significantly faster on high-volume workflow versions, applying the same optimization as the summary endpoint. The response shape is unchanged.
The timeseries endpoint consumes only the per-extraction detail rows, but was still served by the full five-scan paginated read — computing the overall sum, per-field `GROUP BY`, and through-rate counts only to discard them. It now uses a detail-only read, and resolves the latest N extractions from the `Extraction` table (which owns `processed_at`) instead of grouping the multi-million-row analytics cache — roughly 34× faster on the top-N lookup, with existing indexes only and no migration.
**\[API]** Deprecated combined `/analytics` endpoint removed
The deprecated `GET /api/v3/workflows/{id}/analytics` route (flagged for removal when the frontend migration completed) is gone. Use `GET /analytics/summary` for per-version overall and per-field metrics, and `GET /analytics/timeseries` for the cross-version series.
**\[Fixed]** File validation status stays in sync with its datapoints
When you confirm the last unverified datapoint on a file, the file badge now reliably flips to **Validated**, and when you unverify a previously-verified datapoint the file rolls back to **Processed**. Previously, the file-level validation status could drift from its datapoints — most commonly, the last cell confirmation would not flip the file badge.
**\[Improved]** Organization switcher now scales to many organizations
The sidebar organization switcher caps at 10 visible organizations by default with a **Show all** expander, and adds a search box that fuzzy-matches organization names across your full list. Organizations you're a direct member of are prioritized over support-access ones, and a per-user most-recently-used ordering surfaces the organizations you actually work in first. The current organization is always shown.
**\[Fixed]** Deep links to a file's results now switch to the workflow's organization
Opening a workflow file via a direct URL or in a new tab (middle-click) now elevates the active organization to the one the workflow belongs to, matching click-through navigation. Previously a cold-loaded file results page kept the UI on your own organization, hiding the support-access banner and showing the wrong organization in the sidebar.
**\[Fixed]** Organization selector rows stay a readable size on long lists
When you belong to many organizations, each row in the sidebar organization selector keeps a consistent, tappable height with visible spacing, instead of compressing to near edge-to-edge as the list grows.
### June 1, 2026
**\[New]** Workflow version selector
The workflow page header now has a version selector (on the Studio and Monitoring tabs) so you can view any saved version of a workflow. Selecting a non-latest version on Studio disables editing with an explainer — only the latest version can be edited — while Monitoring scopes its analytics to the version you pick.
**\[New]** Per-version monitoring history
The Monitoring tab now shows version history: the accuracy/confidence chart spans all versions with markers at each version change, a changelog panel lists what changed, and KPIs and by-field metrics can be scoped to a selected version.
The analytics endpoint was split into purpose-built reads — `GET /analytics/summary?version_id=` (per-version overall + per-field means and through-rate counts) and `GET /analytics/timeseries?limit=` (cross-version series) — and analytics are now paginated by extraction rather than by (extraction, field) row, so wide schemas no longer collapse the window. The old `/analytics` endpoint is deprecated but retained until the frontend migration completes.
**\[API]** Python SDK now installs as `anyformat`
The official Python SDK's PyPI distribution has been renamed from `anyformat-sdk` to `anyformat` — `pip install anyformat` — mirroring `@anyformat/sdk` on npm. The import path is unchanged (`from anyformat.sdk import Client`).
The distribution starts at version 0.6.0 (the next free slot past the legacy `anyformat==0.5.0` on PyPI). The internal CLI binary that previously occupied the `anyformat` console-script slot was renamed to `af`; `from anyformat.cli import ...` still works.
**\[Improved]** Workflow data sub-tabs keep their structural entry points
When a workflow has many data sub-tabs, the collapsed row keeps the leading tabs (Documents, Split/Classify, the first categories) and pins the selected tab to the end, with the overflow in a "+N" menu — instead of hiding everything but Documents and the active tab. Rows with five or fewer sub-tabs show all of them.
## May 2026
### May 28, 2026
**\[Changed]** Validator nodes now consume credits
`ValidateNode` previously ran for free. Each rule in a validator now costs 25 credits, billed once per extraction. The bill scales with the number of rules in the validator, not with the number of pages or files in the document packet.
A new `validate` operator (`Credits(25)`) was added to `DEFAULT_OPERATOR_PRICES`. The internal billing dispatch was reshaped from `(item) -> list[OperatorName]` to `(item, page_count) -> list[LineItem]`, so non-page-priced operators like validators can ignore the page count and substitute `len(rules)` as the billed unit. The processor now issues a single `SpendCreditV4` call per extraction with all line items combined.
**\[Changed]** Default operator prices realigned with the public pricing page
Default credit prices for several operators now match [anyformat.ai/pricing](https://anyformat.ai/pricing): Parse 25 (was 30), Extract 35 (was 20), Splitter 25 (was 10), Parse (Agentic) 100 (was 75), Extract (Agentic) 150 (was 75). Classify is now billed at 10 credits/page (previously a no-op — running a Classify node was free regardless of the published rate). Organizations with enterprise-specific overrides on `OperatorPriceTable` are unaffected.
**\[Improved]** Documentation restructured into Concepts / Guides / API Reference
[docs.anyformat.ai](https://docs.anyformat.ai) has been reorganized into three scoped tabs. **Concepts** holds definitional pages (Workflows, Schemas, Fields, etc.). **Guides** holds task-oriented walkthroughs (Quickstart, Build workflows, Recipes). **API Reference** holds the REST spec only. The four overlapping "create → run → results" walkthroughs are merged into one Quickstart with UI / curl / Python tabs at each step, and every extraction recipe now uses the canonical typed-graph workflow shape (`{name, nodes, edges}`) instead of the legacy `{fields:[...]}` shortcut. Old URLs redirect.
**\[Improved]** Post-signup `/welcome` page is now reachable only from the signup callback
Loading `/welcome` directly (typed URL, bookmark, fresh tab) now bounces to `/`. The page is only reachable after a brand-new Auth0 signup callback, which closes a path that could let the upstream GA4 / GTM conversion event fire for already-signed-up users.
**\[Improved]** `@anyformat/skill` synced to npm 0.2.2
The `@anyformat/skill` package version in this repo is now aligned with what's live on npm (`0.2.2`). The publish script uses token-based auth and bumps from `max(local, registry)` to prevent version collisions on the next release.
**\[Improved]** Release-PR generation now audits merged PRs upfront
The `/prod-release` Claude Code command (used to open this release PR) now treats the merged-PR list as the ground truth and the changelog as a derived artifact. The audit step surfaced the gap that produced the entries below — previously, PRs merged without a changelog entry could quietly drop out of the release notes.
### May 27, 2026
**\[New]** Attach smart lookup files to fields in the workflow SDK
The Python and JS SDKs can now mark a field as a smart lookup field and attach a reference file when creating a typed workflow. Flag the field with `lookup` and point it at a local CSV or text file; the SDK reads the file and uploads it inline, and extraction resolves that field's values against your reference data. Lookup files are validated as UTF-8 text before anything is written — base64 payloads are never stored or returned.
`POST /api/v2/workflows/` accepts a per-field `lookup` flag (mapped to `source=smart_lookup`) and inline base64 `lookup_files`. Files are pre-validated as UTF-8 text/CSV (with cp1252→UTF-8 transcoding); binary or non-text uploads are rejected with a `400 UNSUPPORTED_LOOKUP_FILE` before any S3 writes. Only S3-backed lookup file references are persisted; base64 payloads and URIs are never stored or echoed back.
**\[Improved]** Excel workbooks now convert to one Markdown page per sheet
Excel→Markdown conversion now emits a separate page for each worksheet, in workbook order, instead of concatenating every sheet into a single page. Multi-tab spreadsheets now classify and process per tab, keeping each sheet's content distinct downstream.
**\[New]** Official Python SDK now available on PyPI
`pip install anyformat-sdk` — the `anyformat-sdk` package is now live on PyPI. Use the fluent builder to author, create, run, and await workflows end-to-end without writing HTTP code; sync and async clients are included, with typed errors (`BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `RateLimited`, `ServerError`, `SDKTimeout`) and a `.raw` escape hatch on every response. The Claude Code skill at `@anyformat/skill` now documents the Python SDK alongside the JS/TS one.
**\[New]** Post-signup `/welcome` landing page
Brand-new signups now land on a dedicated `/welcome` page (instead of going straight to `/`), giving marketing a stable URL for Google Ads conversion configuration via GA4 / GTM URL match. Returning users continue to land on `/` (or the originally-requested URL) as before.
**\[Fixed]** New signups for an already-registered email address are now rejected
Across every API write site that can create a User row (Auth0 just-in-time provisioning, the E2E test-auth endpoint, the `create_user` management command), case-insensitive duplicate emails are now refused before a second row can be created. Auth0 email rotation that would collide with an existing user is also refused. Previously, mixed-case variants of the same address could silently mint duplicate users.
**\[Fixed]** Resolved four frontend dependency vulnerabilities
`qs` (DoS in stringify), `tmp` (path traversal), `uuid` (missing buffer bounds check), and `js-cookie` (prototype hijack) advisories are now resolved via package overrides in `anyformat/services/frontend`. Upstream parents (cypress, exceljs, segment-analytics) hadn't yet adopted the patched versions, so overrides were the minimally-disruptive path.
**\[Improved]** Authenticated images are no longer refetched multiple times per page load
The sidebar mounts the organization logo from three places, which previously meant three identical requests to `/api/v3/iam/organizations/{id}/logo/` on every page load. The image hook now uses TanStack Query so concurrent subscribers share one in-flight request and the result is cached for 5 minutes.
**\[Improved]** Local development supports both `localhost` and `127.0.0.1` for OAuth
For developers running the stack locally, both `http://localhost:/callback` and `http://127.0.0.1:/callback` are now valid OAuth callback URIs and CORS / CSRF origins. Previously only one was registered per environment, which caused login failures if the dev frontend was opened at the other hostname.
**\[Improved]** JS SDK build toolchain refreshed
`esbuild` (0.21.5 → 0.27.7) and `vitest` (2.1.9 → 4.1.7) in the JS SDK package have been updated. No behavioral changes to the SDK itself.
### May 26, 2026
**\[Improved]** Higher monthly allowance on the Business plan
The Business plan's monthly credit grant has increased to 500,000 credits.
**\[Changed]** Free tier now gets a one-time 50,000-credit signup grant instead of a monthly allowance
Free organizations no longer participate in the monthly credit rotation. Each new free org now receives a single 50,000-credit signup grant that never expires — generous enough to evaluate the product, finite enough to make upgrading the natural next step. Paying tiers (Business, Enterprise) keep their recurring monthly allowance unchanged.
**\[Changed]** Stripe top-up pricing is now tier-aware
Top-up checkout rates depend on the organization's tier: free orgs pay €1.50 per 1,000 credits (€0.0015 / credit), Business and Enterprise orgs pay €1.00 per 1,000 credits (€0.001 / credit). Top-up amounts are now denominated in credits (minimum 30,000) instead of euros.
### May 25, 2026
**\[New]** Visualize parse layout with `result.parse.draw()`
The Python SDK can now render a document's parsed layout as an image overlay. Call `result.parse.draw()` to get the page with detected blocks drawn on top — useful for debugging extraction and verifying bounding boxes. A new cookbook example walks through it.
**\[Changed]** Simplified parse configuration
The `engine` (Fast/Performant), `effort`, and `visual_grounding` parse knobs have been removed from Studio and the workflow SDK. `engine` was a no-op (both options mapped to the same model), and the other two now use sensible built-in defaults. `figure_enhancement` and `mode` remain configurable. Existing workflows are unaffected — stored values for the removed knobs are ignored gracefully.
**\[New]** Install the anyformat Claude Code skill from npm
The anyformat Claude Code skill is now published on npm as `@anyformat/skill`. Run `npx @anyformat/skill` to install it — Claude Code can then author, run, and inspect anyformat workflows directly from your editor using your `ANYFORMAT_API_KEY`. Public docs include a new "Claude Code Skill" section under the SDKs page.
**\[Fixed]** Classify and split workflows authored via the Python SDK now route to extraction correctly
Workflows built with the Python SDK's `classify(...)` / `split(...)` builders no longer silently skip downstream extraction when a category or split rule is attached by name. The typed graph renderer now resolves branch ports by name (matching the schema's edge labels) instead of internal IDs, so SDK-authored classify/split workflows produce the same extractions as Studio-authored ones.
**\[Fixed]** OCR text containing NUL bytes no longer fails extraction
Extractions whose parse output contained a U+0000 (NUL) character inside a text value previously failed at the database write step, marking the entire batch as errored. The worker now strips NUL bytes from text at the parsing boundary so these documents complete normally.
**\[API]** Organization balance endpoint now returns a per-bucket breakdown
The new organization balance endpoint reports credits broken down by their source bucket — monthly grant, top-ups, and each redeemed voucher — along with the organization's tier and the monthly anchor day. Read-only by design: querying the endpoint never triggers a balance reset or recompute.
`GET /api/v3/billing/organizations/{org_id}/balance/` returns `{ tier, monthly_anchor_at, monthly: {...}, topup: {...}, vouchers: [...], overdraft: {...} }`. Pre-migration organizations return zeroed buckets with a stable shape. Membership is enforced; any role can read.
**\[New]** New organizations receive a monthly credit grant on signup
Newly created organizations now receive a monthly credit grant scoped to their tier, which resets on the organization's anchor day each month. This replaces the previous one-time welcome top-up and makes ongoing free usage predictable instead of front-loaded.
**\[Improved]** Voucher credits now expire after a configurable lifetime
Vouchers can now carry a `credit_lifetime_days` setting (default 90 days). When a voucher is redeemed, the granted credits expire after that many days, so promotional credits don't sit in a wallet indefinitely. Existing vouchers continue to behave as before until the new field is set.
**\[Fixed]** Signup voucher hint shows voucher value only, in your locale
The voucher preview on the create-organization and join-or-create pages no longer double-counts the welcome grant — a 5,000-credit voucher now renders as "5,000 credits" instead of being added on top of the standard signup grant. The number is also formatted using the browser's locale instead of being hardcoded to German formatting.
### May 22, 2026
**\[New]** Official Python SDK and `afx` CLI
The `anyformat-sdk` package wraps the workflow API with sync and async clients so you can author, create, run, and await a workflow end-to-end without writing HTTP code. Typed errors (`BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `RateLimited`, `ServerError`, `SDKTimeout`) make integration code easier to write correctly, and a `.raw` escape hatch on every response exposes the underlying JSON when you need it.
```python theme={null}
from pathlib import Path
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key="...")
workflow = (
client.workflow("Invoices")
.parse(mode="agentic")
.extract([
Schema.string("vendor", "The company that issued the invoice"),
Schema.string("total", "The total amount due, including currency"),
])
.create()
)
run = workflow.run(file=Path("invoice.pdf"))
result = run.wait(timeout=300)
print(result.fields["vendor"].value)
```
`AsyncClient` mirrors the sync surface for async codebases.
**\[New]** `afx` CLI for one-shot parse and extract
The SDK ships an `afx` CLI for quick command-line use. Run `afx parse --file invoice.pdf` to get markdown, or `afx extract --file invoice.pdf --field "vendor:vendor company" --field "total:total amount"` to extract structured fields. Pass `--schema fields.json` to load a richer schema, `--json` for machine-readable output, and `-o/--output FILE` to write results to disk.
```
export ANYFORMAT_API_KEY=sk_...
afx parse --file invoice.pdf
afx extract --file invoice.pdf --field "vendor:vendor company" --field "total:total amount"
afx extract --file invoice.pdf --schema fields.json --json -o results.json
```
Spinners, step breadcrumbs, and the Markdown/table renderers all detect TTYs and stay quiet when output is piped or `--json` is set, so the CLI is safe to script.
**\[New]** Workflow versions are now semver-numbered
Workflow versions now display as `v1.0`, `v1.1`, `v2.0` across Studio, version history, and the save toast. Saving a workflow only mints a new version when it actually changes — major bump on graph topology or node config changes, minor bump on extraction-schema changes, and no bump at all when you only move nodes on the canvas or reorder columns in the data viewer.
**\[API]** Save layout and reorder fields without minting a new workflow version
New workflow-centric write endpoints separate cosmetic edits from material ones. Layout drags and column reorders no longer create new versions or break stable links to a specific version of your workflow. The legacy v2 write endpoints continue to work but now respond with `Deprecation: true`.
* `PATCH /api/v3/workflows/{id}/config/` — save (may bump major or minor)
* `PATCH /api/v3/workflows/{id}/layout/?version_id=X` — canvas position only (never bumps)
* `PATCH /api/v3/workflows/{id}/field-order/?version_id=X` — column reorder only (never bumps)
Concurrent edits surface a `409` with `latest_version_id` in the response body so your client can rebase without losing local edits. The deprecated v2 endpoints (`PUT /api/v2/.../workflows/{id}/config/` and `PATCH /api/v2/.../workflows/{id}/reorder-fields/`) still function during the transition window.
**\[New]** `npx anyformat-skill` installs the anyformat Claude Code skill
If you use Claude Code, you can now install the anyformat skill with a single command: `npx anyformat-skill` drops it into `~/.claude/skills/anyformat`, or `npx anyformat-skill --project` installs it into the current project. The skill teaches Claude how to drive the typed-graph workflow API end to end — create a workflow with nodes and edges, upload a file, run extraction, and poll for results.
**\[Fixed]** Multi-tab login no longer fails with a code-verifier error
Logging in from two browser tabs at once could clobber the shared PKCE code verifier and surface as a generic "Login failed" page. The callback now silently retries once on this race and lands you on your original destination URL.
### May 21, 2026
**\[Improved]** Cleaner parsed markdown — no structural metadata wrappers
Parsed markdown saved to S3 and returned from the parse-result endpoints no longer carries `` framing or `` wrappers. Block boundaries are now expressed as inert `` anchors that render as nothing in any markdown viewer, and page boundaries are joined with blank lines. Bounding boxes, page numbers, and confidences remain available via the structured results endpoints — they just stop leaking into the human-readable file body.
**\[New]** Modelo 200 workflow template (Spanish corporate income tax)
A new "Modelo 200" template lands in the workflow gallery, oriented around credit underwriting and portfolio early-warning. It covers balance-sheet and profit-and-loss extraction with 45 root fields (liquidity, leverage, profitability, cash conversion, coverage ratios) plus two table fields for directors and principal shareholders. The template is pre-configured to use agentic parsing with Balanced effort.
**\[Fixed]** Nested table columns no longer dropped by the smart-table extractor
Columns of a list-of-objects field (for example, items inside a product table) could previously be missed because the smart-table planner tried to extract them as standalone scalars. Nested fields are now correctly treated as per-row columns of their parent table and reach the final extraction every time.
**\[Fixed]** Editing object-table cells no longer loses focus mid-typing
Validating a value in an object-field table sometimes stole keyboard focus from the input before you finished typing, occasionally swallowing characters. Cell focus is now preserved across both the validation round-trip and the background refetch, and screen-reader row labels stay in sync after row swaps.
**\[Fixed]** Parse → split workflows now return their splits
Workflows that parse and split a document without a downstream extract were silently dropping the resulting splits. Terminal-splitter workflows now persist and return their splits as expected, surfaced via the file-list endpoint with `?include=splits`.
### May 20, 2026
**\[API]** `POST /v2/workflows/` now takes a typed graph; the old fields-only body is gone
`POST /v2/workflows/` now accepts the typed `nodes` + `edges` graph that previously lived at `POST /v2/workflows/with_graph/`. The legacy fields-only body and the `with_graph` path have been removed — there is one canonical workflow-create endpoint going forward.
The `with_graph/` path no longer exists at `api.anyformat.ai/v2/workflows/`. Send the same payload (`{name, description?, nodes, edges}`) to `POST /v2/workflows/`. The legacy body `{name, fields}` is no longer accepted — wrap your fields in an explicit `parse → extract` graph instead. The legacy `multipart/form-data` upload path (creating a workflow from a downloaded `json_file`) has also been removed; rehydrate the workflow into the typed-graph body client-side.
**\[Changed]** `POST /v2/workflows/{id}/run/` response `status` is now `"pending"` instead of `"success"`
The public run endpoint accepts requests asynchronously (`202 Accepted` — the extraction is enqueued, not complete). The previous `"success"` value conflated acceptance with completion; the new `"pending"` matches the vocabulary used by `WorkflowRunListItem.status` elsewhere on the v2 surface. Clients that polled for results regardless of this field are unaffected. Clients that branched on `"success"` to skip polling should now poll on `"pending"`; semantics match what was always intended.
**\[New]** Per-branch Validation tabs across workflow and file detail pages
The Validate node now gets the same per-branch UX as Extract. The workflow page renders one Validation tab per branch alongside its existing Category and Extraction tabs, and the file detail page mirrors the same layout. Split workflows include a subdocument selector with chevrons and a `1/N` counter, so you can step through per-subdocument validation results without leaving the tab.
**\[Improved]** Clear "Not validated" placeholders for rules added after extraction
When you add or edit a validation rule, files that were processed before the change now render explicit "Not validated" cards and cells with a hover-card explanation, instead of an ambiguous "N/A". This makes it obvious which results reflect the current rule set and which would need a re-run.
**\[Improved]** Redesigned Validate node configuration menu
The Validate node menu now uses a tabbed Config / Rules layout aligned with the Classify menu, with a dedicated rule editor that supports inline field chips inside rule descriptions. The previous template-import button has been removed in favor of the new layout.
**\[Fixed]** Validation verdicts now populate for split workflows
In workflows with a Split node, validation verdicts could come back empty in the file list and detail view because the API only inspected the parent extraction. Validation results now traverse parent and child extractions so per-subdocument verdicts surface correctly across the workflow page Validation tab, the file detail page, and the file results UI.
**\[Fixed]** CSV and master-file uploads decode reliably from non-UTF-8 encodings
Uploaded CSV files and master files saved in cp1252 (Word/Outlook smart quotes, Spanish accents), Shift-JIS, GB18030, KOI8, and similar non-UTF-8 encodings could previously either silently produce mojibake or fail extraction with a Unicode decode error. Uploads are now transcoded to UTF-8 at the API boundary, with binary payloads disguised as text rejected up front, so downstream parsing and lookups receive consistent, correctly-decoded bytes.
The master-file upload endpoint and workflow CSV upload path now run inputs through a strict Western decoding ladder (`utf-8-sig` → `utf-8` → `cp1252`) before falling back to charset detection, and persist the re-encoded bytes to storage with a truthful `Content-Type: …; charset=utf-8` header. Allowed text extensions are `csv`, `txt`, `md`, `markdown`, and `rst`. Non-text payloads (PDF, ZIP, images, executables) are rejected at upload time rather than being mis-decoded.
### May 19, 2026
**\[API]** Fetch parsed blocks with bounding boxes via the new parse-result endpoint
The new parse-result endpoint returns the structured page-nested blocks produced by parsing — including bounding boxes, type, confidence, reading order, and layout — along with a presigned URL for the raw markdown. Studio's PDF viewer and JSON view now use these blocks to render overlays consistently across tabs.
`GET /api/v3/files/{file_id}/parse-result/` returns `{ blocks: [...], markdown_url: "..." }`. Blocks are grouped per page and carry a flat `bbox: { x0, y0, x1, y1 }`. The endpoint accepts an optional `execution_id` to scope results to a specific workflow execution.
**\[API]** Slim split results without nested rows
You can now request slim per-split results that omit nested object rows entirely, and then load nested rows lazily per split via a dedicated endpoint. Each slim split carries a per-split version token so caches can invalidate at the split granularity instead of the whole file.
Pass `?include=splits_lite` on the file results endpoint to receive slim per-split entries (mutually exclusive with `?include=splits`). Use `GET /workflow-versions/{version}/files/{file_id}/splits/{split_id}/object-results/{field_persistent_id}/` to fetch paginated object rows scoped to a single split.
**\[Improved]** Faster Studio category tab on workflows with many splits
The Studio category tab now requests slim payloads and fetches nested object rows lazily per split, so opening a file with many splits no longer waits on full results materialization up front. Bounded prefetching when hovering across many cells also keeps the page responsive.
**\[Improved]** Parse JSON view shows the full block on expand
Expanding a block in the Parse → JSON tab now reveals the complete block object — id, bbox, confidence, content, hyperlinks, rows, and `image_base64` — rendered as inert JSON. Pages are grouped under collapsible sticky headers (collapsed by default) for easier navigation in multi-page documents, and the displayed shape mirrors the public parse-result response exactly.
**\[Improved]** Client-side markdown hydration with embedded images
The Markdown tab now hydrates images directly in the browser using each block's bounding box — including `figure` blocks — so the in-tab view and Markdown downloads stay consistent. The dedicated server-side hydration pipeline has been retired in favor of this lighter approach.
**\[New]** "Beta" badge on Studio nodes still being stabilized
Studio nodes can now display a BETA badge in the sidebar, on the canvas header, and in the sidebar hover card. The **Validate** node is the first to use it, marking it as still being stabilized for general production use.
**\[Fixed]** Workflows created via the typed graph endpoint now run extraction
Workflows created via the typed `with_graph` create endpoint sometimes failed to run extraction and surfaced as `EXTRACTION_FAILED (422)` because the extract node was persisted without an engine. New workflows now always include an engine and execute end-to-end as expected.
`POST /v2/workflows/with_graph/` now persists a default engine on every extract node it creates.
**\[Fixed]** Object rows in sibling extract branches no longer collide
When a workflow had sibling extract branches that defined fields with overlapping names, object rows from one branch could be written against the wrong field. Field schema resolution is now scoped per extract node, so each branch's rows land where they belong.
**\[Fixed]** Parse results reliably appear in the UI for new workflow versions
Some parsed files showed missing pages or stale `parsed_at` because parse callbacks resolved graph nodes by an identifier that wasn't unique across workflow versions. Callbacks now scope to the active workflow version, so parse results are persisted to the correct file every time.
**\[Improved]** Other improvements and fixes
Deleted object rows are now consistently excluded from both per-split and file-level row counts; the file results UI deduplicates parse-result requests when navigating between files; and extraction progress estimates fall back to organization-scoped percentiles instead of cross-tenant numbers.
### May 18, 2026
**\[New]** Handwritten text is now a first-class block type in agentic parsing
Agentic PDF-to-Markdown now recognizes handwritten text as its own block type, with a dedicated extraction strategy that combines a vision-language transcription with OCR-derived hints. Handwritten blocks are labeled "Handwritten text" in the Studio UI, distinct from machine-printed text, and can be tuned independently from regular text extraction.
**\[Improved]** More consistent agentic parsing when text-class blocks have multiple sources
When a text-class block has multiple candidate text sources, the agentic parser now writes the chosen source back into the block's elements and regenerates its segments before rendering. Downstream grounding (overlays, citations) now matches the markdown that's actually rendered.
### May 14, 2026
**\[New]** "Beta" badge on agentic parsing mode
The Parse node mode card now shows a "Beta" badge next to **Agentic parsing**, making it clear that this parse mode is still being stabilized for general production use.
**\[New]** Calibrated confidence scores on smart-table extractions
Both table cells and scalar fields produced by the smart-table extractor now carry a calibrated 0–100 confidence, so review UIs and downstream consumers can prioritize low-confidence values. The scores degrade gracefully when scoring is unavailable, so extractions still complete normally.
Confidence is surfaced on each extracted value's `metadata.confidence` field as an integer in `[0, 100]`. Both judges flip together via the `smart_table_confidence_strategy` setting (`auto` / `judge` / `none`); the default is on.
**\[New]** Review threshold filter for file results
File results now include a confidence-threshold slider that filters Fields and JSON to show only extractions at or below the selected confidence — useful for prioritizing low-confidence rows that need human review. The threshold is persisted across reloads and tabs, and an inline hover card explains how the filter works.
**\[Improved]** Image previews use the PDF viewer toolbar
Image uploads (JPEG, PNG, GIF, BMP, TIFF) now render through the same viewer as PDFs and inherit its zoom, rotate, and full-page controls. Multi-page TIFFs gain a page selector, and the viewer now reports specific errors for unsupported formats or images that exceed browser size and canvas limits instead of failing silently.
**\[Improved]** More reliable dense-table parsing in agentic mode
Agentic PDF-to-Markdown table parsing no longer stalls on dense tables. Reasoning-token budgets are now properly honored across the heal, audit, and sub-region passes; partially valid heal plans are applied operation-by-operation instead of being discarded on a single bad op; and the table-parsing token cap has been raised so large tables are no longer truncated mid-document. Workflows that previously stalled for over 15 minutes now finish in roughly 3.
**\[Improved]** Organization logos stay cached when unrelated settings change
The organization logo URL is now invalidated only when the logo bytes themselves change, not whenever any field on the organization is updated. Renaming the org, toggling support access, or any other non-logo edit no longer forces every client to re-download the logo.
**\[Fixed]** Bulk-download of selected files no longer 400s
`GET /api/v2/saas_manager/workflow-versions/{id}/results/` rejected the `file__id__in` and `file__id__not_in` query parameters with `Field 'id' expected a number but got '...'` whenever the frontend passed UUIDs (the v2 file API now returns file `id` as a UUID string). The filter was still joining against the legacy integer PK; it now joins on the file UUID field, matching the regular file filter. Every bulk-download flow that relies on row selection — and the "select all minus N" pattern — is unblocked.
**\[Fixed]** Stricter validation of logo and workflow file uploads
Logo and workflow file uploads are now validated against their actual bytes via magic-byte sniffing rather than the client-supplied `Content-Type` header, and only file types the parse pipeline can actually process are accepted. Uploading a renamed executable, ZIP, or arbitrary JSON as a workflow file is now rejected at the API boundary instead of silently failing later in the pipeline. Stored objects also carry the correct `Content-Type` on the storage layer instead of being labeled `binary/octet-stream`.
**\[Fixed]** Switching organizations on a resource page no longer reverts
Switching the active organization from a workflow detail page (or any other resource page deep-linked from a different org) could occasionally snap back to the original org after showing a "switched" toast. The selection is now preserved on the first click.
**\[Fixed]** Object-field row counts exclude deleted rows
The paginated object-field results endpoint and the slim workflow-results list both now exclude soft-deleted rows from page contents and the row-count total. Counts shown in the UI and returned by the API now match the rows you can actually see.
**\[Fixed]** Sub-table detail rows no longer clip at the bottom
Expanded sub-tables inside object-field result tables previously cut off the bottom row because the row-height calculation didn't account for the surrounding padding and border. The detail row now renders at full height regardless of how many sub-rows it contains.
**\[Fixed]** Classifier category tabs render again on workflow results
Category tabs on classifier workflow results were rendering empty because the slim results response had stopped including the classifier's `graph_node_id`. The field is back in the response (with a batched prefetch to avoid per-file queries), so files routed by a classifier now appear under their correct tab.
**\[Fixed]** Workflow list updates immediately after create, duplicate, or delete
The home page workflow list previously kept the stale list cached after creating, duplicating, or deleting a workflow, so the change only appeared after a refresh. All three mutations now invalidate the workflow list cache and the list reflects the change right away.
### May 13, 2026
**\[Breaking + API]** Workflow creation no longer accepts file uploads
`POST /api/v2/saas_manager/organizations/{org_id}/workflows/` previously accepted a `multipart/form-data` body to import a workflow from a JSON or XLSX template file. That entry point and its template downloads have been removed; the endpoint now accepts JSON request bodies only.
**Action required:** if you were uploading a JSON or XLSX template to this endpoint, switch to sending the equivalent JSON body directly. The chat-driven and from-scratch flows in Studio are unaffected.
**\[New]** Studio is now generally available
The visual workflow editor (Studio) is now visible to every organization member from the workflow page and the new-workflow page. Write access is still controlled by your workflow role. The legacy schema editor at `/workflows/edit/...` has been removed — the Studio tab is now the single editing surface.
**\[New]** Row-level actions on object-field result tables
Object-field result tables now expose a per-row dropdown with **Validate row**, **Add row above**, **Add row below**, and **Remove row**, plus inline up/down chevrons for adjacent reordering. The trailing-column header has a new **Validate all rows** action that promotes every datapoint in the field to human-verified in a single click.
New endpoints under `/api/v2/datapoints/object-fields/{field_id}/`:
* `POST .../rows/` (with an `index` in `[0, n_live_rows]`) inserts an empty row and shifts live siblings up by one in the same transaction.
* `POST .../rows/{a}/swap/{b}/` exchanges two adjacent live rows.
* `POST .../rows/{position}/validate/` bulk-promotes every datapoint in a row to human-verified.
* `POST .../validate-all/` does the same for every live row of the object field.
All four endpoints require Member role.
**\[New]** Signup vouchers now give live feedback during onboarding
When a user lands on the signup flow with a voucher code, the create-organization screen shows the total welcome credits they will receive (the standard signup grant plus the voucher's value, fetched live from the backend) if the voucher is valid, or an amber "voucher no longer valid" hint if it is expired or unknown. Previously the page was silent in both cases.
`GET /api/v3/iam/signup-vouchers/{code}/` returns a read-only preview of the voucher (`{ code, additional_credits }`) and never consumes a redemption — actual redemption still happens atomically inside organization creation. Returns `404 voucher_not_found` for unknown codes and `410 voucher_expired` for expired or exhausted ones. Responses set `Cache-Control: no-store, private` so admin edits to a voucher are reflected on the next page load.
**\[Improved]** Workflow page list is dramatically faster
The workflow page's file list now uses a slim response that omits per-cell datapoint detail until you actually expand an object row. On a workload of 50 files with 2,000 line-items each, the list now returns in \~240 ms instead of \~6 s and the response payload is roughly 770× smaller. Object cells pre-warm on hover so expanding feels instant; edits to a datapoint refresh the expanded sub-table without a poll cycle.
`GET /api/v2/saas_manager/workflow-versions/{id}/files/?include=results_lite` returns scalar datapoints as `{ id, display, status }` and object fields as `{ row_count }`. Full sub-tables are fetched lazily through `GET /api/v2/saas_manager/workflow-versions/{id}/files/{file_id}/fields/{field_persistent_id}/object-results/`. The existing `include=results` (full) path is unchanged and still available for legacy integrators.
**\[Improved]** Stripe top-up is now open to all users and confirms the purchased amount
The **Top up** button on the Usage page now opens the Stripe checkout dialog for every user — the previous fallback to a `mailto:sales@anyformat.ai` link is gone. After a successful payment, the success page now states how many credits were added and tells the user they can start running workflows immediately.
**\[Improved]** Cleaner extraction on scanned PDFs
The agentic PDF-to-markdown parser now honors the routing model's verdict on which text source to trust (the embedded PDF text layer, OCR, or vision) for every block type, including miscellaneous blocks that previously ignored the verdict. On scanned PDFs where the embedded text layer is corrupted, blocks now render the clean OCR text instead of garbled bytes.
**\[Fixed]** Returning from Stripe checkout no longer fails authentication
If you stayed on Stripe long enough for the short-lived access token to expire, returning to anyformat used to land on an authorization error screen — even though the payment had already succeeded on Stripe's side. The app now silently refreshes the token in the background (and replays the first request that hits a 401), so the top-up success page renders cleanly even after a long checkout.
**\[Fixed]** Cross-tab refresh storm and support-elevation isolation
Opening the app in multiple tabs no longer triggers refetch storms when you navigate inside one tab. As a separate fix, support agents who elevate into a customer organization stay elevated only in the current tab — new tabs default back to their own organization until they explicitly re-elevate.
**\[Fixed]** New users are immediately enabled after signup
Users who created their own organization or requested to join an existing one were previously left in a disabled state until an admin reviewed their request, which blocked the create-org path entirely. Both onboarding paths now enable the profile immediately. Join-request approval is still required to gain membership in the target org.
**\[Improved]** Other improvements and fixes
* Structured-output extractions on Amazon Nova models no longer occasionally fail with an empty-JSON parsing error.
* The Usage page no longer renders the "Page consumption" summary box at the top — the cleaner credit-balance card now stands on its own.
* Long organization lists on the join-or-create page no longer get clipped below the fold.
### May 12, 2026
**\[Breaking + API]** v2 file responses now use UUID hex for file IDs
Every v2 endpoint that previously returned a numeric file ID now emits a 32-character UUID hex string. The OpenAPI schema already declared these fields as UUID strings, so generated SDK clients are unaffected — but raw HTTP integrations that parsed the value as an integer, or that constructed URLs from cached numeric IDs, will need to update. Old integer-shaped URLs return `404`.
**Action required:** treat v2 `file_id` values as opaque strings, and refresh any cached file URLs that embedded integer IDs.
* File detail, upload responses, and file-position (`prev_id` / `next_id`)
* Extraction responses and the verification URL builder
* File filters: `id__in`, `id__not_in`, `file_id__in`, `file_id__not_in` now resolve against UUID hex
**\[New]** Delete rows from object-field result tables
Object-field result tables now have a per-row trash action. Remaining rows are reindexed in place so curated positions stay stable and your verification carries over correctly when the file is re-extracted. Rows the model produced are soft-deleted (so accuracy metrics still reflect the model's miss); rows you added yourself are removed entirely.
`DELETE /api/v2/datapoints/object-fields/{id}/rows/{position}/`. Deletion is transactional. A partial unique constraint allows deleted rows to retain their original index, and the result-building SQL filters soft-deleted rows.
**\[API]** Workflow responses now include `organization_id`
Workflow and workflow-version responses (including the nested `versions` collection on workflow detail) now expose a normalized 32-character hex `organization_id`. Useful when a client needs to identify which org owns a workflow without an extra round-trip.
**\[Improved]** Tax ID and billing address now collected during Stripe Checkout
Top-up Checkout sessions now collect the customer's tax ID and billing address before invoices are finalized. This unblocks invoice correctness in EU/UK jurisdictions. No action required for existing customers — the values are persisted on the next Checkout.
**\[Improved]** Smoother markdown viewer on long documents
The "visual" markdown view is significantly snappier on long documents. Scroll-driven page detection is now frame-throttled and only fires on real page transitions, sections subscribe to fewer state changes, and highlight-scroll listeners share a single event bus instead of one per section.
**\[Improved]** Faster agentic PDF-to-markdown parsing
Agentic PDF→Markdown parsing now parallelizes work per page — single-call vision blocks fan out and agentic blocks run in a small per-page pool, so page latency tracks the slowest block instead of the serial sum. Layout-routing calls were also tuned for lower latency.
**\[Fixed]** Org switcher no longer goes blank for orgs without a logo
The organization switcher trigger now always renders the org's initials as the base layer and only overlays the logo image once it loads successfully — so switching to an org without a logo no longer flashes blank.
**\[Improved]** Parse confidence is now reported even when the model omits logprobs
Per-block parse confidence is now computed and stamped on the rendered markdown regardless of whether the parsing model returns token-level log probabilities. Downstream consumers can read `data-parse-confidence` consistently across all supported parsing providers.
### May 11, 2026
**\[Breaking + API]** v2 SaaS Manager list, create, and member endpoints now require an organization in the URL
The bare `/api/v2/saas_manager/...` list and create paths, and the legacy `current` organization shortcuts, have been removed. Workflow versions, file collections, member management, and suggestion routes must now include the org id in the URL. Suggestion endpoints additionally require Member role — Viewers get `403`.
**Action required:** template the org id into the URL for every v2 SaaS Manager request that previously relied on a bare list/create path or the `current` shortcut.
* `GET / POST /api/v2/saas_manager/workflow-versions/` → `.../organizations/{org_id}/workflow-versions/`
* `GET / POST /api/v2/saas_manager/file-collections/` → `.../organizations/{org_id}/file-collections/`
* `PATCH /api/v2/saas_manager/organizations/current/` → `.../organizations/{id}/`
* `PATCH /api/v2/saas_manager/organizations/current/logo/` → `.../organizations/{id}/logo/`
* `GET / POST /api/v2/saas_manager/organizations/current/members/` → `.../organizations/{org_id}/members/`
* `DELETE / PATCH /api/v2/saas_manager/organizations/current/members/{user_id}/[role/]` → `.../organizations/{org_id}/members/{user_id}/[role/]`
* `POST /api/v2/suggestions/{fields,field-description,workflow-description,workflow-name,upload-sample}/` → `.../organizations/{org_id}/suggestions/{...}/`
**\[New]** Self-service invoice downloads from the usage page
Owners and admins can now open Stripe's hosted Customer Portal from the Usage page to download invoices for past top-ups. Members, reviewers, and viewers don't see the button — invoices are financial documents.
`POST /api/v3/billing/organizations/{org_id}/portal-sessions/` mints a short-lived portal URL bound to the org's billing customer record. Orgs that have never run a top-up return `409 stripe_customer_not_provisioned`; the UI surfaces this as a "Top up first" toast. New environment variable `STRIPE_BILLING_PORTAL_RETURN_URL` falls through to `${FRONTEND_URL}/usage`. Configure the operator side (Invoice History toggle, ToS / privacy URLs, default return link) in the Stripe dashboard.
**\[Improved]** Cross-tenant probes on v3 detail endpoints now return 404
v3 file results, file-collection extraction status, file-collection results, and webhook delete now derive the active organization from the resource itself. URLs are unchanged. Cross-organization requests get a `404` to mask resource existence; same-org callers are unaffected.
**\[Improved]** Hovering a stacked bounding box highlights every overlapping box
When several bounding boxes overlap, hovering reveals each one stacked under the cursor instead of just the topmost. Rapid mouse movement is collapsed to one diff per frame so the UI stays responsive.
**\[Improved]** Consistent extracted-object UI across Define, Refine, and File Results
Object and table fields now render the same way across the schema builder, refine step, and file-results panel — same headers, same skeletons, same status states. Refine correctly shows error / cancelled copy on failed extractions instead of an empty data view.
### May 9, 2026
**\[Breaking + API]** Detail and per-resource v2 endpoints no longer read `X-Current-Org`
Datapoints, manual datapoints, extraction results, workflow-version files, file collections, workflow versions, and workflow detail endpoints now derive the active organization from the resource itself. Non-resource list/create endpoints (API key CRUD, manual-datapoint create, workflow list/create) moved under `/api/v2/saas_manager/organizations///`.
**Action required:** stop sending `X-Current-Org` to v2 endpoints — derive the org from the resource (URLs unchanged for detail) or from the new org-scoped URL for list/create. Non-members on a same-resource probe now get `404`; in-org callers with insufficient role get `403`.
### May 8, 2026
**\[API]** Create workflows with full graph definition in a single call
You can now define a workflow's parse, classify, split, and extract steps in one request — the workflow, its initial version, and all extraction fields are created atomically. The existing workflow-create endpoint is also fully typed, so SDKs get auto-complete and field-level validation.
`POST /v2/workflows/with_graph/` accepts the complete graph plus per-extract-node fields and creates everything in a single transaction. The original `POST /v2/workflows/` body is now declared with discriminated field types.
**\[New]** Python builder for typed workflows
The Python SDK now ships a fluent `Workflow` builder: `.parse().classify(...).extract(...).split(...).build()` produces a fully validated request. `branch=` and `route_from=` accept either an id string or the typed category/rule you registered earlier, so typos fail at the call site instead of at build time.
**\[Improved]** Workflow graph validation catches more errors before run
Non-branching nodes with multiple outgoing edges, missing `route_from` on classify→split chains, and broken graph transitions are now rejected with clear validation errors instead of producing confusing runtime failures. The new typed graph payload also rolls back cleanly when persistence of one node fails partway.
**\[New]** Voucher codes on signup grant extra wallet credit
Organization creation now accepts an optional `voucher_code` that grants extra wallet credit to the new org. Vouchers (expiry, usage cap, amount) are managed in the admin panel; invalid or expired codes surface as a clear `400` instead of breaking signup.
**\[Improved]** Doc-viewer table polish — resize, unified sync toggle, safer persisted state
The object-results table is now vertically resizable with sensible bounds (header + one row min, all-rows-visible max). The PDF toolbar's separate auto-focus and auto-scroll preferences collapse into a single "Sync document and results" toggle, and the lookup-files dialog no longer overflows. Co-mounted views in the same tab stay in sync, and corrupt persisted preferences self-recover instead of breaking the UI on load.
### May 7, 2026
**\[Improved]** Org isolation on v2 detail endpoints is now resource-derived
Detail endpoints (datapoints, manual datapoints, extractions, workflow-version files, file collections, workflow versions, workflows) now authorize the caller's role against the resource's organization instead of a request header. Cross-tenant probes return `404`; in-tenant callers with insufficient role return `403`. URLs are unchanged.
**\[Improved]** More accurate extraction confidence via LLM judge
For extractions where the model doesn't return logprobs (or where you prefer the judge), a separate judge model now scores each extracted value against its source markdown and outputs a calibrated 0–100 confidence. Object and table cells are batched for cost efficiency. Choose the strategy per task with `extraction_confidence_strategy = none | auto | judge`.
**\[Fixed]** Login no longer spins on token-exchange failures
The post-login callback now exits cleanly when token exchange or profile fetch fails: a 15-second watchdog renders an error page with a logout option, OAuth errors surface explicitly, and tokens are purged on error.
**\[New]** Bounding boxes for split + extract workflows
The PDF viewer now renders bounding boxes for split + extract workflows by scoping mapped locations per panel — the matching split when navigating by partition, the file's extraction otherwise. Parse, split, classify, and validation panels paint no boxes, as intended.
**\[Fixed]** Renamed split categories no longer lose historical sub-documents
When you rename a split rule across versions (e.g. "Invoice" → "Bill"), historical sub-documents continue to surface under the renamed tab — the UI now filters by the stable split-node identifier instead of the display string.
### May 6, 2026
**\[New]** Top up credits with Stripe Checkout
You can now purchase wallet credit through Stripe Checkout from the Usage page. Sessions are idempotent (no duplicate charges on retry), and every successful top-up generates an invoice you can download later from the Customer Portal.
`POST /api/v3/billing/organizations/{id}/checkout-sessions/` validates the requested credit count against per-session min/max bounds and returns a hosted-page URL. Inbound `checkout.session.completed` webhooks are HMAC-verified and serialized to guarantee exactly-once credit grant under contention.
**\[New]** File results panel with explicit processing states
The per-file extraction panel now renders explicit Unprocessed, Processing, Error, and Processed states with run and retry actions. Heavy result queries only run once the file is actually processed, removing wasted requests and inconsistent loaders for unprocessed files.
**\[New]** Search inside the PDF viewer
The PDF viewer now has a toolbar search bar with case-sensitive/insensitive matching, prev/next navigation that wraps, a `Cmd/Ctrl-F` shortcut, and cross-page highlighting. Search stays responsive on large PDFs thanks to debounced lazy text extraction.
**\[API]** Workflow results now expose classifications, splits, and flat extractions
`GET /v2/workflows/{wid}/files/{cid}/results/` now returns `classifications`, detailed `splits` (with per-file pages and partitions), and a flat `extractions` list keyed by `(split_name, partition)` alongside the existing fields. The v3 file-collection results payload exposes the same keys in place — no new endpoint required.
**\[Improved]** Org switcher orders by ownership, membership, then support access
Organizations are now returned in three alphabetically-sorted buckets — owned, member, support access — replacing the prior database-discovery order. Helpful for accounts that belong to many orgs.
**\[Fixed]** Bulk extraction status no longer marks processed files as pending
Bulk status resolution now aligns identifier formats and queries root extractions only, so processed parents are correctly reported instead of stuck on "pending".
**\[Improved]** Other improvements and fixes
File status badge in the file-results header, PDF toolbar staying visible on multi-line wrap, clearer toasts when a dropped file's type isn't accepted, and a fix for crashes on empty drag-and-drop selections.
### May 5, 2026
**\[Improved]** Smart Lookup retries based on residuals
Smart Lookup now wraps the worker in an orchestrator + auditor loop that can iterate up to three times if residuals or tool-call signals suggest more work is possible. Search blends BM25 and Jaro-Winkler matching with a per-call query cap to improve recall while keeping runtime bounded.
**\[Improved]** Splits panel is now navigable
Clicking a split or partition now switches the extraction tab, persists the selected partition in a URL search param, and scrolls the PDF viewer to the matching page. Refresh-safe deep links to a specific partition just work.
**\[Fixed]** Duplicating workflows with reused field names no longer fails
Workflows that contain multiple extract nodes reusing the same top-level or nested field names can now be duplicated cleanly — field lookups are keyed per source node.
**\[Fixed]** Classify-only workflows handle unwired LLM categories
When the classifier returns a category with no downstream edge, the run now falls through to the end instead of crashing. Classify-only workflows also persist classification records so results are available afterwards.
**\[Improved]** Smart-table list-of-object rows are page-ordered
Smart-table list-of-object extraction rows are now sorted by source page and block, so output is stable and easy to align with the source document across runs. Rows with missing page info are placed last.
**\[Improved]** Cleaner empty states for splits and category grids
Workflows with no rows now render a clean overlay in the splits and category grids instead of stale placeholders.
### May 4, 2026
**\[Breaking]** Audio uploads are no longer accepted
The dropzone no longer accepts audio files (`.mp3`, `.wav`, etc.). Existing audio files already in your collections remain viewable and downloadable.
**Action required:** if your integration uploaded audio for transcription, switch to a supported format. No deprecation window — audio uploads are rejected starting today.
**\[Fixed]** Authentication errors now return the correct status codes
Unauthenticated requests now return `401` (with a `WWW-Authenticate` header) instead of being coerced to `403`. Missing email-claim or unknown-key authentication paths return `AuthenticationFailed` cleanly instead of `500` errors.
**\[Improved]** Profile-fetch failures show a clear error page
Repeated `/profile` failures after login now surface an error page with a logout option instead of stranding users on a spinner. Global query errors are also reported with the failing endpoint for faster triage.
**\[Fixed]** Corrupted local preferences no longer break the UI on load
Persisted UI preferences (sync toggles, panel sizes, etc.) are now safely parsed; corrupt keys are dropped on initial load and reported to monitoring with the offending storage key.
## April 2026
### April 30, 2026
**\[New]** New organizations get 5,000 free credits
Every new organization is now automatically granted 5,000 credits on creation, recorded as a standard billing transaction.
### April 28, 2026
**\[Breaking + API]** Split confidence is now a 0–100 integer
`SplitRange.confidence` is now an integer in `[0, 100]` end-to-end (graph output, backend validation, database, and frontend rendering) instead of a 0–1 float. Existing values were migrated automatically.
**Action required:** if your integration reads `confidence` from split results, expect integers (`87`) instead of floats (`0.87`). Confidence badges no longer render `0.87%` or `8700%`.
**\[Breaking + API]** Workflow results response shape is now canonical
`GET /v2/workflows/{wid}/files/{cid}/results/` now always returns `{collection_id, verification_url, parse, extraction}` — parse-only workflows return `extraction: null` instead of omitting the key. The internal `field_id` no longer leaks in responses.
**Action required:** if your integration relied on `extraction` being absent for parse-only workflows, check for `null` instead. Cookbook examples and the API reference have been rewritten to match.
**\[New]** Classify nodes return evidence and confidence
Classify nodes now return a structured result with category, evidence, and confidence. The UI surfaces confidence and evidence in the category data viewer, with a dedicated Classify tab at workflow level and a per-file Classify tab with verdict cards. The new `classifications` include option exposes the same data via the file API.
**\[New]** HTML files render inside the document viewer
You can now upload `.html` and `.htm` files and view them in the document viewer. Files render inside a sandboxed, sanitized iframe to block script execution and same-origin access.
**\[API]** Better OpenAPI for SDKs and try-it surfaces
`POST /v2/workflows/` now declares its JSON request body, so SDKs and AI agents stop falling back to raw `extra_body` dicts. Bearer authentication is now declared as a proper API-key security scheme, so try-it panels render the lock icon and treat auth as required. Webhook responses now expose `url` as an `HttpUrl` and `created_at` as a `datetime`.
**\[Improved]** Smart-table search now reaches inside table cells
Smart-table search now scans both page prose and table cells, returning cell matches with `{table, row, column, value}` provenance. Scalar values inside tables are no longer dropped behind prose hits.
**\[Improved]** Per-category accuracy and confidence charts in monitoring
The monitoring tab now groups per-field accuracy and confidence charts into per-category sections for multi-schema workflows, with sensible fallbacks and an improved empty state.
**\[Improved]** Validate view now shows results for classify-routed files
Per-file validation now displays extracted data for files routed via Classify by synthesizing entries from the file's results when pinned to an extract node. The partition selector and sibling tabs are hidden when they don't apply.
**\[Fixed]** Improved processing of `.docx` and `.msg` uploads
`.docx`, `.msg`, and other non-PDF uploads now hydrate from their converted PDF instead of feeding the original binary into the PDF parser. Markdown for these formats is now correct and no longer triggers spurious "corrupt PDF" warnings.
**\[Improved]** Other improvements and fixes
Reliable PDF callback uploads via short-lived presigned URLs (no more "Request Too Big" errors on large rendered PDFs), file-detail columns in splits and per-category tables, `search_text` helper exposed in the smart-table sandbox, object/list extraction fields no longer dropped from v3 results, timeouts and retries on frontend list requests, lighter `/files` list payloads, and `eval` and XFA disabled in the PDF viewer for a smaller attack surface.
### April 27, 2026
**\[Fixed]** Smart-table extraction no longer drops scalar fields
Smart-table extraction plans now must assign every schema field to either a table or a `scalar_fields` list, rejecting incomplete plans so scalar fields can no longer be silently dropped.
**\[Fixed]** Master-file + extract workflows now run without error
Non-split workflows that combine a master-file node with an extract node now run cleanly — master-file nodes can augment an extract branch without triggering single-non-split-node validation.
### April 24, 2026
**\[Improved]** Tabbed data viewer on workflow detail
Reworked the workflow detail data viewer into a tabbed layout with shared grid skeletons and improved loading behavior. Added an inline workflow-name editor and tighter studio reload/loader behavior when workflows change.
**\[Fixed]** Empty splits tab now shows a clean overlay
The splits tab now uses a proper no-rows overlay (resolving the previous "Element type is invalid" error) and filters out splits with no results, so empty entries no longer appear.
**\[Improved]** More content captured during PDF parsing
Text missed by layout segmentation now reaches downstream parsing through orphan OCR detection and synthetic text-block injection. Orphans are clustered into lines, merged into nearby blocks where appropriate, or promoted to new blocks; redundant smaller blocks are absorbed to prevent duplicates.
**\[Fixed]** Date parsing in the smart-table sandbox no longer silently returns `None`
The sandbox now allows `datetime.strptime`'s runtime dependencies, so parsed dates are correctly returned instead of being silently converted to `None`.
**\[Fixed]** Smart-table no longer overwrites populated fields with empty values
Storing an empty list can no longer overwrite an already-populated list field (e.g. a transactions table). Column-statistics summaries now clearly label examples and indicate additional unique values so the worker doesn't mistake samples for full datasets.
**\[Improved]** Better table merging when headers differ
Same-title tables with differing headers are disambiguated to avoid dropping rows during merge, and the block-parsing prompt and markdown assembly capture text the model sees that isn't covered by any block id.
**\[Fixed]** Classifier results survive workflow amendments
Category queries continue to return populated classifications and datapoints after prompt adjustments — the version-compatible result builder now maps datapoints across amended versions by their stable persistent identifier.
### April 23, 2026
**\[Fixed]** Partitioned splits now flow into Validate and Smart Lookup
When a split rule declares a `partition_key` and the downstream extract is wired into a Validate or Smart Lookup, each partition is now processed independently — one validation pass per partition, one lookup pass per partition — and the results attach to the right per-partition child extraction. Previously the downstream node saw nothing.
**\[Improved]** Per-partition fan-out is now bounded
Per-partition fan-out is now capped (default `4` parallel branches) so a 100-partition document running through extract → validate → lookup doesn't saturate the model provider.
### April 20, 2026
**\[Breaking]** Smart Lookup is now part of Extract
The standalone Smart Lookup node has been removed — lookup behavior now lives on the Extract node. You manage lookup files directly from the Extract menu, enable lookups per-field via a checkbox in the field editor, and see inline badges when lookup files are missing.
**Action required:** none for most users — existing workflows migrate automatically and inherit Extract's model settings. If you scripted Smart Lookup as a standalone node, switch to enabling lookup fields on Extract.
**\[Breaking]** Smart Table is now an agentic mode on Extract
The standalone Smart Table node has been removed — replaced by a Standard / Agentic mode selector on the Extract node.
**Action required:** none for most users — existing Smart Table workflows migrate automatically to Extract with `mode="agentic"`.
**\[New]** Splits results tab in workflow results
Added a Split tab in the extraction results UI showing split-group cards and a collapsible per-page table, plus a `document_split_groups` include option on the workflow-version file endpoint with category, partition value, page range, and confidence.
**\[Improved]** Redesigned Classify and Split menus
Both nodes now share a Config / Options tab layout with validated card-based editors, uniqueness checks, reserved "Other" handling for Split, click-to-edit from the Config preview tables, and cleaner Save-from-header behavior that closes open forms first.
**\[API]** Split groups consolidated by category and partition value
`document_split_groups` responses now group rows by `(category, partition_value)` and return consolidated `page_ranges` arrays instead of per-row `page_start` / `page_end` fields.
**\[Improved]** Better figure parsing in PDFs
The figure-enhancement prompt now uses chart-type-specific strategies, handles multi-panel figures, gives statistical-annotation guidance, and applies column-header rules. Segment deduplication now preserves the largest picture blocks to avoid losing visual context.
**\[Improved]** Inline field-level errors in the field editor
Replaced the top-level field-form error banner with inline, field-adjacent errors, column-level highlighting, and deduplicated summary messages for enum options and nested fields.
**\[Improved]** Default extraction model upgraded to `gpt-5.2`
The default extraction model is now `gpt-5.2` across markdown and PDF extraction paths.
**\[Fixed]** OneDrive and Google Drive imports work reliably in production
Fixed the cloud-connector reliability issues that affected OneDrive and Google imports in staging and production: a stricter content-security policy now allows the required SDK scripts and frames, SDK load errors surface instead of failing silently, consumer-vs-business OneDrive accounts are detected correctly, and a popup-polling leak is gone.
**\[Improved]** Other improvements and fixes
Validation tab only appears when the workflow includes a Validate node and the file has an extraction; `latest_version` endpoint returns a clean `400` for invalid workflow UUIDs instead of a noisy `500`; OAuth callback token exchange no longer stalls; Smart Lookup handles nested object fields correctly; agentic-extraction description typo fixed; partition tools only exposed when split rules define a partition key; null figure-parsing responses no longer crash; markdown viewer no longer has an infinite scroll loop and the markdown download button moved to the top of the panel.
### April 14, 2026
**\[New]** Redesigned Workflow Studio
The Studio now ships redesigned Parse and Extract node menus, smart node placement with auto-connect, a way to add connected nodes from an output handle, empty-state guidance, an updated sidebar with hover cards, and save-from-header with per-node validation errors.
**\[New]** Validate node for cross-document rule validation
A new Validate node type lets you express cross-document rules that run after extraction.
**\[New]** XML file support
Added an XML-to-PDF converter for files with embedded base64 attachments, so XML files extract end-to-end.
**\[Improved]** API keys are hashed at rest, with multiple named keys per user
API keys are now hashed at rest, support naming, and you can hold multiple keys per user — better security and easier rotation.
**\[Improved]** Per-cell parse confidence with logprob breakdowns
Parse confidence now uses cell-level logprobs when available, with per-cell and per-row logprob breakdowns for table blocks.
**\[Improved]** Studio warns when leaving with unsaved changes
The Studio now blocks tab switches when configuration is dirty or an inline editor is open, and preserves in-progress work across tab navigation.
**\[Improved]** Faster, paginated billing usage
The billing usage table is now server-side paginated with summary cards, dramatically improving query performance on large datasets.
**\[Improved]** Visual grounding toggle for local-model compatibility
Added a toggle to disable visual grounding for environments where the local model doesn't support it.
**\[API]** Files nested under workflows; multi-node results; richer OpenAPI
File endpoints are now nested under workflows. A new multi-node results endpoint returns results across multiple graph nodes in one call. The OpenAPI spec is now enriched for better SDK documentation, and the billing API gained pagination, filtering, and ordering.
**\[Improved]** Other improvements and fixes
OCR confidence is now included in segment and table-cell metadata; the schema builder uses a local config store for faster field editing; toast colors, mobile sidebar, long descriptions in choice fields, lookup-file display overflow, and schema editor header sizing fixes; corrupt-PDF hydration errors are now downgraded to warnings.
### April 6, 2026
**\[New]** Agentic document splitter
The batch LLM splitter has been replaced by an agentic workflow. You can now define custom split rules (name, description, optional partition key for grouping repeating sections) directly in the Studio SplitPages node, with a built-in "Other" catch-all rule.
**\[Fixed]** Non-figure blocks no longer receive figure wrapping
Generic non-figure blocks are no longer treated as pictures, so only true figures receive screenshot crops and figure markup in PDF-to-markdown output.
**\[API]** Removed duplicate health check endpoint
A duplicate health check endpoint has been removed from the API surface.
### April 1, 2026
**\[Breaking + API]** Workflow results endpoint always returns unified JSON
`GET /v2/workflows/{id}/results/` now always returns unified JSON (parse markdown plus extraction data per file). The `output_format`, `as_lists`, and file-filter query parameters have been removed; filter to a single file with the new `file_id` query parameter.
**Action required:** if your integration relied on `output_format`, `as_lists`, or the legacy file-filter param, remove them and use `file_id` instead.
**\[Improved]** Workflow usage endpoint supports pagination and ordering
The workflow usage endpoint now supports server-side pagination (`page`, `page_size`), ordering, and database-level filtering with the standard `results` response wrapper.
**\[API]** v1 routes restored with deprecation headers
v1 routes (workflows, jobs, uploads/results) are back with `Deprecation` and `Sunset` response headers and `X-API-Key` auth fallback for backward compatibility.
## March 2026
### March 30, 2026
**\[Improved]** Rate limits split between submission and general endpoints
Rate limits are now applied independently to file-submission endpoints (60 RPM) and general endpoints (600 RPM). Submission operations (run, upload, create file collection) have a stricter limit; read and management endpoints share a higher limit. The two tiers use independent counters.
### March 26, 2026
**\[Breaking + API]** Removed deprecated `as_lists` parameter from the v2 extraction endpoint
The `as_lists` query parameter on the v2 extraction endpoint has been removed.
**Action required:** drop `as_lists` from any v2 extraction call. The response shape is the unified default introduced earlier this month.
**\[New]** Paste images into the workflow creation chat
Paste images directly into the workflow creation chat for more contextual AI suggestions (up to 3 images, 3 MB each).
**\[Improved]** Concurrent edit protection on workflow saves
Workflow configuration saves now detect concurrent edits and surface them, preventing accidental data loss when two users edit at once.
**\[Fixed]** Image uploads in workflow suggestions accept files within size limit
Image uploads in workflow suggestions are no longer rejected when they're within the allowed size limit.
**\[Improved]** Other improvements and fixes
Workflow-name loading indicator no longer gets stuck; buttons now show the pointer cursor on hover.
### March 25, 2026
**\[Breaking + API]** v2 extraction is now collection-first
Run extraction now returns a file-collection UUID; poll results via `GET /v2/files/{id}/extraction/` (`412` while pending, `200` when ready).
**Action required:** if your integration parsed extraction results synchronously from the run-extraction response, switch to polling the new collection-status endpoint.
**\[Breaking + API]** Upload endpoint no longer accepts `file_collection_id`
`POST /v2/files/` no longer accepts a `file_collection_id` parameter — collections are immutable after creation.
**Action required:** create a new collection per upload batch instead of appending to an existing one.
**\[Breaking + API]** Deprecated `/jobs/` endpoints removed from the v2 API
The deprecated `/jobs/` endpoints are gone.
**Action required:** use `GET /v2/files/{id}/extraction/` to poll extraction state instead.
**\[Breaking + API]** Simpler v2 pagination shape
v2 pagination responses now contain `count`, `page`, `page_size`. The old `total_pages`, `next`, and `previous` fields are gone.
**Action required:** compute next/previous page numbers from `count`, `page`, and `page_size` client-side, or rely on `page` boundaries (`1` and `ceil(count / page_size)`).
**\[Improved]** Upload multiple files in a single request
`POST /v2/files/` now accepts multiple files in a single request.
**\[API]** Customer-facing docs now show Bearer authentication
Customer-facing API docs now reflect the `Authorization: Bearer ` authentication method.
**\[API]** v2 documentation reflects path-based versioning
All API documentation has been updated for v2 endpoints with path-based versioning (`/v2/` prefix).
### March 19, 2026
**\[New]** EML and MSG email file support
You can now upload `.eml` and `.msg` (Outlook) email files for extraction. Embedded images in EML files are correctly inlined during conversion.
**\[API]** New `allow_extend` parameter to append rows on submit
Submit now supports an `allow_extend` parameter that lets agents append rows to existing submissions instead of replacing them.
**\[API]** v3 types and graceful v2-to-v3 delegation
Updated v3 types, v2 file endpoints now delegate to v3, and v3 errors are translated cleanly for v2 callers.
**\[Improved]** Better OCR text detection
OCR text detection now uses word-count and spatial-coverage heuristics instead of simple truthy checks, and the screenshot is always sent to the model for context.
**\[Improved]** Smart-table resubmit replaces rows instead of skipping
Smart-table resubmit now replaces the previous output instead of skipping, with scoped worker context and a consolidated briefing.
**\[Improved]** Per-user rate limits via API keys
Authentication and rate-limiting are now separated, with per-user rate limits derived from API key identity.
**\[Improved]** HTTP security headers added per pentest findings
Added HTTP security headers across the API per recent pentest recommendations.
**\[Fixed]** Bounding boxes align on pages with different dimensions
Bounding boxes no longer drift on documents that mix page sizes.
**\[Improved]** Other improvements and fixes
Table grounding improvements (HTML cell-id injection now gated and tuned), better login-page rendering with static assets loading correctly, cleaner full-page loader on the auth callback, and PDF viewer fixes for documents that mix page sizes (e.g. US Letter and A4).
### March 18, 2026
**\[New]** v2 API with versioned endpoints, pagination wrappers, and improved docs
A full v2 API surface ships: versioned endpoints, pagination wrappers, workflow routes, list-extractions, provenance API, and richer OpenAPI documentation.
**\[API]** Bearer authentication alongside API key
The API now supports the standard `Authorization: Bearer` header alongside the existing API key header.
**\[API]** API rate limiting
The API now enforces per-endpoint rate limits.
**\[Improved]** Smart-table extraction planning with table-scoped delegates
The smart-table extraction planner now uses table-scoped delegates, agent limits that scale with document complexity, and improved sandbox tools.
**\[Improved]** Better row-level grounding for tables
Row-level grounding for tables is now more robust via token-overlap pre-mapping and accuracy fixes to anchor finders.
**\[Improved]** Faster page rotation
A cascade decision engine with landscape support and optimized center-crop reuse makes page-rotation detection faster.
**\[Improved]** File metadata toggle and filtered counts
A new metadata visibility toggle on the table header persists locally. File counts now reflect filtered results instead of selected rows, and action buttons show loading state.
**\[Fixed]** File-collection creation works in production
A regression that returned `403` for all file-collection creation calls in production has been fixed.
**\[Fixed]** File download naming
Markdown downloads no longer end up with doubled file extensions.
**\[Improved]** Other improvements and fixes
Security update for vulnerable dependencies; a botched migration that assigned the same UUID to all files has been corrected.
### March 5, 2026
**\[New]** Raw JSON viewer alongside validated results
Raw JSON extraction results are now available in a dedicated tab alongside validated results, for easier inspection.
**\[Improved]** Better document layout analysis
Enhanced layout analysis for more accurate table and section detection.
**\[Improved]** Hover cards on simple field results
Simple field results now display hover cards with additional context.
**\[Improved]** Resizable workflow creation chat
The workflow creation chat panel is now resizable.
**\[Improved]** Refined validation result cards
Validation result cards have refined styling that makes validated datapoints easier to identify at a glance.
**\[Fixed]** Confidence handling for undefined values
Confidence scores no longer break when a value is undefined.
**\[Fixed]** Clipboard copy errors now show clear feedback
Clipboard copy errors are handled and surfaced with clear feedback instead of failing silently.
**\[Improved]** Other improvements and fixes
UI styling consistency improvements across result cards and the file viewer.
### March 4, 2026
**\[New]** Production-grade webhook system
You can now create webhook subscriptions, receive real-time event notifications, and configure automatic retries on failure.
**\[Improved]** Better error categorization
Error messages now use clearer categories, making it easier to understand and resolve extraction issues.
**\[Improved]** Schema editor auto-saves
Fields and schema changes now auto-save when switching tabs or editing nested fields, preventing accidental data loss.
**\[Improved]** Validation page with keyboard navigation
The validation page now has improved result cards with keyboard navigation and better loading states.
**\[Improved]** Faster OCR processing
OCR text recognition is now significantly faster on large documents thanks to GPU-accelerated batch processing.
**\[Fixed]** Race conditions in field-form submission
Race conditions in field-form submission and save-state management have been eliminated.
**\[Fixed]** Security vulnerability in a third-party dependency
Fixed a security vulnerability in a third-party dependency.
## February 2026
### February 27, 2026
**\[Improved]** Custom date range filter on the usage page
The usage page now lets you filter by a custom date range instead of a fixed year, giving more precise control over billing and usage reports.
**\[Fixed]** Reverting to the original value marks fields as verified
Editing a field value back to its originally extracted value now correctly marks it as human-verified rather than human-edited.
### February 25, 2026
**\[Improved]** Better OCR accuracy
Upgraded text recognition with better support for Latin languages.
**\[Improved]** LLM cost tracking in billing
Billing transactions now include per-extraction LLM token counts and costs for full cost transparency.
**\[Improved]** More accurate table extraction
Improved accuracy when extracting data from tables with closely spaced rows or surrounding text.
**\[Improved]** Faster processing during peak usage
Extraction now scales to handle high volumes, reducing wait times during busy periods.
**\[New]** Sample workflows on new accounts
New accounts are now prepopulated with sample workflows and documents for a guided onboarding experience.
**\[Fixed]** Smart-lookup crash on empty corpus
Smart lookup no longer crashes when the search corpus is empty.
**\[Improved]** Faster extraction callbacks and field-list endpoints
Performance improvements on extraction callbacks and field-list endpoints.
**\[Fixed]** Readable file download filenames
File download filenames are now readable by the frontend.
### February 21, 2026
**\[Improved]** Visual grounding 2.0
Completely revamped evidence highlighting with block-level and element-level precision, making it easier to trace extracted values back to their exact location in the source document.
**\[New]** Billing dashboard
New usage dashboard with remaining credits, weekly summaries, per-workflow usage breakdown, and CSV export.
**\[Improved]** Advanced document layout analysis enabled in production
The advanced document segmentation pipeline is now enabled in production for improved table and section detection.
**\[Fixed]** Bounding-box highlights no longer stuck after navigating fields
Highlights are now cleared correctly when you move between fields.
**\[Fixed]** Confidence calibration
Fixed calibration configuration for confidence scoring.
### February 14, 2026
**\[New]** Redesigned home page
A personalized home with recent workflows, at-a-glance file-status indicators, and a streamlined workflow creation experience with templates.
**\[New]** Redesigned schema builder
A new two-step Define & Refine flow with AI-powered field suggestions, inline file previews, and drag-and-drop uploads.
**\[Improved]** Faster page rotation
About 2× faster page-orientation detection per page.
**\[Improved]** More reliable AI field suggestions
Field suggestions now use an upgraded model for higher reliability.
**\[Fixed]** Smart-lookup settings saved on publish
Smart-lookup settings are now correctly saved when publishing a workflow version.
**\[Fixed]** Workflow studio sidebar overflow
Fixed sidebar overflow in the workflow studio.
### February 7, 2026
**\[Improved]** New dashboard visual design
Refreshed visual design across the platform with updated components, colors, and layout.
**\[Improved]** Faster, more reliable evidence highlighting
Evidence highlighting now uses an event-driven architecture, making interactions faster and more reliable across all views.
**\[Improved]** Files and organization logos served securely through the backend
Files and organization logos are now served securely through the backend, improving security and simplifying local development.
**\[New]** AI workflow-name suggestions
New AI-powered workflow-name suggestions based on your description and uploaded files.
**\[Improved]** File status counts in workflow listings
Workflow listings now show at-a-glance counts of files by processing status.
**\[Fixed]** Document-element classification
Fixed a classification regression that could cause incorrect layout detection in some documents.
### February 3, 2026
**\[New]** Smart Lookup
New feature to enrich extracted data with external reference files using AI-powered matching.
**\[Improved]** Field source tracking
Fields now track their source (extraction vs. smart lookup) for better traceability.
**\[Fixed]** Manual field creation processing
Field data processing now correctly handles manually created fields.
## January 2026
### January 27, 2026
**\[New]** Usage-based billing
Introduced billing for data extraction operations with transparent per-page pricing.
**\[Improved]** Faster, more reliable field suggestions
Field suggestions are now faster and more reliable when creating workflows.
**\[Fixed]** Field value upload formats
Resolved an issue where certain field value formats could cause upload errors.
### January 20, 2026
**\[Improved]** Faster OCR processing
Document processing speed improved with optimized text recognition.
**\[New]** Extraction review from the dashboard
Administrators can now review extractions directly from the dashboard.
**\[Improved]** Organization credits management
Added the ability to manage organization credits for usage billing.
### January 13, 2026
**\[Improved]** Faster document processing
Significantly improved processing speed for large documents.
**\[New]** Filter results by row count
Filter extraction results by the number of rows extracted.
**\[New]** Configurable per-workflow page rotation
Configure custom page-rotation strategies per workflow.
### January 6, 2026
**\[Improved]** Enhanced results filtering on download
Enhanced results download with additional filtering options.
## December 2025
### December 15, 2025
**\[Improved]** Improved reliability
Enhanced system stability and faster deployments.
### December 1, 2025
**\[Improved]** Faster results retrieval for workflows with many documents
Optimized results retrieval for workflows that span many documents.
**\[Improved]** Detailed usage tracking
Added more detailed usage tracking for extraction operations.
## November 2025
### November 24, 2025
**\[New]** Create data rows from specific extractions
You can now create new data rows directly from specific extractions.
### November 17, 2025
**\[Improved]** More accurate table extraction
Improved accuracy when extracting data from complex tables.
**\[API]** New `/user/is-enabled` endpoint
A new endpoint lets you check account status.
**\[Improved]** User info on file validation responses
File validation responses now include the user who performed the validation.
### November 10, 2025
**\[Improved]** Better handling of long documents
Better handling of documents with many pages via smart chunking.
**\[Improved]** Validation tracking
Track which values have been manually validated or edited.
**\[Improved]** More accurate extraction
Enhanced extraction accuracy with an updated default model.
### November 3, 2025
**\[Improved]** Better extraction on documents over the context limit
Improved extraction quality for documents exceeding context limits.
**\[Improved]** Highest-confidence merge
When merging extractions, the highest-confidence value is now preserved.
## October 2025
### October 27, 2025
**\[Improved]** Performance improvements across the platform
General performance improvements across the platform.
### October 13, 2025
**\[Fixed]** CSV export field ordering
Fixed field ordering in CSV exports to match workflow configuration.
**\[Improved]** Consistent results ordering
Extraction results now consistently respect field order.
### October 6, 2025
**\[API]** Endpoint to reorder fields within object-type fields
A new endpoint lets you reorder fields within object-type fields.
**\[Improved]** Verification URLs on job results
Job results now include verification URLs for easy access.
**\[Fixed]** Fields with similar names in nested structures
Fixed handling of fields with similar names in nested structures.
### September 30, 2025
**\[Improved]** Redesigned extraction pipeline
Redesigned extraction workflow for better reliability.
# AI Suggestions
Source: https://docs.anyformat.ai/concepts/ai-suggestions
AI-generated recommendations that help you build schemas faster.
When you create a workflow, you describe what you want and (optionally) upload a sample document. **AI suggestions** are anyformat's first draft based on that — a starting set of fields it thinks you'll want, so you don't have to add them all from scratch.
For example, upload a resume and ask for "candidate contact info", and anyformat might suggest these fields:
* **Full name** (text)
* **Email** (text)
* **Phone** (text)
A suggestion can include the field name, its type, and a draft instruction. You then keep, edit, or delete each one.
***
## How to use them effectively
Think of AI suggestions as:
> A helpful starting point, not a final answer
* Exploring a new document type
* Creating a schema quickly
* You're not sure what fields you need yet
* Accuracy matters
* Data is business-critical
* You're scaling usage
You are always in control — suggestions are editable and optional.
**When reviewing a suggestion, check:**
* **Does the field name match what you actually want?** Rename anything that's vague or off.
* **Is the type right?** A total should be a Decimal number, not Text — see [Field types](/concepts/field-types).
* **Is anything missing?** Add fields the AI didn't think of.
* **Is anything extra?** Delete fields you don't need.
***
## What's next?
Build and run your first workflow
Learn about the structured results you can export
# Document packets
Source: https://docs.anyformat.ai/concepts/document-packets
The unit a workflow runs on: a bundle of one or more files anyformat treats as a single document.
A **document packet** is the unit a workflow runs on: one or more [files](/concepts/files) that anyformat treats as a single document.
Almost always it's a packet of **one** — you upload a file, you get a packet, and the two ideas line up 1-to-1. The packet only stands out as its own concept when you deliberately bundle several files (say, a contract and its two annexes) that belong together for extraction.
***
## Why a packet, not just a file
Extraction is scoped to the packet, not to the individual files inside it. That means:
* Cross-file context is preserved — parse and extract see the whole bundle at once, not one file at a time.
* Results come back at the packet level, as a single set of extracted fields for the whole document.
* Classify, split, and validate operate on the packet as one input.
For a one-file packet this looks identical to file-level processing. For a multi-file packet, it's what lets you treat "contract + annexes" or "invoice + supporting scans" as one document.
***
## Packets and files
| | What it is | What it's for |
| ------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------ |
| **Document packet** | The bundle. The runnable unit. | The handle you upload, run, and pull results against. |
| **File** | One artifact inside the packet — a PDF, an image, an email. | Reading an individual file's parsed content or per-file breakdown. |
The packet has an id; each file inside it has its own id too. You spend almost all your time working with packets — file ids only come up when you need to look at one specific file within a multi-file packet.
**Two API names — one object.** On **v3** the packet is addressed natively: every request and response uses `document_packet_id` (see the [packet endpoints](/api-reference-v3/document-packets/get)). The frozen **v2** surface still calls the same object a *file collection* — `file_collection_id`, sometimes `collection_id` — used the same way. The ids are interchangeable across versions; mapping details in the [migration guide](/api-reference-v3/migrating-from-v2#identifier-renames).
***
## How you get one
You create a packet by uploading. Two ways to hand anyformat the bytes:
* **Direct upload** (multipart) — send the file bytes in the request. The response comes back with `status: "uploaded"` once the bytes are in storage; you can run immediately. Best when the bytes are on the user's machine or in your app's memory.
* **From a URL** — give anyformat an HTTPS URL (a presigned S3 link, a hosted asset) and the server fetches the bytes for you. Best when the file already lives in object storage you can presign, or at a public URL — no need to stream it through your backend.
Both produce a packet id you can run, poll, and pull results against. See [Files](/concepts/files) for supported formats and size limits.
**Two API shapes — one concept.** On **v3**, upload with [`POST .../upload/`](/api-reference-v3/workflows/upload) (multipart, 1–10 files into one packet) or [`POST .../upload/from-url/`](/api-reference-v3/workflows/upload-from-url) (1–10 HTTPS URLs, all-or-nothing) — the URL import is atomic, so a `201` means the packet is fully in storage and there is no pending-fetch state to reason about. On **v2**, use [`POST .../files/`](/api-reference/files/create) or [`POST .../files/from-url/`](/api-reference/files/create-from-url), where the URL fetch completes asynchronously: the response returns `status: "pending"` while the fetch is in flight, and you should trigger the run on the returned packet id straight away rather than polling the packet listing — the listing's `status` reflects extraction state, not upload-fetch state.
***
## One file vs. multiple files
**One file (the default).** Every upload creates its own single-file packet. In everyday use the packet id and the file id are effectively interchangeable — you address the packet.
**Multiple files.** Bundle them into one packet only when they belong together as a single document. The packet is then processed as a whole; extraction runs over the bundle, not file-by-file. If two files are unrelated, upload them as two packets — that keeps their results independent.
***
## What's next?
Supported formats, size limits, and per-file mechanics
What happens when you run a workflow on a packet, and the shape of what comes back
# Field types
Source: https://docs.anyformat.ai/concepts/field-types
Canonical reference for every field type — UI name, API name, example, and when to use it.
The type tells anyformat what kind of value to expect, and shapes how it's validated and stored.
This page is the single source of truth for field types — it lists every type, its name in the UI and in the [API](/api-reference/introduction), and how to use it well.
***
## All field types at a glance
| UI name | API `data_type` | Example value |
| ----------------- | --------------- | --------------------------------------- |
| Text | `string` | `"INV-001"` |
| Decimal number | `float` | `1250.99` |
| Integer number | `integer` | `42` |
| Date | `date` | `"2024-03-15"` |
| Date & time | `datetime` | `"2024-03-15T10:30:00Z"` |
| Yes / No | `boolean` | `true` |
| Select | `enum` | One of a predefined list |
| Multiselect | `multi_select` | A list of values from a predefined list |
| Object (Subtable) | `object` | Repeating structured rows |
There is no separate `list` data\_type. Use **Multiselect** (`multi_select`) for a list of values from a fixed set of options, and **Object** (`object`) for a list of structured rows (a subtable). For a free-form list of strings or numbers, model it as an Object with a single nested field.
### Not sure which to pick?
A quick guide:
* **Is it a number with decimals, like a price or percentage?** → Decimal number (`float`)
* **Is it a whole number, like a count or quantity?** → Integer number (`integer`)
* **Is it a date without a time?** → Date
* **Is it a yes/no answer?** → Yes / No (`boolean`)
* **Does it have to be one value from a fixed list of options?** → Select (`enum`)
* **Could it be several values from that list?** → Multiselect (`multi_select`)
* **Is it a repeating table, like invoice line items?** → Object (Subtable) (`object`)
* **Anything else (names, IDs, addresses, free text)?** → Text (`string`)
Picking the right type matters: anyformat returns the value in that shape (a real number you can sum, a real date you can sort), and it helps the AI know what to look for.
***
## Field definition
Every field requires at least:
```json theme={null}
{
"name": "field_name",
"description": "What this field represents",
"data_type": "string"
}
```
* **`name`** — Unique identifier (snake\_case).
* **`description`** — Clear explanation of what to extract. Used by the AI as guidance.
* **`data_type`** — One of the types listed above.
Complex types (`object`, `enum`, `multi_select`) also take extra properties documented below.
***
## Simple types
**Best for:** Values that vary widely (company names, addresses).
**Watch out for:** Don't use if values are from a known list — use **Select** instead.
**Best for:** Calendar dates when time isn't relevant. Returned as `YYYY-MM-DD`.
**Watch out for:** If time appears in the document, prefer **Date & time**.
**Best for:** Precise timestamps when documents include a time, not just a date. Returned in a standard format like `2024-03-15T10:30:00Z`.
**Watch out for:** Timezones and unusual date formats can be ambiguous — add an instruction if the document's format is unusual.
**Best for:** Money, percentages, measurements.
**Watch out for:** Avoid if values should be whole numbers.
**Best for:** Counts, quantities, item numbers.
**Watch out for:** Not suitable for currency.
**Best for:** Clear true/false questions (Is paid? Is signed?).
**Watch out for:** Avoid vague cues like "maybe" or "often" — make instructions explicit.
***
## Select (`enum`)
Use `enum` when the extracted value must be **one** of a predefined set of options. The field requires an `enum_options` array.
```json theme={null}
{
"name": "payment_status",
"description": "The current payment status of the invoice",
"data_type": "enum",
"enum_options": [
{"name": "pending", "description": "Payment has not been received"},
{"name": "paid", "description": "Payment has been received in full"},
{"name": "partial", "description": "Partial payment has been received"},
{"name": "overdue", "description": "Payment is past the due date"}
]
}
```
If no option matches the document, the field value is `null`.
**Why use Select:**
* Enforces consistency
* Avoids spelling variations
* Makes analytics and filtering reliable
**Best practices:**
* Keep options short and unambiguous
* Provide clear descriptions for each option
* Avoid overlapping meanings
* Prefer Select over Text when values repeat
***
## Multiselect (`multi_select`)
Same shape as `enum`, but the field can return **multiple** matched options as an array.
```json theme={null}
{
"name": "document_tags",
"description": "Categories that apply to this document",
"data_type": "multi_select",
"enum_options": [
{"name": "urgent", "description": "Requires immediate attention"},
{"name": "confidential", "description": "Contains sensitive information"},
{"name": "reviewed", "description": "Has been reviewed by a team member"},
{"name": "pending_approval", "description": "Awaiting approval from management"}
]
}
```
Returns an array of strings:
```json theme={null}
{ "document_tags": ["urgent", "confidential"] }
```
### Select vs. Multiselect
| Feature | Select (`enum`) | Multiselect (`multi_select`) |
| ----------- | -------------------------- | ---------------------------- |
| Selection | Single value | Multiple values |
| Return type | `string` or `null` | `array` of strings |
| Use case | Mutually exclusive options | Non-exclusive categories |
***
## Object / Subtable (`object`)
Use `object` to extract a structured group of properties. Object fields require a `nested_fields` array.
Only **one level of nesting** is supported: the children of an `object` field must be non-object types (`string`, `integer`, `float`, `date`, `datetime`, `boolean`, `enum`, `multi_select`). An `object` field cannot contain another `object` field as a child.
### Single nested object
```json theme={null}
{
"name": "shipping_address",
"description": "Customer shipping address details",
"data_type": "object",
"nested_fields": [
{"name": "street", "data_type": "string", "description": "Street address including number"},
{"name": "city", "data_type": "string", "description": "City name"},
{"name": "postal_code", "data_type": "string", "description": "ZIP or postal code"},
{"name": "country", "data_type": "string", "description": "Country name"}
]
}
```
### Repeating rows (Subtable)
Object fields also capture **repeating tabular data** — invoice line items, transaction rows, anything that's a list of "things with the same structure".
```
line_items (object)
├── description (string)
├── quantity (integer)
├── unit_price (float)
└── line_total (float)
```
Each row in the document becomes one object in the resulting array.
**Use Object when:**
* You see the *same set of fields repeating*
* The document contains a list/table where each row is one "item"
* You need a structured value per row (not a blob of text)
**Best practices:**
* Keep subtable fields minimal at first (3–5 columns)
* Use clear row-level instructions: "Extract one row per item. Ignore headers and totals."
* Add shared fields like `currency` at the top level, not inside each row
* Start with the most reliable columns first (description + amount), then expand
**Common mistakes:**
* Using Object for a **single nested group** (like a "vendor address"). If it's not repeating, it's fine — but consider whether top-level fields would be simpler.
* Making the subtable too wide too early (10+ columns increases ambiguity).
* Nesting an Object **inside another Object**. Only one level of nesting is supported — object children must be non-object fields.
***
## Complete example
A workflow definition that uses several field types together:
```json theme={null}
{
"name": "Invoice Processing",
"description": "Extract invoice data with line items",
"fields": [
{"name": "invoice_number", "description": "Unique invoice identifier", "data_type": "string"},
{"name": "issue_date", "description": "Date when the invoice was issued", "data_type": "date"},
{"name": "total_amount", "description": "Total invoice amount including tax", "data_type": "float"},
{"name": "is_paid", "description": "Whether the invoice has been paid", "data_type": "boolean"},
{
"name": "payment_status",
"description": "Current payment status",
"data_type": "enum",
"enum_options": [
{"name": "pending", "description": "Awaiting payment"},
{"name": "paid", "description": "Fully paid"},
{"name": "overdue", "description": "Past due date"}
]
},
{
"name": "vendor",
"description": "Vendor information",
"data_type": "object",
"nested_fields": [
{"name": "name", "data_type": "string", "description": "Vendor company name"},
{"name": "address", "data_type": "string", "description": "Vendor address"}
]
}
]
}
```
***
## Tips for better results
1. **Be specific in descriptions.** "The invoice number, usually starting with INV-" beats "Invoice number".
2. **Use appropriate types.** `float` for amounts, `date` for dates — not `string`.
3. **Keep field names consistent.** snake\_case throughout.
4. **Describe location only when helpful.** "Total amount shown at the bottom right" can disambiguate, but the AI usually doesn't need it.
***
## What's next?
The three properties every field has
Write better extraction instructions
# Fields
Source: https://docs.anyformat.ai/concepts/fields
What a field is and the three properties every field has.
A **field** represents one piece of information inside a [schema](/concepts/schemas).
Examples:
* Invoice number
* Issue date
* Total amount
* Vendor name
Fields are the most important part of data quality. Clear fields lead to better results and less review.
***
## The three field properties
Every field has at least three properties.
A human-readable label that still makes sense outside anyformat.
Examples: `invoice_number`, `issue_date`, `total_amount`.
Use snake\_case so the name is safe to use as a JSON key or column header.
Tells anyformat what kind of value to expect (text, date, number, etc.).
The type improves consistency, validation, and output quality. See [Field types](/concepts/field-types) for the full list.
Plain-English guidance for how to extract the value.
Example: "Extract the final total including taxes. Ignore subtotals."
See [Instructions](/concepts/instructions) for how to write good ones.
***
## Field source — extraction vs. smart lookup
Every field also has a **source** that tells anyformat where its value comes from:
* **Extraction** (default) — the value is read directly from the document.
* **Smart lookup** — the value is resolved by matching the document against a **reference file** you attach (a CSV/catalog), instead of being read off the page. For example, extract a free-text `vendor_name` from the document, then smart-lookup the canonical `vendor_id` from your vendor catalog.
A smart-lookup field lives in the same schema as your extraction fields — you just mark it as a lookup field and attach the reference file to the extract step. See [Create Workflow](/api-reference/workflows/create) for the API shape.
***
## What's next?
The full list of types — text, date, number, object, enum, multi-select
Write effective extraction instructions
# Files
Source: https://docs.anyformat.ai/concepts/files
What a file is in anyformat and what happens when you upload one.
A file is the raw input — information without structure yet.
Examples:
* PDFs
* Images and scans
* Multi-page documents
anyformat reads files to understand text, layout, tables, and visual cues. You don't need to configure OCR engines or preprocessing steps.
***
## Supported file formats
| Category | Formats |
| ------------ | ---------------------------------------------------------------------------------- |
| PDF | `.pdf` |
| Documents | `.doc`, `.docx`, `.txt`, `.html`, `.htm`, `.rtf`, `.odt`, `.ppt`, `.pptx`, `.epub` |
| Spreadsheets | `.xlsx`, `.xls` |
| Markdown | `.md`, `.markdown` |
| Images | `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.tiff` |
| Email | `.eml`, `.msg` |
| Audio | `.mp3`, `.wav` |
### Limits
* **Maximum file size:** 20 MB per file
* **Page count:** No hard limit (usage-based billing applies)
***
## What happens when you upload a file
anyformat reads the document
Pages are detected
Content is analyzed and made searchable
The file is ready to be run through a workflow
***
## How files relate to workflows
Files aren't run through workflows directly — they're grouped into a [document packet](/concepts/document-packets), and the packet is what the workflow runs on. Most of the time that packet holds a single file, so this distinction stays out of sight; it only surfaces when you deliberately bundle several files as one document.
***
## What's next?
The runnable unit — how anyformat groups files for a workflow
How pages work within a file and why page boundaries matter
How usage is calculated
# How credits work
Source: https://docs.anyformat.ai/concepts/how-credits-work
Credits are anyformat's universal billing unit. Every operator a workflow runs consumes a fixed number of credits — here's what each one costs, how a run adds up, and what a credit is worth on your plan.
Every workflow you run spends **credits** — anyformat's universal billing unit. Each operator (parse, classify, split, extract, validate) consumes a fixed number of credits, and that number is **the same on every plan**. Your plan only decides two things: how many credits you start with, and what a credit costs if you buy more.
Because the per-operator cost is plan-independent, you can estimate any run in credits once and trust the number everywhere.
***
## What a credit is
A credit is the smallest unit anyformat bills in. Operators spend credits; everything else — pages, files, schema size — only matters insofar as it changes *how many* operators run and over *how much* content.
Two things never affect cost:
* How complex your schema is
* How many fields you define
What does affect cost:
* Which operators your workflow runs
* How many pages each operator processes (most operators are priced per page)
The [Usage & Billing](/concepts/usage-and-billing) page explains *why* usage works this way; this page gives you the concrete numbers.
***
## What each operator costs
| Operator | Credits | Billed per |
| ----------------- | ------- | ---------- |
| Parse | 25 | page |
| Classify | 10 | page |
| Split | 25 | page |
| Extract | 35 | page |
| Validate | 5 | rule ¹ |
| Parse — Agentic | 100 | page |
| Extract — Agentic | 150 | page |
¹ A validator is billed **per rule, once per extraction** — not per page or per file. A 3-rule validator costs 15 credits whether the document is 1 page or 100.
Most operators are billed **per page** — a 10-page parse costs 10 × 25 = 250 credits. Validators are the exception: billed **per rule, once per extraction**, so they don't scale with page count.
### Standard vs agentic
Parse and Extract each come in two tiers:
* **Standard** — fast, and the right default for clean, well-structured documents.
* **Agentic** — a heavier, multi-step pass for messy scans, complex tables, and low-quality inputs. It costs roughly **4×** the standard rate (Parse 100 vs 25, Extract 150 vs 35), so reach for it only when standard output isn't good enough.
Start every workflow on standard. Switch a node to agentic only for the document types where standard underperforms — you rarely need it everywhere.
These are the default prices. Enterprise plans can negotiate custom per-operator rates; if your organization has an override, your **Usage** page reflects the rates you actually pay.
***
## How a run adds up
Add up each operator over the content it touches. Example — a **3-page invoice** through parse → classify → extract, with a 2-rule validator on the result:
| Step | Rate | Quantity | Credits |
| --------- | --------- | -------- | ------- |
| Parse | 25 / page | 3 pages | 75 |
| Classify | 10 / page | 3 pages | 30 |
| Extract | 35 / page | 3 pages | 105 |
| Validate | 5 / rule | 2 rules | 10 |
| **Total** | | | **220** |
So one 3-page invoice costs **220 credits**. At the Business rate that's €0.22; at the Free / pay-as-you-go rate it's €0.33 — which is exactly why we quote costs in credits, not euros.
### Common shapes, at a glance
For the page-priced operators, here's what each common workflow costs **per page**:
| Workflow | Credits / page |
| ------------------------------- | -------------- |
| Parse only | 25 |
| Parse → Extract | 60 |
| Parse → Classify → Extract | 70 |
| Parse → Split → Extract | 85 |
| Agentic Parse → Agentic Extract | 250 |
Multiply by your page count, then add any validators (5 credits per rule, billed once per run).
***
## What a credit is worth
A credit's cash value depends on your plan. Included credits are spent at the same per-operator rates above, and top-ups are priced per plan:
| Plan | Top-up rate |
| -------------------- | ------------------------------------------ |
| Free / pay-as-you-go | €1.50 per 1,000 credits (€0.0015 / credit) |
| Business | €1.00 per 1,000 credits (€0.001 / credit) |
| Enterprise | Custom — volume-discounted |
Top-ups are bought in credits (minimum 30,000) from the **Usage** page in the app, via Stripe, and every top-up generates a downloadable invoice.
Because the euro value of a credit changes between plans, we always describe usage and grants in **credits**. The same 220-credit invoice above is €0.22 on Business and €0.33 on Free.
***
## Included credits by plan
Every plan comes with a credit allowance:
| Plan | Included credits |
| ---------- | --------------------------------------------- |
| Free | 50,000 — one-time signup grant, never expires |
| Business | 500,000 per month |
| Enterprise | Custom monthly allowance |
Free organizations don't get a monthly refill, but their signup grant never expires and they can **top up** at any time. See [anyformat.ai/pricing](https://www.anyformat.ai/pricing) for current plan prices.
Credits can come from three sources, tracked separately on your balance:
* **Plan grant** — your included monthly (or one-time, on Free) allowance.
* **Top-ups** — bought any time, priced per plan (above).
* **Vouchers** — promotional credits, which may carry an expiry (90 days by default).
***
## Tracking your usage
The **Usage** page in the [app](https://app.anyformat.ai) shows:
* Your current credit balance, broken down by source (plan grant, top-ups, vouchers)
* Weekly usage summaries and a per-workflow breakdown
* CSV export for your own reporting
* A **Top up** button (Stripe checkout) when you need more
Watch the per-workflow breakdown to spot which workflows dominate your spend — usually the ones running agentic parse/extract over high page counts.
***
## Spending fewer credits
A few habits keep credit usage low:
* **Default to standard parse/extract.** Agentic is \~4× the cost — use it only for document types where standard underperforms.
* **Only add Classify or Split when documents are genuinely mixed.** Each adds a per-page operator to every page; a single-type workflow doesn't need them.
* **Keep validators lean.** Every rule costs 5 credits per run (billed per rule, once — not per page). Validate what matters, not everything.
* **Parse only when you just need markdown.** Feeding your own pipeline (RAG, search, a custom model)? A [parse-only workflow](/guides/recipes/parse-only-workflow) skips extract entirely.
***
## What's next?
The usage model behind credits — what's measured, and why
The five operators that consume credits, and how they wire together
What happens when you run a workflow
Parse → extract walkthrough — UI, curl, and Python
# Instructions
Source: https://docs.anyformat.ai/concepts/instructions
Plain-English guidance you attach to a field to improve extraction accuracy.
**Instructions** tell anyformat *how to extract a field*. They are written in plain English and can include:
* What to look for
* Where it usually appears
* How to interpret ambiguous cases
**Example:**
> "Extract the total amount charged, including taxes. Ignore subtotals."
***
## Why instructions matter
* Reduce ambiguity
* Improve accuracy
* Lower review time
* Are vague
* Repeat the field name
* Try to encode logic unnecessarily
***
## Instruction best practices
Say exactly what you mean. Avoid shortcuts or assumptions.
Focus on *what* you want, not *where* it appears on the page.
If a value can appear in multiple forms, clarify which one to prefer.
Instructions should guide processing, not mirror the source.
You don't need to over-instruct. Clear intent beats long explanations.
***
## Examples
**Field:** `total_amount`
**Instruction:** "Extract the final total amount including taxes. If multiple totals appear, use the one labeled 'Grand Total' or 'Amount Due'."
**Field:** `total_amount`
**Instruction:** "The total amount." *(Too vague — doesn't clarify which total or whether to include taxes.)*
***
## What's next?
Let AI help you write fields and instructions
Put your schema to work with repeatable workflows
# Outputs
Source: https://docs.anyformat.ai/concepts/outputs
What anyformat produces — formats, when to use each, and how to consume them externally.
An output represents:
* The data you asked for
* In a structured, consistent form
* Ready to be reviewed, exported, or connected to other systems
It reflects your [schema](/concepts/schemas) (fields and structure), your [field types](/concepts/field-types), and your [instructions](/concepts/instructions).
An output is **not**:
* The original document
* Raw OCR text
* A screenshot of the page
It's the clean data layer that sits between documents and tools.
**Files** → **Schema & Fields** → **Workflow** → **Outputs**
Outputs are the bridge between documents and action.
***
## Output formats
anyformat works with the same extracted data, but presents it in **different representations** depending on what you want to do next.
### CSV
Structured tabular data:
* Each document becomes one or more rows
* Each field becomes a column
* Subtables are expanded in a consistent way
- Spreadsheets
- Imports
- Data analysis
- Sharing with non-technical users
Think of CSV as: *"Structured data optimized for tables."*
### Excel
Same structured data as CSV, packaged in a format that:
* Is easier to open for business users
* Preserves table structure
* Supports multiple sheets (for subtables or metadata)
- Manual review is part of the process
- Data is shared across teams
- You want minimal friction for non-technical stakeholders
### JSON
Structured data for systems and automation:
* Fields become keys
* Nested structures are preserved
* Data types remain explicit
- Integrating with other systems
- Building automation
- Preserving hierarchical data (like subtables)
Think of JSON as: *"Structured data optimized for machines."*
### Markdown
A **human-readable representation** of the result, used to:
* Inspect what was extracted
* Verify that parsing worked correctly
* Review results per document
- Markdown is **not a bulk export format**
- It's generated **per document**
- It exists to help you *understand* the result, not to move data elsewhere
Think of Markdown as: *"A debugging and review view for humans."*
***
## Choosing the right format
You usually don't need to decide upfront. Many users interact with more than one representation at different stages.
| Output | Best for |
| --------------- | ------------------------------------- |
| **CSV / Excel** | Analysis, sharing, business workflows |
| **JSON** | Integrations, automation |
| **Markdown** | Review and verification |
**Choose when:**
* You need to analyze data in spreadsheets
* Sharing with business users or stakeholders
* Importing into tools that accept tabular data
* Manual review and verification workflows
**CSV vs. Excel:**
* Use **CSV** for maximum compatibility and automation
* Use **Excel** when sharing with non-technical users who prefer spreadsheets
**Choose when:**
* Building integrations with other systems
* Automating data pipelines
* You need to preserve nested structures (like line items)
* Working with APIs or developer tools
**Choose when:**
* Reviewing individual results
* Debugging issues
* Verifying that parsing worked correctly
**Note:** Markdown is a review format, not an export format.
***
## Using outputs outside the platform
Once you trust your results, outputs can be:
* Downloaded manually
* Exported in bulk
* Accessed programmatically via the [API](/api-reference/introduction)
### Export options
Download individual results directly from the platform in your preferred format.
Export multiple documents at once for batch processing or archival.
Retrieve outputs programmatically for automation and integrations.
### Common integration patterns
* **Data pipelines** — Pull JSON outputs into your data warehouse
* **Business tools** — Export Excel/CSV for import into accounting or ERP systems
* **Custom applications** — Use the [API](/api-reference/introduction) to embed processing in your own products
***
## What's next?
Control what data appears in outputs
Produce outputs reliably at scale
# Pages
Source: https://docs.anyformat.ai/concepts/pages
How pages work within a file and why page boundaries matter.
A file can be a single page (an image or a one-page PDF) or many pages (a multi-page PDF). **Most of the time you don't need to think about pages at all — anyformat handles them automatically.** This page covers the two situations where they do come up.
***
## Reviewing where a value came from
When you open a document, anyformat shows you the original alongside the extracted data, and highlights the exact spot on the page where each value was found. Each value also shows a **confidence indicator** — a quick signal of how sure anyformat is about it (see [Analytics & quality](/guides/workflows/analytics-quality) for what confidence means). This is how you check *why* a value was extracted, not just what it is, and correct it if it's wrong.
***
## When a value spans multiple pages
The one time pages can trip you up is when a single thing is split across a page break — for example a table whose rows continue onto the next page, or a total that's on a different page from its line items. anyformat usually stitches these together correctly. If a result looks wrong, open the document and check the highlighted source: it'll show you which page the value came from, so you can spot whether something on a page boundary was missed.
***
## A note on usage
Usage is counted per page, so a 10-page document counts as more than a 1-page one. See [Usage & billing](/concepts/usage-and-billing) for details.
***
## What's next?
How usage is calculated based on pages and files
Define the structure of the data you want to extract
# Runs & results
Source: https://docs.anyformat.ai/concepts/runs-and-results
What happens when you run a workflow on a document — and the shape of what comes back, one section per atomic operation.
Start a run by submitting a document — through the UI, by calling [`POST /v2/workflows/{id}/run/`](/api-reference/workflows/run), or by uploading then running separately. anyformat creates a **file collection** to hold the document, walks the workflow graph, and writes structured **results**.
***
## Run lifecycle
Every run moves through a sequence of statuses:
| Status | Description |
| ------------- | ------------------------------------------------------- |
| `pending` | File collection created, processing not yet started |
| `queued` | Waiting for an available processing slot |
| `in_progress` | Processing is actively running |
| `processed` | Processing complete, results available |
| `error` | Processing failed |
| `cancelled` | Processing was cancelled (terminal state; stop polling) |
**Two API shapes — one concept.** On **v3**, read the run flat: [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get) always returns **200 OK** with the status above and, once `processed`, the results envelope inline. On **v2**, results hang off the file collection: [`GET /v2/workflows/{wid}/files/{collection_id}/results/`](/api-reference/files/results) returns **412 Precondition Failed** while the run is in flight and **200 OK** on completion. Both address the same run — only the read path differs.
In v3 the `pending` state lives on the [document packet](/concepts/document-packets) as `status: not_started` — a run only exists once triggered, so runs start at `queued`.
For production integrations, prefer [webhooks](/api-reference/webhooks/overview) over polling.
***
## What the results contain
A run produces **one output section per node type that ran**. The results envelope has a fixed shape — sections corresponding to nodes you didn't include come back empty (`null` or `[]`), so client code can read every key unconditionally.
| Section | From which node | Shape |
| ----------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parse` | the parse node | Object with structured markdown + per-block confidence. `null` if the workflow has no parse node (rare — parse is required by topology). |
| `classifications` | classify nodes | Array of `{category, confidence, evidence}`. Empty when no classifier ran. |
| `splits` | the splitter node | Array of `{name, files, confidence, partitions}` describing the segments. Empty when no splitter ran. |
| `extractions` | extract nodes | Array of `{split_name, partition, fields}`. Linear workflows produce one untagged entry; branched/split workflows produce one entry per `(split, partition)`. Empty when no extract ran (parse-only workflows). |
| `extraction` | extract nodes (legacy) | **Deprecated.** Flat `{field_name → ExtractedField}` dict, populated only for linear workflows. Kept for backward compatibility; new code should read `extractions[]`. Will be removed in a future major version. |
Each extracted field carries a `value`, an optional human-supplied `value_override`, a `confidence` score (0–100), a `verification_status`, and an `evidence` array (source-text snippets + page numbers). See [Response formats](/api-reference/response-formats) for the full envelope, including the canonical reading pattern for `value` vs `value_override`.
**What the SDKs give you back** — both the [TypeScript](/api-reference/sdks/typescript) and [Python](/api-reference/sdks/python) SDKs return a `Result` from `Run.wait()`. For linear `parse → extract` workflows the scalar values are right there: `result.fields["name"].value` (Python) or `result.field("name")?.value` (TypeScript). The full envelope — parse markdown, classifications, splits, multi-extraction entries — lives at `result.raw` in both languages, with Python additionally exposing a typed `result.parse` view. Expect more typed accessors as the underlying atomic operations stabilise.
***
## Confidence and evidence
Two signals that travel with most outputs.
### Confidence
A 0–100 score indicating how certain anyformat is about a value.
* For `parse`: two document-level scores — one for how confident anyformat is in the **text** it read, and one for how confident it is in the **page layout** it detected — plus a per-block score inside the rendered markdown. (Technically: `parse.parse_confidence` comes from the language model's token log-probabilities, and `parse.layout_confidence` from the YOLO layout-segmentation model; each `` of the markdown carries a `data-confidence` attribute.)
* For `extractions`: a per-field score on each extracted value.
* For `classifications`: a per-verdict score.
### Evidence
An **array** of metadata objects showing where a value came from.
It's an array because some values are **inferred** across multiple spans rather than copied from a single place. Each evidence object has:
* The **snippet of text** the value was derived from
* The **page number** in the document
Evidence is the right signal to surface in any human-review UI — it lets reviewers jump straight to the source.
***
## What's next?
Export formats for results — CSV, Excel, JSON, Markdown
Full schema of every section in the results envelope
The flat v3 read that returns a run with its results inline
Get notified the moment a run completes — no polling
# Schemas
Source: https://docs.anyformat.ai/concepts/schemas
How you describe the shape of the data you want from documents.
A **schema** is your extraction template — the list of fields anyformat should look for in each document. It says:
* Which fields you want (the invoice number, the date, the total…)
* What type of value each one holds (text, a number, a date…)
For example, a schema for invoices might be: **invoice number** (text), **issue date** (date), and **total amount** (a decimal number). Give anyformat that schema, and every invoice you process comes back filled in with those three fields.
A schema doesn't read documents by itself — it describes the **result** you want. The [workflow](/concepts/workflows) does the reading and the AI does the pulling-out; the schema tells them what to aim for.
***
## When you use schemas
You use a schema when:
* You want consistent output across many documents
* You care about structure, not just raw text
* You plan to verify or improve results over time
Most [workflows](/concepts/workflows) are built around a single schema.
***
## What a schema is not
A schema is just the *what* — the list of fields you want back. It is **not** the *how*: it doesn't tell anyformat how to read the document or where on the page to look. anyformat handles that for you. You only describe what the finished result should look like.
***
## What's next?
Define individual pieces of information within a schema
Write plain-English guidance for better accuracy
# Studio
Source: https://docs.anyformat.ai/concepts/studio
The visual editor where you build a workflow by connecting steps on a canvas.
**Studio** is the visual editor where you build a [workflow](/concepts/workflows) yourself, step by step. You drag the steps you need onto a canvas and connect them — like building a flowchart.
You don't always need Studio. The fastest way to start is to [describe what you want](/guides/workflows/creating) and let anyformat build a Parse → Extract workflow for you. Reach for Studio when you need more than that — sorting documents by type, splitting a file that holds several documents, or checking results against your own rules.
***
## How to open Studio
* **On an existing workflow:** open the workflow and click the **Studio** tab.
* **When creating a new one:** choose **Create from Studio** on the new-workflow screen. You start with a single Parse step and a blank canvas to build on.
***
## The building blocks
You drag these from the **Add Stages** panel on the left. Most workflows only use the first two.
| Step | What it does |
| --------------------- | --------------------------------------------------------------------------------------------------- |
| **Parse** | Reads the document and turns it into clean text and tables. Every workflow starts here. |
| **Extract** | Pulls out the specific fields you ask for. |
| **Classify** | Sorts documents into types you define, so each type can be handled differently. |
| **Split** | Breaks one file that contains several documents into separate pieces. |
| **Validate** *(Beta)* | Checks the extracted data against rules you write in plain language, and flags anything that fails. |
***
## What's next?
Step-by-step: open Studio, add steps, connect them, and configure each one
The mental model behind the building blocks
# Usage & Billing
Source: https://docs.anyformat.ai/concepts/usage-and-billing
How anyformat measures usage and how it shows up in billing.
Usage is based on **how much document content is processed**, not on:
* How complex your schema is
* How many fields you define
The main driver is **pages**. Each time anyformat processes a document, it counts the pages in that document. So:
* A 1-page invoice counts as 1 page.
* A 50-page report counts as 50 pages.
* Processing 100 single-page invoices counts as 100 pages.
Adding more fields to your schema, or more steps to your workflow, does **not** increase usage — only the amount of document content does. Documents that fail with an error are not counted.
***
## Why usage works this way
This model:
* Scales predictably
* Reflects real processing cost
* Encourages starting simple
You don't need to optimize usage early. Understanding the basics is enough until you scale.
***
## Pricing
Usage is billed in **credits** — a fixed number per operator, the same on every plan. See [How credits work](/concepts/how-credits-work) for the full per-operator breakdown, a worked example, and what a credit is worth on your plan.
For custom or enterprise pricing, contact us at [info@anyformat.ai](mailto:info@anyformat.ai).
***
## When to come back to this
You'll want to re-read this when:
* You process large volumes
* Costs start to matter
* Documents behave unexpectedly
* You're debugging processing issues
***
## What's next?
Per-operator credit costs and a worked example
Define what data you want
Run your schema repeatedly at scale
# Workflows
Source: https://docs.anyformat.ai/concepts/workflows
A workflow is the set of instructions that tells anyformat what to pull out of your documents. Build it once; it runs on every document you add.
A **workflow** is the set of instructions that tells anyformat what to do with your documents. You build it once, and it runs the same way on every document you give it.
Think of it as an assembly line made of a few simple steps:
* **Read** the document (Parse)
* **Pull out** the fields you care about (Extract)
* and, when you need them, **sort** documents by type (Classify), **split** a multi-document file apart (Split), or **check** the results against your own rules (Validate)
Each workflow targets **one kind of document** and **one shape of output**. Need a different output? Build a different workflow.
You don't have to assemble these steps by hand. The fastest way to start is to describe what you want in plain language and let anyformat build a Parse → Extract workflow for you. To arrange the steps yourself, use **[Studio](/concepts/studio)**, the visual editor.
***
## The five building blocks
Every workflow is built from five steps. Most workflows only need the first two.
| Step | What it does | Example |
| --------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| **Parse** | Reads the document and turns it into clean text and tables anyformat can work with. Every workflow starts here. | Upload a bank statement PDF — Parse reads and prepares all the text and layout. |
| **Extract** | Pulls out the specific fields you ask for. | Get the invoice number, total, and date from each invoice. |
| **Classify** | Sorts documents into types you define, so each type can be handled differently. | Label each incoming file as "Invoice", "Contract", or "Statement". |
| **Split** | Breaks one file that contains several documents into separate pieces. | A PDF with four invoices stapled together becomes four documents. |
| **Validate** *(Beta)* | Checks the extracted data against rules you write in plain language, and flags anything that fails. | Flag expired documents, or check that an IBAN looks valid. |
Most users never go past **Parse → Extract**. Classify, Split, and Validate are there for when your documents are more complicated.
***
## Three common shapes
These are the same building blocks arranged in different ways. You arrange them visually in **[Studio](/guides/studio/index)**.
### Read only
Just Parse, nothing else. You get clean text and tables back — handy when you want to feed anyformat's output into your own tools (search, a custom AI, etc.) rather than pulling out specific fields.
```
[ Parse ]
```
### Read, then pull out — the usual one
Parse into Extract. This is the default: "read this document and give me these fields."
```
[ Parse ] → [ Extract ]
```
### Sort first, then pull out
Classify decides what kind of document it is, then sends it to an Extract step tailored to that type. (Split works the same way for files that hold several documents.)
```
┌──> [ Extract: invoice ]
[ Parse ] → [ Classify ] ─┤
└──> [ Extract: receipt ]
```
See the [recipes](/guides/recipes/index) for end-to-end examples of each shape, or the [Studio guide](/guides/studio/index) to build them yourself.
***
## How to think about workflows
Most users follow the same lifecycle:
Decide which steps you need and how they connect — or just describe what you want and let anyformat build it
Run a few sample documents through it, look at the results, and tighten your fields and instructions
Mark it ready for everyday use
Apply it to many documents — by uploading them, connecting cloud storage, or (for developers) calling the API
In the [web platform](https://app.anyformat.ai) you build workflows visually — either by describing what you want from the home screen, or by arranging the steps yourself in [Studio](/guides/studio/index).
***
## What's next?
The visual editor where you arrange the building blocks into a workflow
What happens when you run a workflow, and what comes back
The kinds of values a field can hold, and when to use each
A step-by-step walkthrough — start in the UI, no code needed
# Coding assistant
Source: https://docs.anyformat.ai/guides/coding-assistant
Let Claude Code build and run anyformat workflows from your editor.
The **anyformat Claude Code skill** is a published npm package (`@anyformat/skill`) that teaches Claude — or any compatible AI coding agent — the anyformat workflow API. Once installed, you can ask Claude to build typed-graph workflows, run documents, and read structured results without context-switching out of your editor.
## Install
```bash theme={null}
npx @anyformat/skill # global — installs to ~/.claude/skills/anyformat
npx @anyformat/skill --project # project-local — installs to ./.claude/skills/anyformat
```
Set your API key in the environment and restart Claude Code so it picks up the new skill:
```bash theme={null}
export ANYFORMAT_API_KEY="af_..."
```
That's it — the next time you launch Claude Code, it has the skill loaded.
## What Claude can do once installed
* Build **linear parse → extract** workflows for a specific document type.
* Build **branched** workflows: classify-then-extract, splitter-then-extract.
* Run a document through an existing workflow and **poll for results** (412 → 200) with the right backoff.
* Read structured output — per-field confidence, evidence, the deprecated `extraction` vs. the new `extractions[]` array.
* Use **agentic parse mode** for documents with mixed tables / figures.
* Avoid the common gotchas: rate-limited submission tier, 412 polling, `extra_forbidden` errors when an unknown node-config key sneaks in.
## Example prompts
Concrete asks you can paste into Claude Code:
> **"Extract invoice number, total, and issue date from invoice.pdf using anyformat."**
> **"Parse contract.pdf to markdown with agentic mode using anyformat, then save the markdown to contract.md."**
> **"Build an anyformat workflow that classifies documents as invoice or receipt and extracts the right fields for each. Wire it up so I can pipe new files into it from the command line."**
> **"Set up an anyformat webhook that posts to my Express server, then run sample.pdf — show me the verification handler."**
Claude has the [SDKs](/api-reference/sdks/overview) (TypeScript and Python) available too, so it can choose the shape that fits your project — fluent builder in TS, fluent builder in Python, or raw HTTP via curl.
## How it works under the hood
The skill is a single SKILL.md plus reference docs that load into Claude Code's context when it detects an anyformat-related task. It drives the public workflow API directly — anything Claude does with the skill is reproducible with plain HTTP calls or either SDK.
If you want to inspect or hack on the skill itself, the source lives at [`@anyformat/skill` on npm](https://www.npmjs.com/package/@anyformat/skill).
## What's next?
Copy-paste examples Claude can also drive — invoices, resumes, contracts, more
Every endpoint Claude calls, with request/response schemas
The fluent layer Claude prefers for TS projects
The fluent layer Claude prefers for Python projects
# Quickstart
Source: https://docs.anyformat.ai/guides/quickstart
Build your first workflow and extract data from a document end-to-end — in the UI, with curl, with the TypeScript SDK, or with the Python SDK.
The walkthrough below has tabs at each step — pick **UI** to drive everything from [app.anyformat.ai](https://app.anyformat.ai), or **curl** / **TypeScript** / **Python** to drive the [API](/api-reference/introduction) directly. The four paths produce the same result.
**Not a developer?** Follow the **UI** tab the whole way down and ignore curl / TypeScript / Python — you can build and run everything from [app.anyformat.ai](https://app.anyformat.ai) without writing any code. For more advanced workflows (sorting documents by type, splitting files, validation rules), see **[Studio](/guides/studio/index)**.
Even if you *do* plan to integrate via the API, we recommend building your **first** workflow in the UI — it's faster to iterate on fields visually, and once it works you can copy the workflow ID and call it from code.
***
## Before you start
Create an account at [app.anyformat.ai](https://app.anyformat.ai). That's it.
Get an API key from [app.anyformat.ai/api-key](https://app.anyformat.ai/api-key). Export it for the snippets below:
```bash theme={null}
export ANYFORMAT_API_KEY="your_api_key_here"
```
Install the SDK (Node 18+):
```bash theme={null}
npm install @anyformat/sdk
```
Get an API key from [app.anyformat.ai/api-key](https://app.anyformat.ai/api-key) and pass it to the constructor — or set `ANYFORMAT_API_KEY` and read it from the environment:
```bash theme={null}
export ANYFORMAT_API_KEY="your_api_key_here"
```
Install the SDK (Python 3.13 — see the [SDK page](/api-reference/sdks/python) for the pin):
```bash theme={null}
pip install anyformat
```
Get an API key from [app.anyformat.ai/api-key](https://app.anyformat.ai/api-key) and pass it to the `Client` — or set `ANYFORMAT_API_KEY` and pick it up from the environment:
```bash theme={null}
export ANYFORMAT_API_KEY="your_api_key_here"
```
***
## 1. Create a workflow
A workflow defines what data to extract. We'll build a simple invoice processor with three fields: `invoice_number`, `total_amount`, `issue_date`.
1. From the home screen, type a description of what you want to extract (e.g. *"Invoice processing: extract invoice number, total, and issue date"*).
2. Drag in a sample invoice PDF (optional but recommended — anyformat will suggest fields from the document).
3. Click the **send icon** (the arrow button in the text box) to start.
anyformat opens the workflow workspace with the document on the left and the fields panel on the right. Review the suggested fields and adjust as needed.
Once your fields look right, copy the workflow ID from the URL or workflow settings — you'll need it if you want to run this workflow via the API later.
A workflow is a [typed graph](/concepts/workflows) of nodes. The minimal extraction shape is a `parse` node feeding an `extract` node — two nodes, one edge.
```bash theme={null}
curl -X POST 'https://api.anyformat.ai/v2/workflows/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-d '{
"name": "Invoice Processing",
"description": "Extract key data from invoices",
"nodes": [
{"id": "parse_1", "type": "parse"},
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{"name": "invoice_number", "description": "The unique invoice identifier", "data_type": "string"},
{"name": "total_amount", "description": "Total invoice amount", "data_type": "float"},
{"name": "issue_date", "description": "Date when the invoice was issued", "data_type": "date"}
]
}
}
],
"edges": [{"source": "parse_1", "target": "extract_1"}]
}'
```
The response contains your workflow ID:
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Invoice Processing",
"description": "Extract key data from invoices",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
```
The SDK exposes a fluent builder over the same [typed graph](/concepts/workflows).
```typescript theme={null}
import { Anyformat, Schema } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
// .create() persists the workflow and returns its id.
const workflowId = await af
.workflow("Invoice Processing", "Extract key data from invoices")
.parse()
.extract([
Schema.string("invoice_number", "The unique invoice identifier"),
Schema.float("total_amount", "Total invoice amount"),
Schema.date("issue_date", "Date when the invoice was issued"),
])
.create();
console.log(`Created workflow: ${workflowId}`);
```
The SDK exposes a fluent builder over the same [typed graph](/concepts/workflows).
```python theme={null}
import os
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
workflow = (
client.workflow("Invoice Processing")
.parse()
.extract([
Schema.string("invoice_number", "The unique invoice identifier"),
Schema.float("total_amount", "Total invoice amount"),
Schema.date("issue_date", "Date when the invoice was issued"),
])
.create() # persists and returns a Workflow handle
)
print(f"Created workflow: {workflow.id}")
```
This is the simple Parse → Extract shape. You can also build parse-only workflows, sort mixed document types with Classify, or split multi-document files — all by arranging the steps yourself in **[Studio](/guides/studio/index)**.
***
## 2. Run the workflow on a document
From the workflow workspace, drag in (or upload via the **Add document** button) the invoice you want to process. Processing starts automatically and usually completes in 10–60 seconds depending on the document.
You can also open a single document (click its filename) and run it from the **Process document** button on the right.
Replace `WORKFLOW_ID` with the ID returned in step 1.
```bash theme={null}
curl -X POST 'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/run/' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-F 'file=@invoice.pdf'
```
The response returns a file ID — keep it; you'll use it to poll for results:
```json theme={null}
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "pending",
"workflow_id": "550e8400-e29b-41d4-a716-446655440000",
"version_id": "FGaV4I2JAA"
}
```
`status: "pending"` means the run was accepted, not that extraction has finished — poll the results endpoint. `version_id` records the workflow version your run was bound to, which lets you verify which schema produced the results (useful right after an edit).
The TS SDK collapses "submit + poll" into a single `.run(file).wait()` chain. Build the same workflow shape (or look it up by id with the low-level client) and call `.run(file)` on it:
```typescript theme={null}
const file: File = /* a File with .name set, e.g. new File([bytes], "invoice.pdf") */;
const result = await af
.workflow("Invoice Processing", "Extract key data from invoices")
.parse()
.extract([
Schema.string("invoice_number", "The unique invoice identifier"),
Schema.float("total_amount", "Total invoice amount"),
Schema.date("issue_date", "Date when the invoice was issued"),
])
.run(file)
.wait(); // continues into step 3
```
```python theme={null}
run = workflow.run("invoice.pdf") # Path | str | bytes
```
***
## 3. Get the extracted data
Results appear in the workflow workspace as soon as processing finishes — no refresh needed.
* **Table view** shows all your documents at once. The first tab lists your documents (one row each); extra tabs show the output of each step in your workflow.
* **Document view** (click a filename) shows one document on its own, with each extracted value highlighted on the page it came from — click a field to see where it came from.
Export the results as **CSV**, **Excel**, or **JSON** from the workflow view. See [Outputs](/concepts/outputs) for the differences.
Poll the results endpoint. It returns **412** while processing and **200** when the run is done.
```bash theme={null}
curl -H "Authorization: Bearer $ANYFORMAT_API_KEY" \
"https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/files/FILE_ID/results/"
```
For production integrations, use [webhooks](/api-reference/webhooks/overview) instead of polling — webhooks deliver results immediately and don't consume rate limit.
`wait()` polls until the run completes (412 → still going, 200 → done) and returns a typed `Result`.
```typescript theme={null}
// `result` was awaited in step 2
console.log(result.field("invoice_number")?.value);
console.log(result.field("total_amount")?.value);
console.log(result.field("issue_date")?.value);
```
`wait()` polls until the run completes (412 → still going, 200 → done) and returns a typed `Result`.
```python theme={null}
result = run.wait()
print(result.fields["invoice_number"].value)
print(result.fields["total_amount"].value)
print(result.fields["issue_date"].value)
```
For production integrations, use [webhooks](/api-reference/webhooks/overview) instead of polling.
A completed result looks like this (one section per node type that ran — `parse` and `extractions` here; `classifications` and `splits` are empty because this workflow has neither):
```json theme={null}
{
"collection_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"verification_url": "https://app.anyformat.ai/workflows/.../files/...",
"parse": { "markdown": "...", "text": "...", "parse_confidence": 94.2, "layout_confidence": 87.4, "blocks": [] },
"classifications": [],
"splits": [],
"extractions": [
{
"split_name": null,
"partition": null,
"fields": {
"invoice_number": {
"value": "INV-2024-0847",
"confidence": 97.0,
"evidence": [{"text": "Invoice #INV-2024-0847", "page_number": 1}],
"verification_status": "not_verified"
},
"total_amount": {
"value": 4087.50,
"confidence": 96.0,
"evidence": [{"text": "Total: $4,087.50", "page_number": 2}],
"verification_status": "not_verified"
},
"issue_date": {
"value": "2024-03-15",
"confidence": 93.0,
"evidence": [{"text": "Date: March 15, 2024", "page_number": 1}],
"verification_status": "not_verified"
}
}
}
]
}
```
See [Runs & results](/concepts/runs-and-results) for the model behind these sections, and [Response formats](/api-reference/response-formats) for every field in the envelope.
***
## Complete script
The three steps above are narrative slices of the same script. Here they are end-to-end as a single pasteable block.
```typescript theme={null}
import { Anyformat, Schema } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
const file: File = /* a File with .name set, e.g. new File([bytes], "invoice.pdf") */;
const result = await af
.workflow("Invoice Processing", "Extract key data from invoices")
.parse()
.extract([
Schema.string("invoice_number", "The unique invoice identifier"),
Schema.float("total_amount", "Total invoice amount"),
Schema.date("issue_date", "Date when the invoice was issued"),
])
.run(file)
.wait();
console.log(result.field("invoice_number")?.value);
console.log(result.field("total_amount")?.value);
console.log(result.field("issue_date")?.value);
```
```python theme={null}
import os
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
workflow = (
client.workflow("Invoice Processing")
.parse()
.extract([
Schema.string("invoice_number", "The unique invoice identifier"),
Schema.float("total_amount", "Total invoice amount"),
Schema.date("issue_date", "Date when the invoice was issued"),
])
.create()
)
result = workflow.run("invoice.pdf").wait()
print(result.fields["invoice_number"].value)
print(result.fields["total_amount"].value)
print(result.fields["issue_date"].value)
```
***
## Where to go next
Deeper walkthrough of the Define → Refine → Publish lifecycle in the UI
End-to-end examples — invoices, resumes, contracts, receipts, and more
Let Claude Code build and run anyformat workflows from your editor
Every endpoint, every response, every error code
# Agentic Parse to Markdown
Source: https://docs.anyformat.ai/guides/recipes/agentic-parse-to-markdown
End-to-end recipe — create a parse-only workflow with agentic routing, upload a document, retrieve markdown with per-block confidence scores
Each block in the document is routed through a typed strategy (text / table / figure / dense-table-agent) instead of a single batched call — better tables and richer figure descriptions.
## What is agentic parsing?
Standard parsing sends every block to one LLM call with one prompt. Agentic parsing first **routes** each block (using a fast LLM with the page image) to the right specialised strategy:
| Block type | Strategy | Why |
| ---------------- | -------------------- | ---------------------------------------------------------------------- |
| Plain text | `text-bytes-first` | Cheap, fast — bypasses LLM entirely when source bytes are extractable |
| Tables | `dense-table-agent` | Tool-calling agent that handles spans, merged cells, multi-page tables |
| Figures / charts | `single-call vision` | LLM with image input for structured figure descriptions |
## End-to-end
```bash theme={null}
# 1. Create an agentic parse-only workflow
curl -X POST 'https://api.anyformat.ai/v2/workflows/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-d '{
"name": "Agentic parse-only",
"nodes": [{"id": "parse_1", "type": "parse", "mode": "agentic"}],
"edges": []
}'
# 2. Submit a document
curl -X POST 'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/run/' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-F 'file=@document.pdf'
# 3. Poll for results (agentic mode takes 30–90s for a typical 3-page document)
curl -H "Authorization: Bearer $ANYFORMAT_API_KEY" \
'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/files/COLLECTION_ID/results/'
```
```typescript theme={null}
import { Anyformat } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
const file: File = /* a File with .name set */;
const workflow = await af
.workflow("Agentic parse-only")
.parse({ mode: "agentic" })
.create();
const run = await workflow.run(file);
const result = await run.wait({ timeoutMs: 180_000, pollMs: 3_000 }); // agentic runs are slower
// Agentic mode doesn't emit parseConfidence — fall back to layoutConfidence.
const confidence = result.parse?.parseConfidence ?? result.parse?.layoutConfidence;
console.log(`document confidence: ${confidence}`);
console.log(result.parse?.markdown?.slice(0, 500) ?? "");
```
```python theme={null}
import os
from anyformat.sdk import Client
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
workflow = (
client.workflow("Agentic parse-only")
.parse(mode="agentic")
.create()
)
result = workflow.run("document.pdf").wait(timeout=180) # agentic runs are slower
# Agentic mode doesn't emit parse_confidence — fall back to layout_confidence.
# Explicit `is not None` check (not `or`) so a real 0.0 confidence still wins.
confidence = (
result.parse.parse_confidence
if result.parse.parse_confidence is not None
else result.parse.layout_confidence
)
print(f"document confidence: {confidence}")
print((result.parse.markdown or "")[:500])
```
Agentic mode takes longer than standard mode — typically 30–90s for a 3-page document — because each block hits its own LLM strategy. Use [webhooks](/api-reference/webhooks/overview) instead of polling for production workloads.
### Sample response
```json theme={null}
{
"collection_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"verification_url": "https://app.anyformat.ai/workflows/.../files/...",
"parse": {
"markdown": "...",
"text": "...",
"parse_confidence": null,
"layout_confidence": 47.8,
"blocks": [/* … */]
},
"classifications": [],
"splits": [],
"extractions": []
}
```
## Per-block vs. document confidence
The response carries two document-level rollups plus a per-block `data-confidence` attribute inside the markdown.
```html theme={null}
# ACME CORPORATION
```
| Field | Use for |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| **`parse.parse_confidence`** — char-weighted mean of per-block LLM logprobs (typical 80–99); `null` when no block had logprob-based confidence | Triage: "is this doc worth processing further?" |
| **`parse.layout_confidence`** — char-weighted mean of YOLO layout-segmentation scores (typical 30–60); always present when blocks exist | Fallback when `parse_confidence` is null. Measures "is this region a table?", not "is the parsed content accurate?" |
| **`data-confidence`** per `` | UI highlighting: dim or flag low-confidence regions inline |
The document-level values are **character-weighted means** — a 500-char paragraph at 80% counts \~100× more than a 5-char header at 99%.
**Mode caveat:** in agentic mode, per-block strategies don't always populate parser logprobs (e.g. the fast `text-bytes-first` strategy never calls an LLM). When logprobs are absent, `parse_confidence` is `null` and callers fall back to `layout_confidence`. For calibrated parser confidence comparable to extraction confidence (80–99 range), use `mode="standard"`.
## Use with AI coding agents
Building an integration on top of anyformat? Install the **anyformat Claude Code skill** so Claude (or any compatible agent) knows the right endpoints, payloads, and gotchas out of the box. See [Coding assistant](/guides/coding-assistant) for installation and example prompts.
## Next steps
Full reference for the typed-graph endpoint
Standard parse-only cookbook with the same endpoint
Add an extract node after the parse step to pull structured fields
Skip polling — receive `extraction.completed` events
# Bank Statement Processing
Source: https://docs.anyformat.ai/guides/recipes/bank-statement-processing
Process transactions from bank statements and retrieve unified results
Run multiple statements through the same workflow and collect transactions row-by-row.
## Workflow fields
We recommend creating this workflow in the [anyformat platform](https://app.anyformat.ai) where you can test with sample statements and iterate on field descriptions. Copy the workflow ID to use with the API.
| Field | Type | Description |
| ------------------------ | ------- | ------------------------------------------------------------ |
| `account_holder` | string | Name on the account |
| `account_number` | string | Account number (masked or full) |
| `statement_period_start` | date | Start of statement period |
| `statement_period_end` | date | End of statement period |
| `opening_balance` | float | Balance at start of period |
| `closing_balance` | float | Balance at end of period |
| `total_deposits` | float | Sum of all deposits |
| `total_withdrawals` | float | Sum of all withdrawals |
| `transaction_count` | integer | Number of transactions |
| `transactions` | object | Individual transactions (date / description / amount / type) |
## End-to-end
```bash theme={null}
# 1. Create the workflow
curl -X POST 'https://api.anyformat.ai/v2/workflows/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-d '{
"name": "Bank Statement Processor",
"description": "Extract statement summary and transactions",
"nodes": [
{"id": "parse_1", "type": "parse"},
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{"name": "account_holder", "description": "Name of the account holder as shown on the statement", "data_type": "string"},
{"name": "account_number", "description": "Bank account number, may be partially masked", "data_type": "string"},
{"name": "statement_period_start", "description": "First day of the statement period", "data_type": "date"},
{"name": "statement_period_end", "description": "Last day of the statement period", "data_type": "date"},
{"name": "opening_balance", "description": "Account balance at the start of the period", "data_type": "float"},
{"name": "closing_balance", "description": "Account balance at the end of the period", "data_type": "float"},
{"name": "total_deposits", "description": "Total amount deposited during the statement period", "data_type": "float"},
{"name": "total_withdrawals", "description": "Total amount withdrawn during the statement period", "data_type": "float"},
{"name": "transaction_count", "description": "Total number of transactions in the statement period", "data_type": "integer"},
{
"name": "transactions",
"description": "Individual transactions listed on the statement",
"data_type": "object",
"nested_fields": [
{"name": "date", "description": "Date of the transaction", "data_type": "date"},
{"name": "description", "description": "Transaction description or memo", "data_type": "string"},
{"name": "amount", "description": "Transaction amount (positive for deposits, negative for withdrawals)", "data_type": "float"},
{
"name": "type",
"description": "Type of transaction",
"data_type": "enum",
"enum_options": [
{"name": "deposit", "description": "Incoming deposit"},
{"name": "withdrawal", "description": "Outgoing withdrawal"},
{"name": "fee", "description": "Bank fee or charge"},
{"name": "transfer", "description": "Transfer between accounts"},
{"name": "interest", "description": "Interest earned or charged"}
]
}
]
}
]
}
}
],
"edges": [{"source": "parse_1", "target": "extract_1"}]
}'
# 2. Run each statement (stay under the 60 req/min file-submission limit)
for f in statement-jan.pdf statement-feb.pdf statement-mar.xlsx; do
curl -X POST 'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/run/' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-F "file=@$f"
sleep 1
done
# 3. Poll each collection_id for results
curl -H "Authorization: Bearer $ANYFORMAT_API_KEY" \
'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/files/COLLECTION_ID/results/'
```
```typescript theme={null}
import { Anyformat, Schema } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
// Create the workflow once, then reuse the handle for every statement.
const workflow = await af
.workflow("Bank Statement Processor", "Extract statement summary and transactions")
.parse()
.extract([
Schema.string("account_holder", "Name of the account holder as shown on the statement"),
Schema.string("account_number", "Bank account number, may be partially masked"),
Schema.date("statement_period_start", "First day of the statement period"),
Schema.date("statement_period_end", "Last day of the statement period"),
Schema.float("opening_balance", "Account balance at the start of the period"),
Schema.float("closing_balance", "Account balance at the end of the period"),
Schema.float("total_deposits", "Total amount deposited during the statement period"),
Schema.float("total_withdrawals", "Total amount withdrawn during the statement period"),
Schema.integer("transaction_count", "Total number of transactions in the statement period"),
Schema.object("transactions", "Individual transactions listed on the statement", [
Schema.date("date", "Date of the transaction"),
Schema.string("description", "Transaction description or memo"),
Schema.float("amount", "Transaction amount (positive for deposits, negative for withdrawals)"),
Schema.enum("type", "Type of transaction", [
Schema.option("deposit", "Incoming deposit"),
Schema.option("withdrawal", "Outgoing withdrawal"),
Schema.option("fee", "Bank fee or charge"),
Schema.option("transfer", "Transfer between accounts"),
Schema.option("interest", "Interest earned or charged"),
]),
]),
])
.create();
// Submit every statement against the existing workflow id.
const files: File[] = /* an array of File objects with .name set */;
for (const file of files) {
const run = await workflow.run(file);
const result = await run.wait();
console.log(result.field("account_holder")?.value, result.field("closing_balance")?.value);
}
```
```python theme={null}
import os
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
workflow = (
client.workflow("Bank Statement Processor")
.parse()
.extract([
Schema.string("account_holder", "Name of the account holder as shown on the statement"),
Schema.string("account_number", "Bank account number, may be partially masked"),
Schema.date("statement_period_start", "First day of the statement period"),
Schema.date("statement_period_end", "Last day of the statement period"),
Schema.float("opening_balance", "Account balance at the start of the period"),
Schema.float("closing_balance", "Account balance at the end of the period"),
Schema.float("total_deposits", "Total amount deposited during the statement period"),
Schema.float("total_withdrawals", "Total amount withdrawn during the statement period"),
Schema.integer("transaction_count", "Total number of transactions in the statement period"),
Schema.object("transactions", "Individual transactions listed on the statement", fields=[
Schema.date("date", "Date of the transaction"),
Schema.string("description", "Transaction description or memo"),
Schema.float("amount", "Transaction amount (positive for deposits, negative for withdrawals)"),
Schema.enum("type", "Type of transaction", options=[
Schema.option("deposit", "Incoming deposit"),
Schema.option("withdrawal", "Outgoing withdrawal"),
Schema.option("fee", "Bank fee or charge"),
Schema.option("transfer", "Transfer between accounts"),
Schema.option("interest", "Interest earned or charged"),
]),
]),
])
.create()
)
for path in ["statement-jan.pdf", "statement-feb.pdf", "statement-mar.xlsx"]:
result = workflow.run(path).wait()
print(result.fields["account_holder"].value, result.fields["closing_balance"].value)
# Iterate transactions from the raw extractions array
rows = result.raw["extractions"][0]["fields"]["transactions"]
for txn in rows:
if txn["type"]["value"] == "fee":
print(f"Fee: {txn['description']['value']} — ${abs(txn['amount']['value'])}")
```
## Tips
XLSX statements yield better results than scanned PDFs — the data is structured in cells rather than requiring OCR.
* Describe `amount` as "positive for deposits, negative for withdrawals" to get a consistent sign convention.
* Submit serially with a small delay (or run multiple workflows in parallel) to stay under the 60 req/min file-submission limit.
* `integer` `transaction_count` is a quick sanity check against the length of the extracted rows.
## Next steps
The unified JSON response shape
Enumerate all processed files in a workflow
# Contract Analysis
Source: https://docs.anyformat.ai/guides/recipes/contract-analysis
Identify clauses and key terms with webhook-driven processing
Uses **webhooks** instead of polling — the recommended pattern for production integrations where contracts are processed asynchronously.
## Workflow fields
We recommend creating this workflow in the [anyformat platform](https://app.anyformat.ai) where you can test with sample contracts and iterate on field descriptions. Copy the workflow ID to use with the API.
| Field | Type | Description |
| ------------------------- | ------------- | ----------------------------------------- |
| `contract_type` | enum | Type of agreement |
| `effective_date` | date | When the contract takes effect |
| `expiration_date` | date | When the contract expires |
| `auto_renewal` | boolean | Whether the contract renews automatically |
| `governing_law` | string | Jurisdiction governing the contract |
| `termination_notice_days` | integer | Required notice period for termination |
| `key_clauses` | multi\_select | Which standard clauses are present |
| `liability_cap` | float | Maximum liability amount if specified |
## Setup and submit
The workflow is created once. After that, each new contract triggers one webhook delivery when processing finishes.
```bash theme={null}
# 1. Create the workflow (once)
curl -X POST 'https://api.anyformat.ai/v2/workflows/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-d '{
"name": "Contract Analyzer",
"description": "Extract key terms and clauses from contracts",
"nodes": [
{"id": "parse_1", "type": "parse"},
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{
"name": "contract_type",
"description": "The type of legal agreement",
"data_type": "enum",
"enum_options": [
{"name": "nda", "description": "Non-disclosure agreement"},
{"name": "service_agreement", "description": "Service or consulting agreement"},
{"name": "employment", "description": "Employment contract"},
{"name": "lease", "description": "Lease or rental agreement"},
{"name": "partnership", "description": "Partnership agreement"},
{"name": "licensing", "description": "Licensing agreement"}
]
},
{"name": "effective_date", "description": "Date the contract takes effect", "data_type": "date"},
{"name": "expiration_date", "description": "Date the contract expires or terminates", "data_type": "date"},
{"name": "auto_renewal", "description": "Whether the contract automatically renews at expiration", "data_type": "boolean"},
{"name": "governing_law", "description": "State or jurisdiction whose laws govern this contract", "data_type": "string"},
{"name": "termination_notice_days", "description": "Number of days advance notice required to terminate", "data_type": "integer"},
{
"name": "key_clauses",
"description": "Standard contract clauses that are present in this agreement",
"data_type": "multi_select",
"enum_options": [
{"name": "confidentiality", "description": "Confidentiality or NDA clause"},
{"name": "non_compete", "description": "Non-compete restriction"},
{"name": "indemnification", "description": "Indemnification or hold-harmless clause"},
{"name": "limitation_of_liability", "description": "Cap on damages or liability"},
{"name": "force_majeure", "description": "Force majeure or act-of-God clause"},
{"name": "arbitration", "description": "Mandatory arbitration clause"},
{"name": "intellectual_property", "description": "IP ownership or assignment clause"},
{"name": "non_solicitation", "description": "Non-solicitation of employees or clients"}
]
},
{"name": "liability_cap", "description": "Maximum liability amount in dollars, if a cap is specified", "data_type": "float"}
]
}
}
],
"edges": [{"source": "parse_1", "target": "extract_1"}]
}'
# 2. Register a webhook (once) — store the returned secret
curl -X POST 'https://api.anyformat.ai/v2/webhooks/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-d '{
"url": "https://your-server.com/webhooks/anyformat",
"events": ["extraction.completed", "extraction.failed"]
}'
# → { "id": "...", "secret": "..." } ← STORE THIS, it is only returned once
# 3. Submit a contract (no polling — the webhook will fire when done)
curl -X POST 'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/run/' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-F 'file=@contract.pdf'
```
```typescript theme={null}
import { Anyformat, Schema } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
// 1. Build the workflow — the typed graph is assembled by the fluent builder.
const builder = af
.workflow("Contract Analyzer", "Extract key terms and clauses from contracts")
.parse()
.extract([
Schema.enum("contract_type", "The type of legal agreement", [
Schema.option("nda", "Non-disclosure agreement"),
Schema.option("service_agreement", "Service or consulting agreement"),
Schema.option("employment", "Employment contract"),
Schema.option("lease", "Lease or rental agreement"),
Schema.option("partnership", "Partnership agreement"),
Schema.option("licensing", "Licensing agreement"),
]),
Schema.date("effective_date", "Date the contract takes effect"),
Schema.date("expiration_date", "Date the contract expires or terminates"),
Schema.boolean("auto_renewal", "Whether the contract automatically renews at expiration"),
Schema.string("governing_law", "State or jurisdiction whose laws govern this contract"),
Schema.integer("termination_notice_days","Number of days advance notice required to terminate"),
Schema.multiSelect("key_clauses", "Standard contract clauses present in this agreement", [
Schema.option("confidentiality", "Confidentiality or NDA clause"),
Schema.option("non_compete", "Non-compete restriction"),
Schema.option("indemnification", "Indemnification or hold-harmless clause"),
Schema.option("limitation_of_liability", "Cap on damages or liability"),
Schema.option("force_majeure", "Force majeure or act-of-God clause"),
Schema.option("arbitration", "Mandatory arbitration clause"),
Schema.option("intellectual_property", "IP ownership or assignment clause"),
Schema.option("non_solicitation", "Non-solicitation of employees or clients"),
]),
Schema.float("liability_cap", "Maximum liability amount in dollars, if a cap is specified"),
]);
// 2. Create the workflow + a webhook (one-time setup). Webhook endpoints
// aren't on the SDK surface yet — call them via fetch and store the secret.
const workflow = await builder.create();
const webhookResponse = await fetch("https://api.anyformat.ai/v2/webhooks/", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ANYFORMAT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://your-server.com/webhooks/anyformat",
events: ["extraction.completed", "extraction.failed"],
}),
});
const webhook = await webhookResponse.json();
// STORE webhook.secret securely — it is only returned once.
// 3. Submit each contract against the existing workflow handle. Skip .wait()
// — the webhook delivers the result.
const file: File = /* a File with .name set, e.g. new File([bytes], "contract.pdf") */;
const run = await workflow.run(file);
console.log(`Submitted ${run.id}; waiting for webhook…`);
```
```python theme={null}
import os
import httpx
from anyformat.sdk import Client
from anyformat.workflow import Schema
api_key = os.environ["ANYFORMAT_API_KEY"]
client = Client(api_key=api_key)
# 1. Create the workflow (once)
workflow = (
client.workflow("Contract Analyzer")
.parse()
.extract([
Schema.enum("contract_type", "The type of legal agreement", options=[
Schema.option("nda", "Non-disclosure agreement"),
Schema.option("service_agreement", "Service or consulting agreement"),
Schema.option("employment", "Employment contract"),
Schema.option("lease", "Lease or rental agreement"),
Schema.option("partnership", "Partnership agreement"),
Schema.option("licensing", "Licensing agreement"),
]),
Schema.date("effective_date", "Date the contract takes effect"),
Schema.date("expiration_date", "Date the contract expires or terminates"),
Schema.boolean("auto_renewal", "Whether the contract automatically renews at expiration"),
Schema.string("governing_law", "State or jurisdiction whose laws govern this contract"),
Schema.integer("termination_notice_days","Number of days advance notice required to terminate"),
Schema.multi_select("key_clauses", "Standard contract clauses present in this agreement", options=[
Schema.option("confidentiality", "Confidentiality or NDA clause"),
Schema.option("non_compete", "Non-compete restriction"),
Schema.option("indemnification", "Indemnification or hold-harmless clause"),
Schema.option("limitation_of_liability", "Cap on damages or liability"),
Schema.option("force_majeure", "Force majeure or act-of-God clause"),
Schema.option("arbitration", "Mandatory arbitration clause"),
Schema.option("intellectual_property", "IP ownership or assignment clause"),
Schema.option("non_solicitation", "Non-solicitation of employees or clients"),
]),
Schema.float("liability_cap", "Maximum liability amount in dollars, if a cap is specified"),
])
.create()
)
# 2. Register a webhook (once). Webhook endpoints aren't on the SDK surface
# yet — call them via httpx and store the returned secret.
webhook = httpx.post(
"https://api.anyformat.ai/v2/webhooks/",
headers={"Authorization": f"Bearer {api_key}"},
json={
"url": "https://your-server.com/webhooks/anyformat",
"events": ["extraction.completed", "extraction.failed"],
},
).json()
# STORE webhook["secret"] securely — it is only returned once.
# 3. Submit each contract. Skip .wait() — the webhook will fire when done.
workflow.run("contract.pdf")
```
## Handle the webhook callback
When processing completes, your server receives a POST with the `collection_id` and `workflow_id`. Verify the HMAC signature, then fetch the results from `GET /v2/workflows/{workflow_id}/files/{collection_id}/results/`. See [Webhooks overview](/api-reference/webhooks/overview#payload-structure) for the full payload schema and the verification recipe.
A minimal Python (Flask) handler:
```python theme={null}
import hmac, hashlib, os, httpx
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["ANYFORMAT_WEBHOOK_SECRET"]
API_KEY = os.environ["ANYFORMAT_API_KEY"]
@app.route("/webhooks/anyformat", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Webhook-Signature", "")
expected = hmac.new(WEBHOOK_SECRET.encode(), request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(f"sha256={expected}", signature):
return jsonify({"error": "invalid signature"}), 401
event = request.json
if event["event"] == "extraction.completed":
data = event["data"]
results = httpx.get(
f"https://api.anyformat.ai/v2/workflows/{data['workflow_id']}"
f"/files/{data['collection_id']}/results/",
headers={"Authorization": f"Bearer {API_KEY}"},
).json()
extraction = results["extractions"][0]["fields"]
if "indemnification" not in extraction["key_clauses"]["value"]:
print("WARNING: no indemnification clause")
return jsonify({"status": "ok"}), 200
```
The TypeScript story is analogous: receive the POST in your server framework of choice (Express, Hono, Next route handler…), verify the signature with `crypto.createHmac("sha256", secret)`, then fetch the results via `fetch()` or the SDK's low-level client.
## Tips
Webhook secrets are only returned at creation. Store the secret immediately. If lost, delete the webhook and create a new one.
* Webhooks eliminate polling overhead and rate-limit consumption.
* `integer` for notice periods lets you do calendar math directly.
## Next steps
Set up, sign, and verify webhook deliveries
`multi_select`, `enum`, and other field shapes
# Email Lead Extraction
Source: https://docs.anyformat.ai/guides/recipes/email-lead-extraction
Extract leads from email text using the text input method
Uses the **text input method**, which sends raw email body text directly to the API without a file upload.
## Workflow fields
We recommend creating this workflow in the [anyformat platform](https://app.anyformat.ai) where you can test with sample emails and iterate on field descriptions. Copy the workflow ID to use with the API.
| Field | Type | Description |
| -------------- | ------------- | --------------------------------------------- |
| `sender_name` | string | Name of the person who sent the email |
| `sender_email` | string | Email address of the sender |
| `company_name` | string | Company or organization the sender represents |
| `inquiry_type` | enum | Category of the inquiry |
| `urgency` | enum | How urgent the request seems |
| `topics` | multi\_select | Products or areas mentioned |
| `summary` | string | Brief summary of the email |
## End-to-end
```bash theme={null}
# 1. Create the workflow
curl -X POST 'https://api.anyformat.ai/v2/workflows/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-d '{
"name": "Email Lead Extractor",
"description": "Triage inbound sales emails into structured leads",
"nodes": [
{"id": "parse_1", "type": "parse"},
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{"name": "sender_name", "description": "Full name of the person who sent the email", "data_type": "string"},
{"name": "sender_email", "description": "Email address of the sender", "data_type": "string"},
{"name": "company_name", "description": "Company or organization the sender represents", "data_type": "string"},
{
"name": "inquiry_type",
"description": "The primary category of this inquiry",
"data_type": "enum",
"enum_options": [
{"name": "pricing", "description": "Pricing or cost inquiry"},
{"name": "demo_request", "description": "Request for a product demo"},
{"name": "support", "description": "Technical support or help request"},
{"name": "partnership", "description": "Partnership or integration inquiry"},
{"name": "other", "description": "Other inquiry type"}
]
},
{
"name": "urgency",
"description": "How urgent this request appears based on language and deadlines mentioned",
"data_type": "enum",
"enum_options": [
{"name": "low", "description": "No urgency signals"},
{"name": "medium", "description": "Moderate urgency or soft deadline"},
{"name": "high", "description": "Explicit deadline or urgent language"}
]
},
{
"name": "topics",
"description": "Products or areas of interest mentioned in the email",
"data_type": "multi_select",
"enum_options": [
{"name": "product_a", "description": "Product A"},
{"name": "product_b", "description": "Product B"},
{"name": "enterprise", "description": "Enterprise plan or features"},
{"name": "integration", "description": "Integration or API capabilities"},
{"name": "pricing", "description": "Pricing or billing"},
{"name": "security", "description": "Security or compliance"}
]
},
{"name": "summary", "description": "A one-sentence summary of what the sender is asking for", "data_type": "string"}
]
}
}
],
"edges": [{"source": "parse_1", "target": "extract_1"}]
}'
# 2. Submit the email body as text (no file upload)
curl -X POST 'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/run/' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"text": "From: Maria Lopez \nSubject: Enterprise pricing for Q2\n\nHi, I am the VP of Engineering at Acme Corp..."
}'
# 3. Poll for results
curl -H "Authorization: Bearer $ANYFORMAT_API_KEY" \
'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/files/COLLECTION_ID/results/'
```
```typescript theme={null}
import { Anyformat, Schema } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
const emailText = `From: Maria Lopez
Subject: Enterprise pricing for Q2
Hi, I am the VP of Engineering at Acme Corp...`;
// `Workflow.run(null, { text })` submits raw text instead of a file.
const workflow = await af
.workflow("Email Lead Extractor", "Triage inbound sales emails into structured leads")
.parse()
.extract([
Schema.string("sender_name", "Full name of the person who sent the email"),
Schema.string("sender_email", "Email address of the sender"),
Schema.string("company_name", "Company or organization the sender represents"),
Schema.enum("inquiry_type", "The primary category of this inquiry", [
Schema.option("pricing", "Pricing or cost inquiry"),
Schema.option("demo_request", "Request for a product demo"),
Schema.option("support", "Technical support or help request"),
Schema.option("partnership", "Partnership or integration inquiry"),
Schema.option("other", "Other inquiry type"),
]),
Schema.enum("urgency", "How urgent this request appears", [
Schema.option("low", "No urgency signals"),
Schema.option("medium", "Moderate urgency or soft deadline"),
Schema.option("high", "Explicit deadline or urgent language"),
]),
Schema.multiSelect("topics", "Products or areas of interest mentioned", [
Schema.option("product_a", "Product A"),
Schema.option("product_b", "Product B"),
Schema.option("enterprise", "Enterprise plan or features"),
Schema.option("integration", "Integration or API capabilities"),
Schema.option("pricing", "Pricing or billing"),
Schema.option("security", "Security or compliance"),
]),
Schema.string("summary", "A one-sentence summary of what the sender is asking for"),
])
.create();
const run = await workflow.run(null, { text: emailText });
const result = await run.wait();
if (result.field("urgency")?.value === "high") {
console.log(`HIGH PRIORITY: ${result.field("sender_name")?.value} from ${result.field("company_name")?.value}`);
}
```
```python theme={null}
import os
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
email_text = """From: Maria Lopez
Subject: Enterprise pricing for Q2
Hi, I am the VP of Engineering at Acme Corp..."""
workflow = (
client.workflow("Email Lead Extractor")
.parse()
.extract([
Schema.string("sender_name", "Full name of the person who sent the email"),
Schema.string("sender_email", "Email address of the sender"),
Schema.string("company_name", "Company or organization the sender represents"),
Schema.enum("inquiry_type", "The primary category of this inquiry", options=[
Schema.option("pricing", "Pricing or cost inquiry"),
Schema.option("demo_request", "Request for a product demo"),
Schema.option("support", "Technical support or help request"),
Schema.option("partnership", "Partnership or integration inquiry"),
Schema.option("other", "Other inquiry type"),
]),
Schema.enum("urgency", "How urgent this request appears", options=[
Schema.option("low", "No urgency signals"),
Schema.option("medium", "Moderate urgency or soft deadline"),
Schema.option("high", "Explicit deadline or urgent language"),
]),
Schema.multi_select("topics", "Products or areas of interest mentioned", options=[
Schema.option("product_a", "Product A"),
Schema.option("product_b", "Product B"),
Schema.option("enterprise", "Enterprise plan or features"),
Schema.option("integration", "Integration or API capabilities"),
Schema.option("pricing", "Pricing or billing"),
Schema.option("security", "Security or compliance"),
]),
Schema.string("summary", "A one-sentence summary of what the sender is asking for"),
])
.create()
)
# `Workflow.run(text=...)` submits raw text instead of a file.
result = workflow.run(text=email_text).wait()
if result.fields["urgency"].value == "high":
print(f"HIGH PRIORITY: {result.fields['sender_name'].value} from {result.fields['company_name'].value}")
```
## Tips
A `string` field like `summary` with a descriptive prompt acts as a mini-summarizer, giving you a one-sentence digest alongside the extracted fields.
* Write detailed `enum_options` descriptions to help distinguish between similar categories. "pricing" vs "demo\_request" can be ambiguous without good descriptions.
* `multi_select` captures all relevant topics even when an email discusses several products at once.
* For `.eml` or `.msg` files, use file upload instead — it preserves headers and attachments.
## Next steps
All input methods: file upload and text
Use webhooks instead of polling for production email pipelines
# Overview
Source: https://docs.anyformat.ai/guides/recipes/index
Production-ready recipes for common document processing use cases
Copy-paste recipes for real-world document processing scenarios. Each recipe includes a complete workflow configuration, code examples, and domain-specific tips. Every recipe also highlights a different API capability.
Convert documents to structured markdown without extraction
Extract line items, totals, and dates from invoices using nested objects
Parse candidate data from DOC/DOCX resumes
Extract totals from receipt photos with consistent array formatting
Identify clauses and key terms with webhook-driven processing
Capture leads from email text using the text input method
Process transactions from statements in CSV and JSONL formats
# Invoice Processing
Source: https://docs.anyformat.ai/guides/recipes/invoice-processing
Extract line items, totals, and dates from invoices using nested objects
This recipe uses the `object` field type with `nested_fields` to capture tabular line-item data.
## Workflow fields
We recommend creating this workflow in the [anyformat platform](https://app.anyformat.ai) where you can test with sample invoices and iterate on field descriptions. Copy the workflow ID to use with the API.
| Field | Type | Description |
| ---------------- | ------ | ------------------------------------------------------------------ |
| `invoice_number` | string | The unique invoice identifier |
| `vendor_name` | string | Name of the company issuing the invoice |
| `issue_date` | date | Date the invoice was issued |
| `due_date` | date | Payment due date |
| `subtotal` | float | Amount before tax |
| `tax_amount` | float | Total tax applied |
| `total_amount` | float | Final amount due including tax |
| `currency` | enum | Currency code (USD, EUR, GBP, …) |
| `line_items` | object | Individual line items (description, quantity, unit\_price, amount) |
## End-to-end
Three-step flow: create the workflow, run a document, poll the results endpoint until it returns `200`.
```bash theme={null}
# 1. Create the workflow
curl -X POST 'https://api.anyformat.ai/v2/workflows/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-d '{
"name": "Invoice Processor",
"description": "Extract invoice header and line items",
"nodes": [
{"id": "parse_1", "type": "parse"},
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{"name": "invoice_number", "description": "The unique invoice identifier", "data_type": "string"},
{"name": "vendor_name", "description": "Name of the company that issued the invoice", "data_type": "string"},
{"name": "issue_date", "description": "Date the invoice was issued", "data_type": "date"},
{"name": "due_date", "description": "Date by which payment is due", "data_type": "date"},
{"name": "subtotal", "description": "Amount before tax", "data_type": "float"},
{"name": "tax_amount", "description": "Total tax amount", "data_type": "float"},
{"name": "total_amount", "description": "Final total amount due including tax", "data_type": "float"},
{
"name": "currency",
"description": "Currency of the invoice amounts",
"data_type": "enum",
"enum_options": [
{"name": "USD", "description": "US Dollar"},
{"name": "EUR", "description": "Euro"},
{"name": "GBP", "description": "British Pound"}
]
},
{
"name": "line_items",
"description": "Individual line items listed on the invoice",
"data_type": "object",
"nested_fields": [
{"name": "description", "description": "Description of the item or service", "data_type": "string"},
{"name": "quantity", "description": "Number of units", "data_type": "integer"},
{"name": "unit_price", "description": "Price per unit", "data_type": "float"},
{"name": "amount", "description": "Total amount for this line item", "data_type": "float"}
]
}
]
}
}
],
"edges": [{"source": "parse_1", "target": "extract_1"}]
}'
# → { "id": "550e8400-…", ... } ← workflow_id
# 2. Run a document
curl -X POST 'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/run/' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-F 'file=@invoice.pdf'
# → { "id": "069dcc2c-…", "status": "pending", ... } ← collection_id
# 3. Poll for results (412 while processing, 200 when done)
curl -H "Authorization: Bearer $ANYFORMAT_API_KEY" \
'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/files/COLLECTION_ID/results/'
```
For production integrations, use [webhooks](/api-reference/webhooks/overview) instead of polling — they deliver results immediately and don't consume your rate limit.
```typescript theme={null}
import { Anyformat, Schema } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
const file: File = /* a File with .name set (e.g. read from or new File([bytes], "invoice.pdf")) */;
const workflow = await af
.workflow("Invoice Processor", "Extract invoice header and line items")
.parse()
.extract([
Schema.string("invoice_number", "The unique invoice identifier"),
Schema.string("vendor_name", "Name of the company that issued the invoice"),
Schema.date("issue_date", "Date the invoice was issued"),
Schema.date("due_date", "Date by which payment is due"),
Schema.float("subtotal", "Amount before tax"),
Schema.float("tax_amount", "Total tax amount"),
Schema.float("total_amount", "Final total amount due including tax"),
Schema.enum("currency", "Currency of the invoice amounts", [
Schema.option("USD", "US Dollar"),
Schema.option("EUR", "Euro"),
Schema.option("GBP", "British Pound"),
]),
Schema.object("line_items", "Individual line items listed on the invoice", [
Schema.string("description", "Description of the item or service"),
Schema.integer("quantity", "Number of units"),
Schema.float("unit_price", "Price per unit"),
Schema.float("amount", "Total amount for this line item"),
]),
])
.create();
const run = await workflow.run(file);
const result = await run.wait();
// Scalar accessor — undefined for missing or list-valued fields.
console.log(result.field("invoice_number")?.value);
console.log(result.field("total_amount")?.value);
// Nested rows live under extractions[0].fields.
console.log(result.extractions[0]?.fields.line_items);
```
```python theme={null}
import os
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
workflow = (
client.workflow("Invoice Processor")
.parse()
.extract([
Schema.string("invoice_number", "The unique invoice identifier"),
Schema.string("vendor_name", "Name of the company that issued the invoice"),
Schema.date("issue_date", "Date the invoice was issued"),
Schema.date("due_date", "Date by which payment is due"),
Schema.float("subtotal", "Amount before tax"),
Schema.float("tax_amount", "Total tax amount"),
Schema.float("total_amount", "Final total amount due including tax"),
Schema.enum("currency", "Currency of the invoice amounts", options=[
Schema.option("USD", "US Dollar"),
Schema.option("EUR", "Euro"),
Schema.option("GBP", "British Pound"),
]),
Schema.object("line_items", "Individual line items listed on the invoice", fields=[
Schema.string("description", "Description of the item or service"),
Schema.integer("quantity", "Number of units"),
Schema.float("unit_price", "Price per unit"),
Schema.float("amount", "Total amount for this line item"),
]),
])
.create()
)
result = workflow.run("invoice.pdf").wait()
print(result.fields["invoice_number"].value)
print(result.fields["total_amount"].value)
# Nested rows: read from the raw extractions array
print(result.raw["extractions"][0]["fields"]["line_items"])
```
### Example response
The full envelope is documented in [Response formats](/api-reference/response-formats). The top-level keys are `collection_id`, `verification_url`, `parse`, `classifications`, `splits`, and `extractions`. Each scalar field follows the `ExtractedField` shape (`value`, `value_override`, `verification_status`, `confidence`, `evidence`).
```json theme={null}
{
"collection_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"verification_url": "https://app.anyformat.ai/workflows/.../files/...",
"parse": {"markdown": "...", "text": "...", "parse_confidence": 94.2, "layout_confidence": 87.4, "blocks": []},
"classifications": [],
"splits": [],
"extractions": [
{
"split_name": null,
"partition": null,
"fields": {
"invoice_number": {
"value": "INV-2024-0847",
"confidence": 97.0,
"evidence": [{"text": "Invoice #INV-2024-0847", "page_number": 1}],
"verification_status": "not_verified"
},
"total_amount": {
"value": 4087.50,
"confidence": 96.0,
"evidence": [{"text": "Total: $4,087.50", "page_number": 2}],
"verification_status": "not_verified"
},
"currency": {
"value": "USD",
"confidence": 98.0,
"evidence": [{"text": "USD", "page_number": 1}],
"verification_status": "not_verified"
}
}
}
]
}
```
## Tips
* Write specific field descriptions. "Total amount due including tax" extracts better than just "total".
* If invoices span multiple currencies, add the `enum_options` for all currencies you expect.
* Multi-page invoices are handled automatically; no special configuration needed.
## Next steps
Object, enum, and other complex field types
Full schema of every section in the results envelope
# Parse-Only Workflow
Source: https://docs.anyformat.ai/guides/recipes/parse-only-workflow
Convert documents to structured markdown without extraction — useful for previewing, debugging, or building custom pipelines
## When to use this
* **Document preview** — see the markdown before writing extraction fields
* **Custom pipelines** — feed parsed markdown into your own LLM, search index, or RAG system
* **Debugging** — understand how a document is parsed (blocks, tables, reading order)
* **Lightweight integration** — you only need the text + per-block confidence
## End-to-end
A parse-only workflow has one `parse` node and no edges. For documents with mixed content (tables, figures, dense text), use **agentic mode** by flipping `mode` to `"agentic"` — each block is routed through a typed strategy (text / table / figure) instead of a single batched call.
```bash theme={null}
# 1. Create the workflow (standard mode shown; for agentic, add "mode": "agentic")
curl -X POST 'https://api.anyformat.ai/v2/workflows/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-d '{
"name": "Document Parser",
"description": "Parse documents to markdown without extraction",
"nodes": [{"id": "parse_1", "type": "parse"}],
"edges": []
}'
# 2. Submit a document
curl -X POST 'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/run/' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-F 'file=@document.pdf'
# 3. Poll for results
while true; do
RESPONSE=$(curl -s -w '\n%{http_code}' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
"https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/files/COLLECTION_ID/results/")
STATUS=$(echo "$RESPONSE" | tail -1)
[ "$STATUS" = "200" ] && echo "$RESPONSE" | head -n -1 && break
[ "$STATUS" != "412" ] && echo "Error: $STATUS" && exit 1
sleep 5
done
```
```typescript theme={null}
import { Anyformat } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
const file: File = /* a File with .name set */;
// Standard mode. For agentic, pass { mode: "agentic" } to parse().
const workflow = await af
.workflow("Document Parser", "Parse documents to markdown without extraction")
.parse()
.create();
const run = await workflow.run(file);
const result = await run.wait();
const markdown = result.parse?.markdown ?? "";
// parseConfidence may be null in some parse modes — fall back to layoutConfidence.
const confidence = result.parse?.parseConfidence ?? result.parse?.layoutConfidence;
console.log(`confidence: ${confidence}`);
console.log(markdown.slice(0, 500));
```
```python theme={null}
import os
from anyformat.sdk import Client
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
# Standard mode. For agentic, pass mode="agentic" to .parse().
workflow = (
client.workflow("Document Parser")
.parse()
.create()
)
result = workflow.run("document.pdf").wait()
# parse_confidence may be None in some parse modes — fall back to layout_confidence.
# Explicit `is not None` check (not `or`) so a real 0.0 confidence still wins.
confidence = (
result.parse.parse_confidence
if result.parse.parse_confidence is not None
else result.parse.layout_confidence
)
print(f"confidence: {confidence}")
print((result.parse.markdown or "")[:500])
```
For a parse-only workflow, the `extractions` array is empty in the response — only the `parse` section is populated:
```json theme={null}
{
"collection_id": "069dcc2c-e14c-7606-8000-2ee4fb17b4e1",
"verification_url": "https://app.anyformat.ai/workflows/.../files/...",
"parse": {
"markdown": "...",
"text": "...",
"parse_confidence": 94.2,
"layout_confidence": 87.4,
"blocks": [/* … */]
},
"classifications": [],
"splits": [],
"extractions": []
}
```
## Confidence
The response carries two document-level confidence rollups plus a per-block attribute inside the rendered markdown.
| Field | Source | Range | Use for |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- |
| `parse.parse_confidence` | Char-weighted mean of per-block LLM logprobs. `null` when no blocks have logprob-based confidence. | 80–99 typical | Triage — "is this doc trustworthy enough to extract from?" |
| `parse.layout_confidence` | Char-weighted mean of YOLO layout-segmentation scores. Present whenever blocks exist. | 30–60 typical | Fallback when `parse_confidence` is null. Measures "is this region a table?", not "is the parsed content accurate?" |
| `data-confidence` attribute on each `` | Per-block — calibrated logprobs when available, YOLO fallback otherwise | 0–100 | UI highlight — "dim low-confidence regions" |
**Agentic mode caveat:** agentic strategies don't always populate per-block logprobs (e.g. `text-bytes-first` never calls an LLM), so `parse_confidence` is often `null` and callers fall back to `layout_confidence`. For calibrated parser confidence (apples-to-apples with extraction confidence), use `mode="standard"`.
## Example output
```markdown theme={null}
# ACME CORPORATION
123 Business Ave, Suite 100
New York, NY 10001
Item
Quantity
Price
Widget A
10
$25.00
```
Each `` includes:
* **`id`** — block identifier (page and block number)
* **`data-type`** — semantic type: `title`, `text`, `table`, `picture`, `other`
* **`data-confidence`** — 0–100 confidence in this block (parser-calibrated when available, YOLO fallback otherwise)
* **`data-bbox`** — bounding-box coordinates (normalised 0–1)
* **`data-cell-id`** — table-cell identifiers for precise referencing
## Tips
* **Reuse one workflow.** Create a single parse-only workflow and submit all documents to it — no need for separate workflows per document type.
* **Tables are preserved** — output as HTML `
` with cell IDs.
* **Multi-page** — each page gets its own `` block with a `page` attribute. All pages processed automatically.
* **Use `parse.parse_confidence` for triage** — filter low-confidence documents into a manual-review queue before downstream processing. Fall back to `parse.layout_confidence` when `parse_confidence` is null (e.g. agentic mode).
## Next steps
The full agentic walkthrough with confidence details
Add extraction fields to get structured data
A full extraction example with nested fields and line items
Full schema of the results endpoint
# Receipt Scanning
Source: https://docs.anyformat.ai/guides/recipes/receipt-scanning
Extract totals from receipt photos
## Workflow fields
We recommend creating this workflow in the [anyformat platform](https://app.anyformat.ai) where you can test with sample receipt photos and iterate on field descriptions. Copy the workflow ID to use with the API.
| Field | Type | Description |
| ------------------ | ------- | ----------------------------- |
| `store_name` | string | Name of the store or merchant |
| `store_address` | string | Store location |
| `receipt_date` | date | Date of purchase |
| `total_amount` | float | Total amount charged |
| `tax_amount` | float | Tax amount |
| `number_of_items` | integer | Number of items purchased |
| `payment_method` | enum | How the purchase was paid |
| `contains_alcohol` | boolean | Whether alcohol was purchased |
## End-to-end
```bash theme={null}
# 1. Create the workflow
curl -X POST 'https://api.anyformat.ai/v2/workflows/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-d '{
"name": "Receipt Scanner",
"description": "Extract receipt details from photos and scans",
"nodes": [
{"id": "parse_1", "type": "parse"},
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{"name": "store_name", "description": "Name of the store or merchant", "data_type": "string"},
{"name": "store_address", "description": "Full address of the store location", "data_type": "string"},
{"name": "receipt_date", "description": "Date of the purchase transaction", "data_type": "date"},
{"name": "total_amount", "description": "Total amount charged including tax", "data_type": "float"},
{"name": "tax_amount", "description": "Total tax amount", "data_type": "float"},
{"name": "number_of_items", "description": "Total number of items purchased", "data_type": "integer"},
{
"name": "payment_method",
"description": "Payment method used for the transaction",
"data_type": "enum",
"enum_options": [
{"name": "cash", "description": "Cash payment"},
{"name": "credit_card", "description": "Credit card payment"},
{"name": "debit_card", "description": "Debit card payment"},
{"name": "mobile_payment", "description": "Mobile or digital wallet payment"}
]
},
{"name": "contains_alcohol", "description": "Whether any alcoholic beverage was included in the purchase", "data_type": "boolean"}
]
}
}
],
"edges": [{"source": "parse_1", "target": "extract_1"}]
}'
# 2. Run a receipt photo
curl -X POST 'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/run/' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-F 'file=@receipt.jpg'
# 3. Poll for results
curl -H "Authorization: Bearer $ANYFORMAT_API_KEY" \
'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/files/COLLECTION_ID/results/'
```
```typescript theme={null}
import { Anyformat, Schema } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
const file: File = /* a File with .name set, e.g. new File([bytes], "receipt.jpg") */;
const workflow = await af
.workflow("Receipt Scanner", "Extract receipt details from photos and scans")
.parse()
.extract([
Schema.string("store_name", "Name of the store or merchant"),
Schema.string("store_address", "Full address of the store location"),
Schema.date("receipt_date", "Date of the purchase transaction"),
Schema.float("total_amount", "Total amount charged including tax"),
Schema.float("tax_amount", "Total tax amount"),
Schema.integer("number_of_items", "Total number of items purchased"),
Schema.enum("payment_method", "Payment method used for the transaction", [
Schema.option("cash", "Cash payment"),
Schema.option("credit_card", "Credit card payment"),
Schema.option("debit_card", "Debit card payment"),
Schema.option("mobile_payment", "Mobile or digital wallet payment"),
]),
Schema.boolean("contains_alcohol", "Whether any alcoholic beverage was included"),
])
.create();
const run = await workflow.run(file);
const result = await run.wait();
console.log(result.field("store_name")?.value);
console.log(result.field("total_amount")?.value);
console.log(result.field("contains_alcohol")?.value);
```
```python theme={null}
import os
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
workflow = (
client.workflow("Receipt Scanner")
.parse()
.extract([
Schema.string("store_name", "Name of the store or merchant"),
Schema.string("store_address", "Full address of the store location"),
Schema.date("receipt_date", "Date of the purchase transaction"),
Schema.float("total_amount", "Total amount charged including tax"),
Schema.float("tax_amount", "Total tax amount"),
Schema.integer("number_of_items", "Total number of items purchased"),
Schema.enum("payment_method", "Payment method used for the transaction", options=[
Schema.option("cash", "Cash payment"),
Schema.option("credit_card", "Credit card payment"),
Schema.option("debit_card", "Debit card payment"),
Schema.option("mobile_payment", "Mobile or digital wallet payment"),
]),
Schema.boolean("contains_alcohol", "Whether any alcoholic beverage was included"),
])
.create()
)
result = workflow.run("receipt.jpg").wait()
print(result.fields["store_name"].value)
print(result.fields["total_amount"].value)
print(result.fields["contains_alcohol"].value)
```
## Tips
* For receipt photos, good lighting and a flat surface produce significantly better results.
* PNG images generally extract better than compressed JPG. If quality matters, prefer PNG.
* `enum` with explicit options constrains the output to known values, preventing free-text variations like "Visa" vs "credit card" vs "CC".
## Next steps
The unified JSON response shape
Handle rate limits and retries in production
# Resume Parsing
Source: https://docs.anyformat.ai/guides/recipes/resume-parsing
Parse candidate data from DOC/DOCX resumes
## Workflow fields
We recommend creating this workflow in the [anyformat platform](https://app.anyformat.ai) where you can test with sample resumes and iterate on field descriptions. Copy the workflow ID to use with the API.
| Field | Type | Description |
| --------------------- | ------- | ---------------------------------------------------------------- |
| `candidate_name` | string | Full name of the candidate |
| `email` | string | Email address |
| `phone` | string | Phone number |
| `skills` | list | Technical and professional skills |
| `years_of_experience` | integer | Total years of professional experience |
| `education` | object | Educational background (institution / degree / graduation\_date) |
| `work_history` | object | Previous employment (company / title / start\_date / end\_date) |
## End-to-end
```bash theme={null}
# 1. Create the workflow
curl -X POST 'https://api.anyformat.ai/v2/workflows/' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-d '{
"name": "Resume Parser",
"description": "Extract candidate details from resumes",
"nodes": [
{"id": "parse_1", "type": "parse"},
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{"name": "candidate_name", "description": "Full name of the candidate", "data_type": "string"},
{"name": "email", "description": "Email address", "data_type": "string"},
{"name": "phone", "description": "Phone number including country code if present", "data_type": "string"},
{"name": "skills", "description": "List of technical skills, programming languages, tools, and competencies", "data_type": "list"},
{"name": "years_of_experience", "description": "Total years of professional work experience", "data_type": "integer"},
{
"name": "education",
"description": "Educational qualifications and degrees",
"data_type": "object",
"nested_fields": [
{"name": "institution", "description": "University or school name", "data_type": "string"},
{"name": "degree", "description": "Degree obtained (e.g. BSc CS, MBA)", "data_type": "string"},
{"name": "graduation_date", "description": "Date of graduation", "data_type": "date"}
]
},
{
"name": "work_history",
"description": "Previous jobs and roles, most recent first",
"data_type": "object",
"nested_fields": [
{"name": "company", "description": "Company name", "data_type": "string"},
{"name": "title", "description": "Job title", "data_type": "string"},
{"name": "start_date", "description": "Start date of employment", "data_type": "date"},
{"name": "end_date", "description": "End date of employment, or empty if current", "data_type": "date"}
]
}
]
}
}
],
"edges": [{"source": "parse_1", "target": "extract_1"}]
}'
# 2. Run a resume
curl -X POST 'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/run/' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-F 'file=@resume.docx'
# 3. Poll for results (412 → keep polling; 200 → done)
curl -H "Authorization: Bearer $ANYFORMAT_API_KEY" \
'https://api.anyformat.ai/v2/workflows/WORKFLOW_ID/files/COLLECTION_ID/results/'
```
```typescript theme={null}
import { Anyformat, Schema } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
const file: File = /* a File with .name set, e.g. new File([bytes], "resume.docx") */;
const workflow = await af
.workflow("Resume Parser", "Extract candidate details from resumes")
.parse()
.extract([
Schema.string("candidate_name", "Full name of the candidate"),
Schema.string("email", "Email address"),
Schema.string("phone", "Phone number including country code if present"),
Schema.string("skills", "List of technical skills, programming languages, tools, and competencies"),
Schema.integer("years_of_experience","Total years of professional work experience"),
Schema.object("education", "Educational qualifications and degrees", [
Schema.string("institution", "University or school name"),
Schema.string("degree", "Degree obtained (e.g. BSc CS, MBA)"),
Schema.date("graduation_date", "Date of graduation"),
]),
Schema.object("work_history", "Previous jobs and roles, most recent first", [
Schema.string("company", "Company name"),
Schema.string("title", "Job title"),
Schema.date("start_date", "Start date of employment"),
Schema.date("end_date", "End date of employment, or empty if current"),
]),
])
.create();
const run = await workflow.run(file);
const result = await run.wait();
console.log(result.field("candidate_name")?.value);
console.log(result.field("email")?.value);
console.log(result.field("years_of_experience")?.value);
// Nested rows: result.extractions[0].fields.work_history
```
```python theme={null}
import os
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
workflow = (
client.workflow("Resume Parser")
.parse()
.extract([
Schema.string("candidate_name", "Full name of the candidate"),
Schema.string("email", "Email address"),
Schema.string("phone", "Phone number including country code if present"),
Schema.string("skills", "List of technical skills, programming languages, tools, and competencies"),
Schema.integer("years_of_experience","Total years of professional work experience"),
Schema.object("education", "Educational qualifications and degrees", fields=[
Schema.string("institution", "University or school name"),
Schema.string("degree", "Degree obtained (e.g. BSc CS, MBA)"),
Schema.date("graduation_date", "Date of graduation"),
]),
Schema.object("work_history", "Previous jobs and roles, most recent first", fields=[
Schema.string("company", "Company name"),
Schema.string("title", "Job title"),
Schema.date("start_date", "Start date of employment"),
Schema.date("end_date", "End date of employment, or empty if current"),
]),
])
.create()
)
result = workflow.run("resume.docx").wait()
print(result.fields["candidate_name"].value)
print(result.fields["email"].value)
print(result.fields["years_of_experience"].value)
# Nested rows: result.raw["extractions"][0]["fields"]["work_history"]
```
## Tips
DOCX resumes typically yield better results than scanned PDFs since the text is natively accessible.
* Describe `end_date` as "empty if current role" so processing returns `null` for current positions.
* `years_of_experience` as `integer` lets you filter directly without parsing.
## Next steps
All input methods: file upload and text
Lists, objects, and other field shapes
# Using Studio
Source: https://docs.anyformat.ai/guides/studio/index
Open Studio, add steps to the canvas, connect them, and build workflows that go beyond Parse → Extract.
**[Studio](/concepts/studio)** is the visual editor for building workflows by hand. You add the steps you need onto a canvas and connect them, then configure each one.
Use Studio when the simple "describe it" flow isn't enough — for example when you need to **sort** documents by type, **split** a file that holds several documents, or **check** results against your own rules. For a plain Parse → Extract workflow, [describing what you want](/guides/workflows/creating) is faster.
***
## Opening Studio
* **On an existing workflow:** open the workflow and click the **Studio** tab.
* **Starting fresh:** on the new-workflow screen, click **Create from Studio**. You begin with a single **Parse** step and an otherwise blank canvas.
***
## The layout
Studio has three areas:
* **Add Stages** (left) — the palette of steps you can drag onto the canvas.
* **Canvas** (center) — your workflow: the steps and the connections between them.
* **Configuration panel** (right) — opens when you click a step, where you set it up (define fields, categories, rules, and so on).
***
## Working with steps
**Add a step** — drag it from the **Add Stages** panel on the left onto the canvas and drop it where you want it. Every workflow needs a **Parse** step first; it's added for you when you create from Studio.
**Open and configure a step** — click a step on the canvas. Its configuration panel opens on the right, where you set it up: the fields for Extract, the categories for Classify, the rules for Split or Validate. Click empty canvas (or another step) to switch away.
**Move a step** — drag it around the canvas to keep things tidy. Position is just for readability; it doesn't change how the workflow runs.
**Remove a step** — hover over the step and click the **⋯** (*Node options*) button in its corner, then choose **Remove**.
***
## Working with connections
Connections set the **order** the steps run in — the output of one step flows into the next.
**Make a connection** — drag from the small handle on the right edge of a step to the handle on the left edge of the next step. For example, drag from **Parse** to **Extract** so the text Parse produces flows into Extract.
**Remove a connection** — hover over the connecting line and click the **×** button that appears on it.
### What can connect to what
The steps only connect in ways that make sense, so you can't build a broken workflow:
* **Parse** is always the first step. Nothing connects *into* it, and there's only **one** Parse per workflow.
* **Parse** can connect to **Extract**, **Classify**, or **Split**.
* **Classify** and **Split** connect onward to **Extract** — one Extract per category or split rule, so each type is handled on its own.
* **Extract** can connect to **Validate**.
* **Validate** is an end step — nothing connects out of it.
Each output connects to one next step. If you need a step to feed several others (like a different Extract per document type), that's what **Classify** and **Split** are for — they branch automatically into one output per category.
***
## Run and refine
Once your steps are connected and configured, add a sample document and run it, check the results, and adjust. Publish when it looks right. See **[The building blocks](/guides/studio/nodes)** for what each step does and how to configure it.
***
## Common shapes
The same steps arranged differently cover most needs:
### Read only
Just **Parse**. You get clean text and tables back — useful when you want to feed anyformat's output into your own tools rather than pulling out specific fields.
```
[ Parse ]
```
### Read, then pull out
**Parse → Extract**. The default: read the document and return the fields you defined.
```
[ Parse ] → [ Extract ]
```
### Sort first, then pull out
**Parse → Classify**, then a separate **Extract** for each type. Use this when one workflow needs to handle several kinds of document differently. **Split** works the same way for files that hold several documents stapled together.
```
┌──> [ Extract: invoice ]
[ Parse ] → [ Classify ] ─┤
└──> [ Extract: receipt ]
```
### Add a check at the end
Put a **Validate** step after Extract to flag results that break your rules (an expired date, a malformed ID) before you trust them.
```
[ Parse ] → [ Extract ] → [ Validate ]
```
***
## What's next?
What each step does and how to set it up
Review and verify the results your workflow produces
# The building blocks
Source: https://docs.anyformat.ai/guides/studio/nodes
What each Studio step does, what you configure, and when to add it.
A workflow is made of five steps. Most workflows only use the first two — **Parse** and **Extract**. The other three are there for when your documents are more complicated.
***
## Parse
**Reads the document.** Parse turns your file (PDFs, images, CSVs, and more) into clean text and tables that the rest of the workflow can work with. Every workflow starts with a Parse step.
* **You configure:** usually nothing — Parse works out of the box. Advanced options let you give a hint about the document or tune how figures are handled.
* **Add it when:** always. It's the first step in every workflow.
> *Example:* upload a bank statement PDF — Parse reads and prepares all the text and layout.
***
## Extract
**Pulls out the fields you ask for.** Extract takes the text from Parse and returns structured data according to the [fields](/concepts/fields) you define.
* **You configure:** the list of fields — each with a name, a [type](/concepts/field-types), and an instruction. This is your [schema](/concepts/schemas).
* **Add it when:** you want specific values back (the usual case). Skip it only for a parse-only workflow.
> *Example:* define fields like "Account number", "Transaction date", and "Amount" to get them from every statement.
***
## Classify
**Sorts documents into types you define.** Classify looks at each document and labels it as one of the categories you set up, so you can handle each type differently downstream.
* **You configure:** a list of categories, each with a name and a short description of what belongs in it.
* **Add it when:** one workflow needs to handle several kinds of document — for example a mailbox that receives both invoices and contracts. After Classify, connect a separate Extract step for each category.
> *Example:* automatically label incoming files as "Invoice", "Contract", or "Statement".
***
## Split
**Breaks one file into several documents.** Split takes a single file that actually contains multiple documents and separates it into pieces, so each piece is processed on its own.
* **You configure:** the split rules, each with a name and a description. Anything that doesn't match a rule is grouped as "other".
* **Add it when:** your uploads bundle several documents together — for example a single PDF with four invoices, or a 50-page report you want handled page by page.
> *Example:* a 50-page report gets split so each section is processed separately.
***
## Validate (Beta)
**Checks the results against rules you write.** Validate takes the data from an Extract step and tests it against rules you describe in plain language, then flags anything that fails — so problems surface automatically instead of slipping through.
**Validate vs. verify — two different things.** The **Validate** step is an automatic check built into the workflow: anyformat runs your rules on every document. **Verifying** is something *you* do by hand afterward — marking a value as correct with the thumbs-up (see [Verification & review](/guides/workflows/verification-review)). One is the machine checking your rules; the other is you confirming the result.
* **You configure:** a list of rules. Each rule is one of two kinds — an **AI** rule you describe in plain language (for example, "the IBAN is a valid format" or "the document is not expired"), or a **deterministic** rule you build from a structured check (a number/date in range, `a + b = c`, a value in a set, a pattern match, a required field) that runs instantly with no AI. You can mix both in the same step.
* **Add it when:** some results must meet conditions, and you want failures flagged automatically. Place Validate after the Extract step whose output you want to check.
* **Where results show up:** failures appear in the **Validation** tab of the results, alongside the extracted data.
> *Example:* check that the name on an ID matches the name on the contract (AI), confirm the invoice year is between 1995 and 2050, or that subtotal + tax equals the total (deterministic).
See **[Validation rules](/guides/studio/validate-rules)** for the full list of deterministic checks and when to choose AI vs deterministic.
***
## What's next?
Open Studio, add steps, and connect them
The kinds of values an Extract step can return
# Validation rules
Source: https://docs.anyformat.ai/guides/studio/validate-rules
Check extracted data with AI rules or instant deterministic checks — in Studio, the API, or the SDKs.
A **Validate** step runs a list of rules over the data an Extract step produced and flags anything that fails. Every rule is one of two kinds, and a single Validate step can mix both.
| Rule kind | How it's checked | Best for | Cost |
| ----------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------- |
| **AI** | A model reads your plain-language description and judges the data | Fuzzy / semantic conditions ("the vendor looks like a real company", "the two names refer to the same person") | Billed per rule |
| **Deterministic** | A structured check runs in plain code — no model | Exact, testable conditions (a number in range, `a + b = c`, a value in a set, a pattern, a required field) | **Free** |
Reach for a **deterministic** rule whenever the condition is something you could check with a calculator or a lookup — it's instant, free, and never flakes. Use an **AI** rule when the condition needs judgement or language understanding.
## Deterministic checks
Each deterministic rule is built from one check. Fields are referenced by name (the field name from the upstream Extract step).
| Check | What it asserts | Example |
| ------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------- |
| **Number in range** | A numeric field falls within bounds | `amount` between `0` and `1000000` |
| **Date in range** | A date field falls within bounds — each an ISO date or `today` (relative) | `expiry_date` on or after `today` (not expired) |
| **Arithmetic** | A sum / difference / product of fields equals another field, within a tolerance | `subtotal + tax = total` (±0.01) |
| **Comparison** | A field compares (`=`, `≠`, `>`, `≥`, `<`, `≤`) to a fixed value or another field | `end_date ≥ start_date` |
| **One of** | A field's value is in an allowed set | `currency` is one of `EUR`, `USD`, `GBP` |
| **Pattern** | A field matches a regular expression | `iban` matches `^[A-Z]{2}\d{2}` |
| **Required** | A field is present and not empty | `vendor_name` is filled in |
Each check resolves to **pass**, **fail**, or **inconclusive** (when the referenced field is missing or can't be read as the expected type). Numbers are read leniently — `"€1.234,56"` and `"1,234.56"` both parse — and dates accept the common written formats.
## Build a rule in Studio
1. Add a **Validate** step after the Extract step whose output you want to check.
2. Open the step, go to the **Rules** tab, and click **Add rule**.
3. Choose **AI** or **Deterministic** with the toggle.
* **AI** — type the rule in plain language; reference a field with the field picker.
* **Deterministic** — pick a check type, choose the field(s), and enter the values.
4. Set a **severity** (`error` or `warning`) and save.
Failures appear in the **Validation** tab of the results, next to the extracted data.
## Author rules via the API
Validation rules are part of the `validate` node in the typed-graph create body — no separate endpoint. A deterministic rule carries a `check`; an AI rule carries a `description`. Reference fields by their **name** as defined on the upstream extract.
```json theme={null}
{
"name": "Invoice with checks",
"nodes": [
{ "id": "parse_1", "type": "parse" },
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{ "name": "invoice_date", "description": "Invoice date.", "data_type": "date" },
{ "name": "subtotal", "description": "Subtotal before tax.", "data_type": "float" },
{ "name": "tax", "description": "Tax amount.", "data_type": "float" },
{ "name": "total", "description": "Grand total.", "data_type": "float" },
{ "name": "currency", "description": "ISO currency code.", "data_type": "string" }
]
}
},
{
"id": "validate_1",
"type": "validate",
"rules": [
{
"id": "not-expired",
"kind": "deterministic",
"severity": "error",
"check": { "type": "date", "field": "invoice_date", "earliest": "2000-01-01", "latest": "today" }
},
{
"id": "totals-add-up",
"kind": "deterministic",
"severity": "error",
"check": { "type": "arithmetic", "operands": ["subtotal", "tax"], "operator": "sum", "equals": "total", "tolerance": 0.01 }
},
{
"id": "known-currency",
"kind": "deterministic",
"severity": "warning",
"check": { "type": "one_of", "field": "currency", "allowed": ["EUR", "USD", "GBP"] }
},
{
"id": "vendor-legit",
"kind": "ai",
"severity": "warning",
"description": "The vendor is a real, named company."
}
]
}
],
"edges": [
{ "source": "parse_1", "target": "extract_1" },
{ "source": "extract_1", "target": "validate_1" }
]
}
```
### Check payloads
```jsonc theme={null}
{ "type": "range", "field": "amount", "min": 0, "max": null }
// Date bounds: an ISO date "YYYY-MM-DD", "today" (relative), or null.
{ "type": "date", "field": "expiry_date", "earliest": "today", "latest": null } // not expired
{ "type": "date", "field": "invoice_date", "earliest": "2024-01-01", "latest": "2024-12-31" }
{ "type": "arithmetic", "operands": ["subtotal", "tax"], "operator": "sum", "equals": "total", "tolerance": 0.01 }
{ "type": "comparison", "left": "end_date", "op": ">=", "right": { "source": "field", "field": "start_date" } }
{ "type": "comparison", "left": "year", "op": ">=", "right": { "source": "literal", "value": 2000 } }
{ "type": "one_of", "field": "currency", "allowed": ["EUR", "USD", "GBP"], "case_sensitive": false }
{ "type": "regex", "field": "iban", "pattern": "^[A-Z]{2}\\d{2}" }
{ "type": "required", "field": "vendor_name" }
```
`operator` defaults to `sum`; `tolerance` to `0`; `case_sensitive` to `false`. Unset date bounds are `null`.
## Author rules with the SDK
The check classes are exported from the workflow package; pass `ValidationRule`s to `.validate()`.
```python Python theme={null}
from anyformat.workflow import (
Schema, ValidationRule,
DateCheck, ArithmeticCheck, OneOfCheck,
)
wf = (
client.workflow("Invoice with checks")
.parse()
.extract([
Schema.date("invoice_date", "Invoice date."),
Schema.float("subtotal", "Subtotal before tax."),
Schema.float("tax", "Tax amount."),
Schema.float("total", "Grand total."),
Schema.string("currency", "ISO currency code."),
])
.validate(
ValidationRule(id="not-expired", kind="deterministic", severity="error",
check=DateCheck(type="date", field="invoice_date", earliest="2000-01-01", latest="today")),
ValidationRule(id="totals-add-up", kind="deterministic", severity="error",
check=ArithmeticCheck(type="arithmetic", operands=["subtotal", "tax"], equals="total", tolerance=0.01)),
ValidationRule(id="known-currency", kind="deterministic", severity="warning",
check=OneOfCheck(type="one_of", field="currency", allowed=["EUR", "USD", "GBP"])),
# An AI rule still works the same way:
ValidationRule(id="vendor-legit", description="The vendor is a real, named company."),
)
.create()
)
```
```typescript TypeScript theme={null}
const wf = await client
.workflow("Invoice with checks")
.parse()
.extract([
Schema.date("invoice_date", "Invoice date."),
Schema.float("subtotal", "Subtotal before tax."),
Schema.float("tax", "Tax amount."),
Schema.float("total", "Grand total."),
Schema.string("currency", "ISO currency code."),
])
.validate([
{ id: "not-expired", kind: "deterministic", severity: "error",
check: { type: "date", field: "invoice_date", earliest: "2000-01-01", latest: "today" } },
{ id: "totals-add-up", kind: "deterministic", severity: "error",
check: { type: "arithmetic", operands: ["subtotal", "tax"], operator: "sum", equals: "total", tolerance: 0.01 } },
{ id: "known-currency", kind: "deterministic", severity: "warning",
check: { type: "one_of", field: "currency", allowed: ["EUR", "USD", "GBP"] } },
{ id: "vendor-legit", kind: "ai", description: "The vendor is a real, named company." },
])
.create();
```
Rules referencing a field by name require that field to exist on the upstream Extract step. The Studio also lets you reference fields you haven't saved yet by their persistent id — both forms resolve at run time.
## What's next?
The five building blocks, including Validate.
AI rules bill per rule; deterministic rules are free.
# Analytics & Quality
Source: https://docs.anyformat.ai/guides/workflows/analytics-quality
Understanding and improving processing quality over time
Analytics help you answer one practical question: **can I trust this workflow's results before I rely on them?**
Once you've verified some documents, the **Analytics** tab in each workflow shows you how well it's performing — how reliable the results are, where errors tend to appear, and which fields need attention.
**The short version:** anyformat gives every value a **confidence** score so you know where to look — review the low-confidence ones first. Over time, your verifications produce an **accuracy** number that tells you how often the workflow is actually right. Aim for high accuracy with light review on the low-confidence cases — you don't need to check everything.
The rest of this page explains both numbers in detail.
***
## Confidence
### What is confidence?
**Confidence** represents how certain anyformat is about an extracted value.
It's expressed as a percentage:
* **High confidence** - the model is very sure
* **Low confidence** - the value may be ambiguous or unclear
Confidence is calculated per:
* Field
* Document
* Workflow (average)
***
### What confidence is (and isn't)
* A **signal**, not a verdict
* A way to prioritize human review
* A guide for where to look first
* A guarantee of correctness
* A replacement for verifying results yourself
* A measure of business accuracy
A value can have:
* High confidence and still be wrong
* Low confidence and still be correct
***
### How to use confidence effectively
Use confidence to:
* Focus review on low-confidence fields
* Skip reviewing obviously reliable values
* Reduce overall human effort
A good workflow doesn't eliminate low confidence — it **contains it**.
***
## Accuracy explained
### What is accuracy?
**Accuracy** measures how often extracted values are actually **correct**, based on the documents you've verified.
> Accuracy reflects confirmed correctness, not how sure the model felt.
Accuracy is calculated from:
* Fields you verified as correct
* Fields you corrected
***
### Accuracy vs confidence
| Confidence | Accuracy |
| ------------------------------------------------ | --------------------------------- |
| How sure the model is | How often it's confirmed right |
| Available immediately, before you check anything | Builds up as you verify documents |
| A guess about each value, up front | A track record, after the fact |
| Helps you prioritize what to review | Measures real performance |
You need **both**:
* Confidence to guide review
* Accuracy to judge quality
***
### What accuracy tells you
Accuracy helps you answer:
* Can I trust this workflow?
* Is it ready to scale?
* Which fields are fragile?
Low accuracy usually points to:
* Ambiguous instructions
* Poor field definitions
* Edge cases in documents
***
## Viewing analytics
### Workflow-level analytics
In the **Analytics** tab of a workflow, you can see:
* Average confidence
* Average accuracy
* Trends over time (if available)
This gives you a high-level sense of workflow health.
***
### Field-level analytics
You can also analyze metrics **per field**:
* Average confidence by field
* Accuracy by field
* Sort fields by performance
This is often the most useful view. It helps you quickly spot:
* Fields that consistently fail
* Fields that don't need review anymore
* Outliers dragging accuracy down
***
## Improving results
### How to improve confidence
To improve confidence:
* Make instructions more explicit
* Clarify where information appears
* Reduce ambiguity in field definitions
* Split complex fields into simpler ones
Confidence improves when field definitions become clearer.
***
### How to improve accuracy
To improve accuracy:
* Correct wrong values while verifying
* Review low-confidence fields carefully
* Refine the workflow when patterns appear
* Adjust fields or instructions if needed
Accuracy improves through **human feedback loops**.
***
### When to refine the workflow
You should consider refining a workflow when:
* The same field is often corrected
* Accuracy plateaus below expectations
* New document variations appear
Refinement improves **future documents**, not past ones.
***
## A realistic quality goal
You don't need:
* 100% confidence
* 100% accuracy
A good goal is:
> High accuracy with focused human review on low-confidence cases.
That's how anyformat scales without burning time.
***
## How Analytics fits into the bigger picture
**Verification** tells you *what is correct now*.
**Analytics** tells you *how good the system is overall*.
Together, they help you:
* Decide where to spend time
* Decide when to scale
* Decide when a workflow is "good enough"
***
## What's next?
Improve field definitions and instructions to raise accuracy
End-to-end examples for common document types
# Creating Workflows
Source: https://docs.anyformat.ai/guides/workflows/creating
How to create and configure workflows in anyformat
There are **two ways to create a workflow**, plus a head-start option:
* **Describe it** *(recommended for most people)* — type what you want in plain language and anyformat builds a Parse → Extract workflow for you (Parse reads the document, Extract pulls out the fields), with suggested fields.
* **Build it yourself in [Studio](/guides/studio/index)** — arrange the steps visually on a canvas. Use this when you need more than Parse → Extract (sorting documents by type, splitting multi-document files, validation rules).
* **Start from a template** — begin from a ready-made workflow for a common document type and tweak it. See [Templates](#starting-from-a-template) below.
This page covers the **describe it** flow. For the visual editor, see the [Studio guide](/guides/studio/index).
***
## Describe it (from the home screen)
The easiest way to start is from the **home** screen, where you'll find a text box similar to a chat interface.
Here, you can:
* Describe the type of document you want to process
* Explain what you want to extract
* Upload a sample document
You can upload documents by:
* Dragging and dropping
* Clicking **Add document**
**Our recommendation:** Provide both a description *and* a sample document. This gives the best starting point.
When you're ready, click the **send icon** (the arrow button in the box) to start. anyformat creates a workflow that reads your document and suggests the fields to extract.
***
## Starting from a template
If your document is a common type, you can start from a **template** instead of describing everything from scratch. A template is a ready-made workflow with the fields already defined for that document type — for example an invoice (vendor, line items, taxes, totals, due dates, currency).
When you pick a template:
1. A new workflow is created with the fields already filled in.
2. You adjust the fields to match your exact needs (add, remove, or rename).
3. From there you follow the same Define → Refine → Publish flow as any other workflow.
Templates just save you the setup — everything stays editable. See [Versions & templates](/guides/workflows/versions-templates) for the full list of available templates.
***
## The workflow lifecycle
Every workflow goes through **three steps**:
Define what data you want to extract
Check and improve quality
Make the workflow available for use at scale
You always move in this order.
***
## Step 1: Define
This is where you define **what data you want**.
### The workspace layout
During definition, the screen is split into two areas:
* Displays the uploaded document
* Shows one document at a time
* Supports multi-page documents
You can scroll through pages, switch pages, zoom in/out, and search within the document.
* Add fields manually
* Ask AI to suggest fields
* Edit existing fields
Each field has a name, a type, and instructions.
***
### AI suggestions
Suggestions are generated based on:
* The prompt
* The document
* Or both
You can:
* Add suggested fields
* Edit them later
* Ignore them completely
Suggestions are optional helpers.
***
### Adding fields manually
To add a field:
1. Click **New field**
2. Choose:
* Field name
* Field type *(see [Field types](/concepts/field-types))*
* Instructions (or let AI generate them from the name)
You can also:
* Duplicate fields
* Delete fields (from the three-dot menu on the field card)
***
### Special field types
For some field types:
* **Select / Multiselect** - define option names and descriptions
* **Object (Subtable)** - define subfields
When editing a field:
* Click the field
* View its properties in the card preview
* See options or subfields directly
***
### Before moving on
Before continuing:
* Make sure all fields are **saved**
* You can edit fields later, but saving now avoids issues
***
## Step 2: Refine
This step is about **checking and improving quality**.
### Running the workflow
* Processing runs automatically
* It may take a few moments to complete
***
### Reviewing results
You can review results using:
* The document view
* The Markdown representation
Markdown helps verify that parsing worked correctly.
***
### Visual grounding
Each extracted field is linked to its location in the document:
* Highlighted in a single color in the document viewer
* Clicking a field highlights it in the document — and clicking an area in the document highlights the corresponding field
This helps you understand **where values came from**.
***
## Step 3: Publish
Once you've reviewed the results and they look correct:
* You can publish the workflow
After publishing:
* Upload additional documents
* Apply the workflow at scale
* Start generating data
***
### After publishing
Published workflows:
* Appear in the home
* Appear in the Workflows section
* Can be reused and run multiple times
***
## What's next?
Learn how to run workflows on documents
Review and verify extracted data at scale
# Running Workflows
Source: https://docs.anyformat.ai/guides/workflows/running
How to run workflows on documents in anyformat
Once a workflow is published, you can run it on documents to extract structured data.
***
## Adding documents
Documents can be added to a workflow:
* **Manually** - Upload through the interface
* **Via API** - Send documents programmatically
* **Cloud storage** - Connect to Google Drive or Microsoft SharePoint
***
## Document statuses
Each document has a status that reflects its state:
| Status | Description |
| --------------- | --------------------------------------------------------------------------- |
| **Not started** | The workflow has not been run on this document yet |
| **Queued** | The document is waiting for an available processing slot |
| **In progress** | anyformat is currently processing the document |
| **Processed** | The document has been processed, but not yet reviewed |
| **Verified** | The extracted data has been reviewed and confirmed correct (shown in green) |
| **Error** | There was an error during processing (not counted toward usage) |
| **Cancelled** | Processing was cancelled before it finished |
***
## Running a workflow
There are two ways to start processing:
* **In bulk, from the table:** select the documents that haven't been processed and click **Run**.
* **One at a time, from the document view:** open a document (click its filename in the table) and click **Process document** in the panel on the right. If a document errored, the same button reads **Process again** so you can retry it.
Processing usually takes anywhere from a few seconds for a short document to a couple of minutes for a long one. It depends on:
* Document size
* Number of pages
* Current system load
If a document ends up in **Error**, open it and click **Process again** to retry. Errors are usually temporary or caused by an unreadable file — errored documents aren't counted toward usage. If it keeps failing, check that the file isn't corrupted or password-protected.
***
## Viewing results
As soon as a document finishes processing, its results appear automatically — no need to refresh. You can view them two ways:
Review all your documents at once. The first tab shows your documents (one row per document, one column per field); additional tabs show the output of each step in your workflow (Parse, Classify, Split, Extraction, Validation).
Open one document (click its filename) to review it on its own, with each value highlighted on the page it came from. It has the same per-step tabs as the table view.
***
## Exporting results
From the workflow view, you can download processed results in:
* **CSV** - For spreadsheets and data analysis
* **Excel** - For business users
* **JSON** - For integrations and automation
You can export:
* All documents at once
* Selected documents
* Individual documents (row-level export)
***
## What's next?
Learn how to verify and correct extracted data
Monitor and improve quality
# Verification & Review
Source: https://docs.anyformat.ai/guides/workflows/verification-review
Reviewing and verifying extracted data at scale
Once a workflow has been created and documents have been processed, the next step is to **review and verify the extracted data** — confirm it's correct, and fix anything that isn't.
This section is about:
* Reviewing results
* Verifying data
* Correcting outputs
* Preparing data for export or use
***
## Two things that sound alike
It's easy to confuse these, so to be clear:
Done while building — improves how data is extracted **going forward**, on every future document.
Done after processing — confirms whether the data extracted from a **specific document** is correct.
If results aren't good enough, you can always go back and **refine the workflow**.
anyformat learns from your verifications and corrections over time to improve future results.
This section focuses on **verifying results**, not on editing the workflow.
***
## Table view
### Reviewing data at scale
The **table view** lets you review all extracted data across documents.
The first tab shows your documents, where you'll see:
* One row per document
* One column per field in the schema
* Metadata such as creation date, processed date, and who verified it
Alongside the documents tab, there's a tab for each step in your workflow (Parse, Classify, Split, Extraction, Validation), so you can review the output of each stage on its own.
***
### Table actions
From the table view, you can:
* Run the workflow on unprocessed documents
* Download processed results (CSV, Excel, JSON)
* Delete selected documents
* Open a document in the document view (click its filename)
* Download results per document (row-level export)
You can select one or multiple documents to apply actions.
***
### Working with columns
In the table view you can:
* Reorder columns
* Resize columns
* Sort by any column
* Filter by any column
* Search by document or field value
This makes it easy to spot errors, find edge cases, and focus review where it matters.
***
### Objects in table view
Fields of type **Object (Subtable)** — like invoice line items — appear as their own table. Expand the object to see every row laid out together, so you can review repeated data on its own.
***
## Document view
### Reviewing one document at a time
To open a single document, **click its filename** in the table. This opens the **document view**, designed for detailed, per-document review.
The layout is similar to the workflow creation view:
* Document viewer on one side
* Extracted fields on the other
It has the same per-step tabs as the table view (Parse, Classify, Split, Extraction, Validation), so you can check each stage for that document. The key difference from the build screen:
> Here, you are verifying final results, not changing the workflow.
***
### Visual grounding and evidence
Each extracted value is visually linked to the document:
* A single color highlights where the value appears in the document
* Clicking a field highlights it in the document — and clicking an area in the document highlights the corresponding field
This helps you understand *why* a value was extracted.
***
### Confidence indicators
Each field shows a **confidence score** — a quick signal of how sure anyformat is about that value.
* Shown as a percentage with a colored dot
* Explained on hover
* Low-confidence values are the ones worth checking first
The goal is to reduce your effort by telling you where to look. See [Analytics & quality](/guides/workflows/analytics-quality) for what confidence does and doesn't mean.
***
## Verifying results
When a value is correct, you mark it as **verified** with the thumbs-up button. You can verify:
* Individual fields
* Entire documents
* Multiple documents at once
A verified field turns green. To undo, press the thumbs-up again (or use **Undo** after an edit). As you verify, the document's status updates to **Verified**.
***
### Keyboard shortcuts
To speed up review:
| Shortcut | Action |
| --------------- | ------------------------------------ |
| `Shift + ← / →` | Navigate between documents |
| `Ctrl + Enter` | Verify the current document |
| `Shift + Enter` | Verify and move to the next document |
***
## Correcting outputs
When a value is incorrect, just edit it. The corrected value is saved as the verified result, so what you export reflects your fix.
For **Object (Subtable)** fields you can verify individual rows or the whole object. You can also add new rows, or filter to show only the rows visible on the current page.
***
## What's next?
Monitor and improve quality over time
Learn about export formats and using data externally
# Versions & Templates
Source: https://docs.anyformat.ai/guides/workflows/versions-templates
Workflow versioning and document templates to get started faster
## Workflow versions
anyformat automatically tracks changes to your workflows through versioning.
### How versioning works
Every time you edit a workflow, a new version is created automatically. This means:
* Your previous configurations are preserved
* You can see the history of changes
* Each version captures the complete workflow state
### What creates a new version?
* Adding, removing, or modifying fields
* Changing the schema
* Updating workflow configuration
### Version history
Versions form a chain, allowing you to:
* Track how your workflow evolved
* Understand what changed between versions
* Maintain a complete audit trail
### Why this matters
Versioning is your safety net. For example: say you edit an invoice workflow so the "total" field excludes VAT — and the next batch of results suddenly looks wrong. Because the previous version was saved automatically, you can look back at the history and return to the version that was working, instead of trying to rebuild it from memory.
Editing a workflow only affects **future** runs. Documents you've already processed keep the results from the version they were run on — changing the workflow doesn't silently rewrite past data.
***
## Document templates
Document templates are pre-configured starting points for common document types.
They represent:
* A known document category
* A partially defined schema
***
## Available templates
When you create a workflow, you can start from one of these ready-made templates:
| Template | What it extracts |
| ------------------------------------------ | ---------------------------------------------------------- |
| **Invoice** | Vendor, line items, taxes, totals, due dates, and currency |
| **Environmental Product Declaration** | Parts list, material composition, and environmental impact |
| **Product Requirements** | Parts list, material composition, and specifications |
| **Modelo 200 (Impuesto sobre Sociedades)** | Data from the Spanish corporate income tax return |
If your document type isn't listed, just [describe it](/guides/workflows/creating) instead — templates are a convenience, not the only way in.
Templates save time, but behave like normal workflows once created.
***
## How templates work
When you use a template:
1. A new workflow is created with pre-defined fields
2. You can modify the schema as needed
3. The workflow follows the same lifecycle (Define → Refine → Publish)
Templates are just a faster starting point — you're not locked into anything.
***
## When to use templates
Use templates when:
* You're working with a common document type
* You want to get started quickly
* You're not sure what fields to define
Use custom workflows when:
* Your document type is unique
* You need specific fields not in templates
* You want full control from the start
***
## What's next?
Learn the full workflow creation process
Monitor and improve quality
# Changelog
Source: https://docs.anyformat.ai/changelog/overview
Stay up to date with the latest changes and improvements to the anyformat API.
## July 2026
### July 7, 2026
**\[New + API]** API v3 launched — document packets and runs as first-class resources
The public API's new stable major is live under `/v3/`, rebuilt around three resources: **workflows**, **[document packets](/concepts/document-packets)** (the runnable bundle of 1+ files — v2's "file collection", renamed and promoted), and **[runs](/concepts/runs-and-results)** (one execution attempt of a workflow against a packet, with its own id, status, and results). The full reference lives in the new [v3 API tree](/api-reference-v3/introduction); v2 users can follow the [path-by-path migration guide](/api-reference-v3/migrating-from-v2). Highlights:
* **Flat run reads, no more 412-polling** — [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get) always returns `200` with a `status` field; `results` carries the (unchanged) results envelope inline once `status` reaches `processed`. The `/results/` sub-path is gone.
* **One upload shape** — [`POST .../upload/`](/api-reference-v3/workflows/upload) groups 1–10 files into a single packet atomically, [`.../upload/run/`](/api-reference-v3/workflows/upload-and-run) does upload + run in one call and returns a `run_id`, and [`.../upload/from-url/`](/api-reference-v3/workflows/upload-from-url) imports 1–10 HTTPS URLs all-or-nothing.
* **Re-run without re-uploading** — [`POST /v3/document-packets/{id}/run/`](/api-reference-v3/document-packets/run) creates a fresh run each call; earlier runs stay readable.
* **The workflow read is the definition** — [`GET /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/get) returns the complete typed graph with a `persistent_id` per field, and [`PATCH`](/api-reference-v3/workflows/update) echoes that shape back: echo the `persistent_id` and a renamed field keeps its identity (analytics, ground truth, and quality metrics stay attached). There is no separate `/definition/` endpoint and no `PUT` in v3.
* **Keyset pagination everywhere** — every list returns `{items, next_cursor}` with `?limit=` (max 100) and `?cursor=`; no totals or page numbers.
* **Idempotent submission** — the upload and run-trigger POSTs accept an [`Idempotency-Key` header](/api-reference-v3/introduction#idempotency); a retry with the same key replays the original response instead of creating a duplicate packet or billing a second extraction.
* **`X-API-Version: 3.0.0`** is stamped on every `/v3/` response.
v2 is unchanged and keeps serving traffic through its announced deprecation window (see the July 3 entry below). Authentication, the error envelope, the results envelope, webhooks, and rate limits are identical across both versions.
### July 6, 2026
**\[Changed + API]** Dataset membership moves to collection-keyed, idempotent endpoints
Test-set membership is being renamed to the product's **dataset** vocabulary and re-keyed onto the collection — the identity a document already carries everywhere else (the `id` in every document response). Membership now lives under the workflow: `PUT /api/v3/workflows/{workflow_id}/dataset/files/{collection_id}/` adds a document to the dataset (optionally snapshotting a chosen extraction as ground truth via `{ "extraction_id": … }`), `DELETE` the same path removes it, and `POST /api/v3/workflows/{workflow_id}/dataset/files/bulk/` adds a filtered grid selection. All three writes are **idempotent** — re-adding a member or removing a non-member is a `200`/`204` no-op instead of a `409`. The test-file list rows now include a `collection_id` so callers can address membership without a second lookup.
The previous file-scoped routes `POST /api/v3/files/{file_id}/test-set/` and `DELETE /api/v3/files/{file_id}/test-set/` still work but are **deprecated** and now idempotent too; migrate to the `dataset/` paths above.
### July 3, 2026
**\[Deprecated + API]** v2 enters its deprecation window — `Deprecation`/`Sunset` headers on every response
With v3 as the new stable major, `/v2/` is frozen: no new features, existing behavior untouched. Every `/v2/` response now carries the [RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594) deprecation signals — `Deprecation: Fri, 03 Jul 2026 00:00:00 GMT` and `Sunset: Sat, 03 Oct 2026 00:00:00 GMT`. Per-route `Link: <...>; rel="successor-version"` headers pointing at the v3 equivalent follow as the mappings are wired up. `/v3/` responses carry none of these headers.
**Action recommended:** wire an alert on the `Sunset` header so migration work can't be missed — [Versioning & deprecation](/api-reference/introduction#versioning--deprecation) shows a five-line client middleware that does it. Then plan the move with the [v2 → v3 migration guide](/api-reference-v3/migrating-from-v2). No client changes are required today; v2 keeps working until the sunset date.
## June 2026
### June 29, 2026
**\[New + API]** Deterministic validation rules — instant, free checks alongside AI rules
A **Validate** step can now mix two kinds of rules. **AI** rules stay as they were: you describe the condition in plain language and a model judges it. **Deterministic** rules are new — structured checks that run in plain code with no model call, so they're instant and **free**. Seven check types ship: **number range**, **date range** (with a relative `today` bound for not-expired checks), **arithmetic** (`subtotal + tax = total`, with a tolerance), **comparison** (a field vs a value or another field), **one-of** (value in an allowed set), **pattern** (regex), and **required** (present and non-empty). Build them in Studio with the new **AI / Deterministic** toggle on each rule, or author them via the API and SDKs — deterministic rules reference fields by name and carry a `check` payload, AI rules carry a `description`. Pattern checks run on a linear-time regex engine, so a user-supplied pattern can't hang an extraction. Existing workflows are unchanged: rules default to `kind: "ai"`. See [Validation rules](/guides/studio/validate-rules).
### June 22, 2026
**\[New]** Self-serve Business subscriptions, recurring monthly credits, and a billing incident banner
Business is now a self-serve plan. From the dashboard, an organisation owner or admin can click **Upgrade to Business** to open Stripe Checkout, enter a card, and become a paying Business customer in a single round-trip — no support ticket required. Once active, the organisation receives a fresh **100,000-credit monthly grant** on each successful renewal, replacing the previous month's grant rather than stacking on top of it. Enterprise is unchanged in spirit (still sales-led) but rides the same plumbing: support agrees on a custom monthly grant and price, mints a dedicated Stripe price, and pins both to the org on a `BillingPlan` row. Top-ups still work on top of the recurring grant for one-off credit additions.
Subscription customers get a **Customer Portal** link from the **Usage** page to manage their payment method and cancel the subscription themselves. When Stripe reports a failed renewal, a **billing incident banner** appears on every authenticated page describing the current state (`PastDue` during the grace window, `Suspended` if grace expires, `Canceled` after cancellation) with a deep-link to the action that resolves it — update the card, reactivate, or restart the subscription. Late-payment recovery is exact: if a card is fixed after the grace window has zeroed the grant, the next successful invoice restores `cycle_monthly_grant − consumed_during_unpaid_cycle` credits (floored at zero), so a customer never pays for and then loses what they already used.
This rollout is gated behind `BILLING_SUBSCRIPTIONS_ENABLED`. Existing Business and Enterprise organisations whose tier was set by the legacy one-shot upgrade keep their tier through a fallback path until their first real subscription invoice clears.
### June 18, 2026
**\[New + API]** `GET /v2/workflows/{workflow_id}/definition/` — round-trippable workflow definition
A new sub-resource returns the workflow's typed graph in exactly the shape `PUT /v2/workflows/{workflow_id}/` accepts. SDK callers can now GET, mutate one field, and PUT the result back without re-authoring the rest of the schema. The PATCH side preserves `persistent_id` per `(node_id, field_name)` so an unchanged echo cuts no new version (`ChangeKind.NONE` modulo edge order), and genuine edits keep `persistent_id` stable for unchanged-by-id fields — analytics, ground-truth, and monitoring continuity all hold across edits.
**\[Changed + API]** Field `description` is no longer required to be non-empty
The typed surface (`AnyField.description`) previously enforced `min_length=1`, but Path A persistence (the Studio frontend's save path) never enforced it — so legacy workflows can carry fields with empty descriptions that the new GET-definition endpoint needs to round-trip cleanly. Empty descriptions are now accepted on `POST /v2/workflows/`, `PUT /v2/workflows/{id}/`, and the generated SDKs. SDKs and docs still encourage non-empty descriptions — an empty value degrades extraction quality, it isn't structurally invalid.
**\[New + API]** `POST /v2/workflows/{workflow_id}/files/from-url/` — upload by URL instead of multipart
A new route accepts an HTTPS URL and tells anyformat to fetch the bytes server-side, returning the same `CreateCollectionResponse` shape as the multipart `POST /v2/workflows/{workflow_id}/files/` upload. Use it when the source document already lives in object storage you can presign (S3, GCS, R2, …) or at a public HTTPS URL — no need to stream bytes through your own backend. The fetch is bounded by a 10-second timeout and the same 20 MB cap as multipart upload, and runs through the outbound-URL SSRF guard so URLs resolving to non-globally-routable IPs (loopback, RFC1918, link-local, cloud-metadata, IPv4-mapped IPv6) are refused before any connection opens. HTTPS-only at the gateway; non-2xx upstream, DNS failure, or timeout surfaces as `422`. Documented under [Upload File from URL](/api-reference/files/create-from-url) and in [Files](/concepts/files#how-to-provide-a-file).
### June 15, 2026
**\[Fixed + API]** SDK consumers can pull per-file results through `/v2/workflows/{wf}/results/?file_id=...` again
The v3 route `GET /api/v3/files/{file_id}/results/` declared its `workflow_version_id` query parameter as `int`, but the v2 gateway forwards the 10-character `WorkflowVersion.external_id` string returned by `/api/v2/.../latest_version/`. Ninja's int coercion rejected the string with `422`, breaking any SDK call that chained `latest_version` into the per-file-results lookup. The route now accepts `workflow_version_id` as a string, resolves the `external_id` to the internal PK in-route, and returns `404` (not `422`) for an unknown id — matching every other v3 endpoint's "the public `external_id` is the single public identifier" contract.
### June 11, 2026
**\[Breaking + API]** Object fields support only one level of nesting
Object fields have always been limited to **one level of nesting** — children of an object must be non-object types — but the API surface advertised the opposite: `ObjectField.nested_fields` was typed recursively, so the generated OpenAPI spec listed object-in-object schemas as valid. The contract is now enforced at the source: posting a schema with a nested object to `POST /v2/workflows/` returns `422`, and the same constraint is mirrored in the regenerated TypeScript SDK types and the Python SDK CLI (which now returns a clean "only one level of nesting is supported" error instead of a raw pydantic traceback).
**Action required:** schemas that declared an object inside an object must be flattened before submission. Single-level object fields are unaffected.
**\[Breaking + API]** `POST /api/v3/extractions/progress` removed; replaced by the workflow-progress digest
The batch progress endpoint `POST /api/v3/extractions/progress` is gone. Callers that want per-extraction progress alongside per-status file counts for a workflow version should switch to the new `POST /api/v3/extractions/workflow-progress-digest` (see the \[New + API] entry below). Single-extraction progress via `GET /api/v3/extractions/{uuid}/progress` is unchanged.
**\[New + API]** `POST /api/v3/extractions/workflow-progress-digest` — one round-trip for workflow polling
A new digest endpoint returns per-extraction progress for a caller-supplied set of `extraction_uuids` AND unfiltered per-status file counts for the surrounding `workflow_version_id` in a single response. The dashboard now polls this endpoint once every 4 seconds while files are processing, replacing three separate polls (`/extractions/progress` at 2.5s, and the v2 `/files/count/` and `/files/` polls at 5s). Polling stops automatically when no extraction is queued or in-progress. Counts come back `null` for unknown or cross-organisation workflow versions; cross-organisation extraction UUIDs are silently dropped, matching the existing single-extraction-progress semantics.
**\[New + API]** Cell-level grounding on extraction evidence
Table-row and scalar evidence now carries true cell provenance end-to-end: `evidence` and `mapped_locations` entries on extraction responses gain optional `cell` and `row` keys identifying the exact source cell that fed each value. Evidence without cell-level grounding (non-table datapoints, or table rows the model couldn't ground precisely) stays byte-identical to before — the new keys are emitted only when present. On the reference invoice that motivated this work, table rows with precise grounding went from 0% to 100% (626/626) with zero wrong-row assignments, and scalars whose value lives in a header table (invoice ids, dates) now resolve to one precise evidence on an LLM-cited page instead of a page-backfilled guess.
**\[New + API]** Test-set membership for files
Files can now be designated as part of a curated, locked test set. A new boolean `is_test_file` is returned on file responses, and two v3 routes manage membership: `POST /api/v3/files/{file_id}/test-set` to add and `DELETE /api/v3/files/{file_id}/test-set` to remove. Files in the test set are **frozen from re-verification**: every verification write (verify, unverify, edit, discard at the datapoint level; verify-extraction; add/delete/swap/verify-row/verify-all at the datarow level, across both v2 and v3) now returns `409 Conflict` with code `ground_truth_locked`, so an extraction designated as ground truth cannot be mutated out from under a downstream accuracy scorer. Ground-truth values and accuracy reporting land in a follow-up release.
**\[New]** Download lookup files from the Extract node UI
Lookup files (master files attached to an Extract node) were upload-only — once added, you couldn't get them back. The **Manage files** dialog in the Extract node now has a download button next to each lookup file, backed by a new presigned-URL endpoint `GET /api/v2/saas_manager/workflows/{id}/master_file/download/?uri=`. Org membership and the workflow-scoped S3 key prefix are both enforced; the presign is pinned to the canonical bucket so a crafted cross-bucket URI cannot redirect the signed GET.
**\[Improved]** Table extraction now always uses the refine engine
Table parsing previously branched upfront on a heuristic between a cheap `simple_table` call (one shot, no retry — silent data loss on empty/truncated output) and a much slower `dense_table` agent (\~10–20× the cost and latency). The classification was fuzzy and data integrity depended on getting that one-shot guess right, so the routing prompt was biased toward dense, over-escalating and burning cost. Every table now takes the same path — cheap parse → audit critic → in-place refinement with tools — with a best-draft-wins guarantee, so a misclassification can no longer lose data. On an 18-document Iberia benchmark refine was **27% cheaper** (cheaper on every doc) and substantially faster on the slow tail (worst single doc 1346s → 317s), with all four `full_content` quality metrics improving. As part of the change the routing prompt now requires ≥2 rows × ≥2 columns of tabular evidence before sending a block to the table pipeline, so label/value pairs, signatures and logos are no longer misrouted as tables.
**\[Improved + API]** Workflow analytics timeseries response is much smaller and faster
`GET /api/v3/workflows/{id}/analytics/timeseries` now serves the default monitoring view in **a few KB of gzipped JSON** instead of 1.7–3.9 MB. The "Overall" series across all fields is shipped pre-collapsed as a new `overall_rows` array (one entry per extraction), and an opt-in repeatable `field_ids` query parameter narrows the legacy `rows` payload to only the fields you want — present-but-empty (`?field_ids=`) returns counts and `overall_rows` only. Calls that omit `field_ids` still receive the full legacy rows, so existing integrations keep working unchanged. The cold-path recomputation now scopes the live aggregation to cold versions instead of the full workflow, self-heals to all-warm on the first full-universe read, and the response is gzip-compressed.
### June 10, 2026
**\[New]** Per-user timezone preference for API datetimes
User profiles now carry an IANA timezone preference, defaulting to `Europe/Madrid`. Datetime fields in API responses are serialized in the user's chosen zone, and the dashboard **Account** page gains a searchable timezone selector that persists through the existing profile-update flow. Storage stays UTC end-to-end — only the wire serialization (and transitively, what the dashboard renders) reflects the preference.
**\[Fixed + API]** Oversized request bodies now return `400` instead of `500`
Any v3 endpoint that reads a request body returned a `500` when the JSON payload exceeded the 20 MB limit (`DATA_UPLOAD_MAX_MEMORY_SIZE`): Django raised `RequestDataTooBig` while django-ninja read `request.body` during parameter resolution, before the view ever ran, so it could not be caught inside the endpoint. A global exception handler on the v3 API now returns `400` with `{"detail": "Request body is too large. Maximum allowed size is 20 MB."}`. The limit in the message is derived from the setting, so it stays accurate if the cap is tuned.
**\[API]** Rate limiting now enforced on the external API in production
The external API's rate limiter is now active in production. It had been silently disabled for roughly three months because `RATE_LIMIT_ENABLED` and `REDIS_URL` were added to the dev config but never propagated to prod. Callers exceeding the configured limits may now receive `429` responses.
**\[Breaking + API]** Deprecated v2 datapoint and file-verification endpoints removed
The following v2 endpoints are gone. They were already marked deprecated and verified unreachable from the dashboard, the external API, the SDKs, and the cronjobs before removal:
* `PATCH /api/v2/.../datapoints/{id}/` and `POST` / `DELETE /api/v2/.../datapoints/{id}/validation/`
* `POST /api/v2/.../files/{id}/verify/`
**Action required:** integrations that still target these paths must switch to the v3 replacements: `POST /api/v3/extractions/{uuid}/datapoints/{verify,unverify}` for verification toggles, `POST /api/v3/extractions/{uuid}/datapoints/{edit,discard}` for value edits and discards, and `POST /api/v3/extractions/{uuid}/verify` for whole-extraction verification.
**\[Fixed]** Verify Document button now works on split + extract workflows
The header **Verify Document** button used to be a permanent no-op on workflows that combine a Split node with an Extract: it targeted the file-level extraction, but extracted data on a split lives on per-split child extractions, so verifying the parent verified nothing. The button now targets the **active split's child extraction** (and is labelled **Verify Extraction** on a split tab to match), and is disabled on parse, split, and validation tabs where there is nothing to verify. A split file is reported as verified only once every non-empty child extraction is verified — empty category tabs no longer wedge the rollup. Shift+Enter advances through unverified splits before moving on to the next file.
**\[Improved + API]** New `verifier` API surface; `validator` deprecated
The "human-validation → verification" rename now reaches the file-level API. A new endpoint `POST` / `PATCH /api/v2/.../files/{id}/verifier/` assigns and updates the verifier for a file, a `verifier__in` filter narrows file lists by assigned verifier, and `?include=verifier` returns the assigned verifier in file responses. The legacy `/validator` action, `validator__in` filter, and `?include=validator` option remain live for backward compatibility but are now flagged `deprecated` in the OpenAPI spec; integrations should plan the move.
**\[Fixed + API]** Non-member reads of workflow analytics now return `404`
`GET /api/v3/workflows/{id}/analytics/summary` and `GET /api/v3/workflows/{id}/analytics/timeseries` previously returned `403 Forbidden` to a caller outside the workflow's organisation, leaking the existence of the workflow. They now return `404 Not Found`, matching `GET /api/v3/workflows/{id}`. Members continue to receive analytics as before.
**\[Fixed]** New free accounts now see their signup credits on the Usage page
The **Usage** page counted only purchased and granted credits, so a brand-new free account — whose only balance is its signup bonus — showed a zero balance and the empty-state screen instead of the credits it could actually spend. Signup credits are now included in both the headline total and the per-source breakdown.
### June 9, 2026
**\[API]** New v3 endpoints for row-level edits on object fields
The v3 API now exposes positional row operations on object-field tables: add, delete, swap, verify-row, and verify-all. Rows are addressed positionally by `(extraction_uuid, field_persistent_id, index)` instead of an opaque datarow id, and the dense `[0, n_live]` invariant is enforced at the boundary (negative indexes now return `422` instead of a silent `404`). The corresponding v2 actions on `DatarowViewSet` are now marked deprecated but remain live; integrations should plan to migrate.
**\[Improved]** Document parsing now retries transient LLM failures more reliably
The parse pipeline now retries `LengthFinishReasonError` (model hit max-tokens, often non-deterministic) and detects silent parse failures where the LLM returned a 200 but the response could not be coerced into the schema. Both previously skipped straight to the fallback model on the first attempt; they now go through the configured retry budget on the primary model first. The per-page parsing token budget was also raised from 8K to 32K to reduce length-truncation failures, and `ParseFailedError` now carries the underlying per-page error detail.
**\[Fixed]** Lookup + Validate workflows now complete end-to-end
Two independent bugs that broke the same Lookup + Validate workflow are fixed:
* A `Validate` node downstream of an `Extract` node with lookup fields previously returned every rule as *inconclusive ("No extraction data available to validate.")*, because the validate step resolved its upstream to a synthetic lookup node and read a state key nothing ever wrote. It now resolves through smart-lookup hops back to the real producing extract.
* The Gemini page-sum verification schema was missing a top-level `title`, which made model-build raise outside the per-task `try/except` and aborted the whole extraction whenever Phase-1 sum verification failed and Gemini verification was attempted.
**\[Improved]** Faster admin pages, file listings, and date-filtered results
Several database hotspots flagged by APM are now indexed and the corresponding queries rewritten to be sargable:
* `Extraction.uuid` and `File.name` get btree indexes (built `CONCURRENTLY`), eliminating sequential scans on the hundreds of lookups per day they served.
* A new composite `FileCollection(workflow_id, created_at DESC)` index backs the per-workflow collection listing.
* `created_at` date filters across the admin dashboard, billing workflow-usage queries, and the public `ResultsFilter` are rewritten from `(created_at AT TIME ZONE 'UTC')::date` casts (which disabled the index, costing up to \~2s on a single filter path) to half-open `created_at` ranges. Behavior is unchanged because the deployment timezone is UTC.
**\[Breaking + API]** Deprecated v2 datarow endpoints removed
The v2 `DatarowViewSet` actions marked deprecated in the same release as the v3 datarow rollout — `create_empty`, `delete_by_position`, `swap`, `validate_row`, `validate_all` — and their five `/api/v2/.../files/{file_id}/datarows/` routes are now removed.
**Action required:** integrations that still drive row-level edits through the v2 paths must migrate to the v3 surface: `POST /api/v3/extractions/{extraction_uuid}/datarows/{add,delete,swap,verify-row,verify-all}`.
**\[Improved]** Per-category grid restores split-granular column filtering
Workflows that include a Classify or Split node show their extracted data in the Per-Category grid in the data viewer. Column filters on that grid had been deliberately disabled because the previous file-paginated path compared against the wrong extraction. A new split-granular results endpoint backs the grid: a filter on an extracted field now narrows to exactly the matching split results, paginated and counted server-side, with filter and sort behavior matching the main file grid.
### June 5, 2026
**\[Improved]** Transient frontend request failures now auto-retry with backoff
The web app retries idempotent React Query requests that fail with transient connectivity errors or 5xx responses, using capped exponential backoff (up to \~10s) and up to 3 attempts. Deterministic 4xx responses and unknown errors still fail fast. Connectivity errors are now detected via the Fetch spec's `TypeError` instead of message matching, which cuts down on false-positive error reports during brief network blips.
**\[API]** Malformed `Authorization: Bearer` headers now return `401`
`Authorization` headers with malformed bearer tokens (for example, an empty token or whitespace inside the token value) now return `401 Unauthorized` instead of `500 Internal Server Error`. The successful auth path and well-formed token-rejection responses are unchanged.
**\[Fixed]** Newly created workflows lay out their nodes with wider default spacing
Newly created workflow nodes are placed with a larger default gap so the auto-layout reads more clearly out of the box and matches the spacing the rest of the editor assumes.
**\[Fixed]** Full-page loading spinner no longer jumps during initial load
The full-page spinner stays anchored in place while the app boots, instead of shifting position as the surrounding layout settles in.
**\[New]** How credits work — new pricing & credits docs page
A new Concepts → Account page, **How credits work**, documents per-operator credit costs (including Validate at 5 credits per rule per page), plan €/credit conversion, included credits, usage tracking, and tips for keeping cost down. The Usage & Billing page links to it in place of the previous "contact us for pricing" stub.
**\[Improved]** Monitoring over-time chart now plots on a true time axis
The accuracy and confidence over-time chart on the Monitoring tab positions each extraction at its real timestamp on a time-scaled axis, instead of forcing points onto an evenly-spaced grid. Time-bucketing now adapts to the visible span, so long, dense histories coarsen gracefully while short spans stay at full resolution. Version markers sit at their exact creation time and merge into a single ranged label (for example `v1.0–v1.2`) when several versions ship the same day, and the empty states now distinguish between no rows, no reviewed extractions, and no recorded confidence scores.
**\[Improved]** Large workflow versions no longer time out when loading file counts
Loading the file counts for a high-volume workflow version no longer tips over the request timeout. The count is now served by a single annotated aggregate backed by an index-only scan, replacing the per-file status scan that caused worker timeouts on workflows with many files.
**\[Fixed]** Filtering and sorting restored on smart-lookup columns
Columns backed by a smart-lookup field keep their sort and filter controls in the data viewer while still showing the lookup indicator. Previously the custom lookup header replaced the entire header cell, hiding the filter button on those columns even though filtering still worked underneath.
**\[Fixed]** Overlapping bounding-box highlights no longer wash out the PDF page
In the PDF visualizer, hovering a stack of overlapping bounding boxes now reads as a single translucent tint on both light and dark pages, instead of compounding toward opaque as more boxes overlapped. Boxes shared by many fields, such as table sections, also render more efficiently.
### June 2, 2026
**\[Breaking + API]** Monitoring summary `total_extractions` now counts processed extractions only
`GET /api/v3/workflows/{id}/analytics/summary` previously counted every queued extraction in `total_extractions`, including in-flight rows that had not yet finished processing. It now counts processed extractions only — matching the `verified`, `through`, and per-field counts already returned by the same endpoint, and matching what the cold path (no version filter) has always reported.
**Action required:** if your integration reads `total_extractions` from the summary endpoint and expects in-flight rows to be included, switch to a non-summary query or add the in-flight count from your own tracking. The Studio monitoring UI is unaffected.
**\[Improved]** Monitoring page now reveals data progressively
Each card on the Monitoring tab paints its title and controls immediately and shows a skeleton only for the body that is still loading, off the data source that feeds it. A fast endpoint can paint without waiting on a slower sibling, instead of the whole page sitting behind a single page-level skeleton. The time-series chart also now keeps daily resolution on 30-day ranges instead of collapsing into \~5 weekly points.
**\[Improved]** Faster workflow version analytics summary
`GET /api/v3/workflows/{id}/analytics/summary` is significantly faster on high-volume workflow versions. Overall metrics, per-field metrics, verified counts, and throughput are now computed in two table scans instead of five, with no change to the response shape.
The endpoint previously routed through the paginated read path and ran the per-extraction aggregate twice plus a discarded pagination subquery. It now serves the summary from a dedicated read that combines the by-field `GROUP BY` and a single per-extraction `COUNT(...) FILTER(...)` aggregate, with the paginated read path unchanged for the timeseries and detail endpoints.
**\[Improved]** Faster workflow version analytics timeseries
`GET /api/v3/workflows/{id}/analytics/timeseries` is now significantly faster on high-volume workflow versions, applying the same optimization as the summary endpoint. The response shape is unchanged.
The timeseries endpoint consumes only the per-extraction detail rows, but was still served by the full five-scan paginated read — computing the overall sum, per-field `GROUP BY`, and through-rate counts only to discard them. It now uses a detail-only read, and resolves the latest N extractions from the `Extraction` table (which owns `processed_at`) instead of grouping the multi-million-row analytics cache — roughly 34× faster on the top-N lookup, with existing indexes only and no migration.
**\[API]** Deprecated combined `/analytics` endpoint removed
The deprecated `GET /api/v3/workflows/{id}/analytics` route (flagged for removal when the frontend migration completed) is gone. Use `GET /analytics/summary` for per-version overall and per-field metrics, and `GET /analytics/timeseries` for the cross-version series.
**\[Fixed]** File validation status stays in sync with its datapoints
When you confirm the last unverified datapoint on a file, the file badge now reliably flips to **Validated**, and when you unverify a previously-verified datapoint the file rolls back to **Processed**. Previously, the file-level validation status could drift from its datapoints — most commonly, the last cell confirmation would not flip the file badge.
**\[Improved]** Organization switcher now scales to many organizations
The sidebar organization switcher caps at 10 visible organizations by default with a **Show all** expander, and adds a search box that fuzzy-matches organization names across your full list. Organizations you're a direct member of are prioritized over support-access ones, and a per-user most-recently-used ordering surfaces the organizations you actually work in first. The current organization is always shown.
**\[Fixed]** Deep links to a file's results now switch to the workflow's organization
Opening a workflow file via a direct URL or in a new tab (middle-click) now elevates the active organization to the one the workflow belongs to, matching click-through navigation. Previously a cold-loaded file results page kept the UI on your own organization, hiding the support-access banner and showing the wrong organization in the sidebar.
**\[Fixed]** Organization selector rows stay a readable size on long lists
When you belong to many organizations, each row in the sidebar organization selector keeps a consistent, tappable height with visible spacing, instead of compressing to near edge-to-edge as the list grows.
### June 1, 2026
**\[New]** Workflow version selector
The workflow page header now has a version selector (on the Studio and Monitoring tabs) so you can view any saved version of a workflow. Selecting a non-latest version on Studio disables editing with an explainer — only the latest version can be edited — while Monitoring scopes its analytics to the version you pick.
**\[New]** Per-version monitoring history
The Monitoring tab now shows version history: the accuracy/confidence chart spans all versions with markers at each version change, a changelog panel lists what changed, and KPIs and by-field metrics can be scoped to a selected version.
The analytics endpoint was split into purpose-built reads — `GET /analytics/summary?version_id=` (per-version overall + per-field means and through-rate counts) and `GET /analytics/timeseries?limit=` (cross-version series) — and analytics are now paginated by extraction rather than by (extraction, field) row, so wide schemas no longer collapse the window. The old `/analytics` endpoint is deprecated but retained until the frontend migration completes.
**\[API]** Python SDK now installs as `anyformat`
The official Python SDK's PyPI distribution has been renamed from `anyformat-sdk` to `anyformat` — `pip install anyformat` — mirroring `@anyformat/sdk` on npm. The import path is unchanged (`from anyformat.sdk import Client`).
The distribution starts at version 0.6.0 (the next free slot past the legacy `anyformat==0.5.0` on PyPI). The internal CLI binary that previously occupied the `anyformat` console-script slot was renamed to `af`; `from anyformat.cli import ...` still works.
**\[Improved]** Workflow data sub-tabs keep their structural entry points
When a workflow has many data sub-tabs, the collapsed row keeps the leading tabs (Documents, Split/Classify, the first categories) and pins the selected tab to the end, with the overflow in a "+N" menu — instead of hiding everything but Documents and the active tab. Rows with five or fewer sub-tabs show all of them.
## May 2026
### May 28, 2026
**\[Changed]** Validator nodes now consume credits
`ValidateNode` previously ran for free. Each rule in a validator now costs 25 credits, billed once per extraction. The bill scales with the number of rules in the validator, not with the number of pages or files in the document packet.
A new `validate` operator (`Credits(25)`) was added to `DEFAULT_OPERATOR_PRICES`. The internal billing dispatch was reshaped from `(item) -> list[OperatorName]` to `(item, page_count) -> list[LineItem]`, so non-page-priced operators like validators can ignore the page count and substitute `len(rules)` as the billed unit. The processor now issues a single `SpendCreditV4` call per extraction with all line items combined.
**\[Changed]** Default operator prices realigned with the public pricing page
Default credit prices for several operators now match [anyformat.ai/pricing](https://anyformat.ai/pricing): Parse 25 (was 30), Extract 35 (was 20), Splitter 25 (was 10), Parse (Agentic) 100 (was 75), Extract (Agentic) 150 (was 75). Classify is now billed at 10 credits/page (previously a no-op — running a Classify node was free regardless of the published rate). Organizations with enterprise-specific overrides on `OperatorPriceTable` are unaffected.
**\[Improved]** Documentation restructured into Concepts / Guides / API Reference
[docs.anyformat.ai](https://docs.anyformat.ai) has been reorganized into three scoped tabs. **Concepts** holds definitional pages (Workflows, Schemas, Fields, etc.). **Guides** holds task-oriented walkthroughs (Quickstart, Build workflows, Recipes). **API Reference** holds the REST spec only. The four overlapping "create → run → results" walkthroughs are merged into one Quickstart with UI / curl / Python tabs at each step, and every extraction recipe now uses the canonical typed-graph workflow shape (`{name, nodes, edges}`) instead of the legacy `{fields:[...]}` shortcut. Old URLs redirect.
**\[Improved]** Post-signup `/welcome` page is now reachable only from the signup callback
Loading `/welcome` directly (typed URL, bookmark, fresh tab) now bounces to `/`. The page is only reachable after a brand-new Auth0 signup callback, which closes a path that could let the upstream GA4 / GTM conversion event fire for already-signed-up users.
**\[Improved]** `@anyformat/skill` synced to npm 0.2.2
The `@anyformat/skill` package version in this repo is now aligned with what's live on npm (`0.2.2`). The publish script uses token-based auth and bumps from `max(local, registry)` to prevent version collisions on the next release.
**\[Improved]** Release-PR generation now audits merged PRs upfront
The `/prod-release` Claude Code command (used to open this release PR) now treats the merged-PR list as the ground truth and the changelog as a derived artifact. The audit step surfaced the gap that produced the entries below — previously, PRs merged without a changelog entry could quietly drop out of the release notes.
### May 27, 2026
**\[New]** Attach smart lookup files to fields in the workflow SDK
The Python and JS SDKs can now mark a field as a smart lookup field and attach a reference file when creating a typed workflow. Flag the field with `lookup` and point it at a local CSV or text file; the SDK reads the file and uploads it inline, and extraction resolves that field's values against your reference data. Lookup files are validated as UTF-8 text before anything is written — base64 payloads are never stored or returned.
`POST /api/v2/workflows/` accepts a per-field `lookup` flag (mapped to `source=smart_lookup`) and inline base64 `lookup_files`. Files are pre-validated as UTF-8 text/CSV (with cp1252→UTF-8 transcoding); binary or non-text uploads are rejected with a `400 UNSUPPORTED_LOOKUP_FILE` before any S3 writes. Only S3-backed lookup file references are persisted; base64 payloads and URIs are never stored or echoed back.
**\[Improved]** Excel workbooks now convert to one Markdown page per sheet
Excel→Markdown conversion now emits a separate page for each worksheet, in workbook order, instead of concatenating every sheet into a single page. Multi-tab spreadsheets now classify and process per tab, keeping each sheet's content distinct downstream.
**\[New]** Official Python SDK now available on PyPI
`pip install anyformat-sdk` — the `anyformat-sdk` package is now live on PyPI. Use the fluent builder to author, create, run, and await workflows end-to-end without writing HTTP code; sync and async clients are included, with typed errors (`BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `RateLimited`, `ServerError`, `SDKTimeout`) and a `.raw` escape hatch on every response. The Claude Code skill at `@anyformat/skill` now documents the Python SDK alongside the JS/TS one.
**\[New]** Post-signup `/welcome` landing page
Brand-new signups now land on a dedicated `/welcome` page (instead of going straight to `/`), giving marketing a stable URL for Google Ads conversion configuration via GA4 / GTM URL match. Returning users continue to land on `/` (or the originally-requested URL) as before.
**\[Fixed]** New signups for an already-registered email address are now rejected
Across every API write site that can create a User row (Auth0 just-in-time provisioning, the E2E test-auth endpoint, the `create_user` management command), case-insensitive duplicate emails are now refused before a second row can be created. Auth0 email rotation that would collide with an existing user is also refused. Previously, mixed-case variants of the same address could silently mint duplicate users.
**\[Fixed]** Resolved four frontend dependency vulnerabilities
`qs` (DoS in stringify), `tmp` (path traversal), `uuid` (missing buffer bounds check), and `js-cookie` (prototype hijack) advisories are now resolved via package overrides in `anyformat/services/frontend`. Upstream parents (cypress, exceljs, segment-analytics) hadn't yet adopted the patched versions, so overrides were the minimally-disruptive path.
**\[Improved]** Authenticated images are no longer refetched multiple times per page load
The sidebar mounts the organization logo from three places, which previously meant three identical requests to `/api/v3/iam/organizations/{id}/logo/` on every page load. The image hook now uses TanStack Query so concurrent subscribers share one in-flight request and the result is cached for 5 minutes.
**\[Improved]** Local development supports both `localhost` and `127.0.0.1` for OAuth
For developers running the stack locally, both `http://localhost:/callback` and `http://127.0.0.1:/callback` are now valid OAuth callback URIs and CORS / CSRF origins. Previously only one was registered per environment, which caused login failures if the dev frontend was opened at the other hostname.
**\[Improved]** JS SDK build toolchain refreshed
`esbuild` (0.21.5 → 0.27.7) and `vitest` (2.1.9 → 4.1.7) in the JS SDK package have been updated. No behavioral changes to the SDK itself.
### May 26, 2026
**\[Improved]** Higher monthly allowance on the Business plan
The Business plan's monthly credit grant has increased to 500,000 credits.
**\[Changed]** Free tier now gets a one-time 50,000-credit signup grant instead of a monthly allowance
Free organizations no longer participate in the monthly credit rotation. Each new free org now receives a single 50,000-credit signup grant that never expires — generous enough to evaluate the product, finite enough to make upgrading the natural next step. Paying tiers (Business, Enterprise) keep their recurring monthly allowance unchanged.
**\[Changed]** Stripe top-up pricing is now tier-aware
Top-up checkout rates depend on the organization's tier: free orgs pay €1.50 per 1,000 credits (€0.0015 / credit), Business and Enterprise orgs pay €1.00 per 1,000 credits (€0.001 / credit). Top-up amounts are now denominated in credits (minimum 30,000) instead of euros.
### May 25, 2026
**\[New]** Visualize parse layout with `result.parse.draw()`
The Python SDK can now render a document's parsed layout as an image overlay. Call `result.parse.draw()` to get the page with detected blocks drawn on top — useful for debugging extraction and verifying bounding boxes. A new cookbook example walks through it.
**\[Changed]** Simplified parse configuration
The `engine` (Fast/Performant), `effort`, and `visual_grounding` parse knobs have been removed from Studio and the workflow SDK. `engine` was a no-op (both options mapped to the same model), and the other two now use sensible built-in defaults. `figure_enhancement` and `mode` remain configurable. Existing workflows are unaffected — stored values for the removed knobs are ignored gracefully.
**\[New]** Install the anyformat Claude Code skill from npm
The anyformat Claude Code skill is now published on npm as `@anyformat/skill`. Run `npx @anyformat/skill` to install it — Claude Code can then author, run, and inspect anyformat workflows directly from your editor using your `ANYFORMAT_API_KEY`. Public docs include a new "Claude Code Skill" section under the SDKs page.
**\[Fixed]** Classify and split workflows authored via the Python SDK now route to extraction correctly
Workflows built with the Python SDK's `classify(...)` / `split(...)` builders no longer silently skip downstream extraction when a category or split rule is attached by name. The typed graph renderer now resolves branch ports by name (matching the schema's edge labels) instead of internal IDs, so SDK-authored classify/split workflows produce the same extractions as Studio-authored ones.
**\[Fixed]** OCR text containing NUL bytes no longer fails extraction
Extractions whose parse output contained a U+0000 (NUL) character inside a text value previously failed at the database write step, marking the entire batch as errored. The worker now strips NUL bytes from text at the parsing boundary so these documents complete normally.
**\[API]** Organization balance endpoint now returns a per-bucket breakdown
The new organization balance endpoint reports credits broken down by their source bucket — monthly grant, top-ups, and each redeemed voucher — along with the organization's tier and the monthly anchor day. Read-only by design: querying the endpoint never triggers a balance reset or recompute.
`GET /api/v3/billing/organizations/{org_id}/balance/` returns `{ tier, monthly_anchor_at, monthly: {...}, topup: {...}, vouchers: [...], overdraft: {...} }`. Pre-migration organizations return zeroed buckets with a stable shape. Membership is enforced; any role can read.
**\[New]** New organizations receive a monthly credit grant on signup
Newly created organizations now receive a monthly credit grant scoped to their tier, which resets on the organization's anchor day each month. This replaces the previous one-time welcome top-up and makes ongoing free usage predictable instead of front-loaded.
**\[Improved]** Voucher credits now expire after a configurable lifetime
Vouchers can now carry a `credit_lifetime_days` setting (default 90 days). When a voucher is redeemed, the granted credits expire after that many days, so promotional credits don't sit in a wallet indefinitely. Existing vouchers continue to behave as before until the new field is set.
**\[Fixed]** Signup voucher hint shows voucher value only, in your locale
The voucher preview on the create-organization and join-or-create pages no longer double-counts the welcome grant — a 5,000-credit voucher now renders as "5,000 credits" instead of being added on top of the standard signup grant. The number is also formatted using the browser's locale instead of being hardcoded to German formatting.
### May 22, 2026
**\[New]** Official Python SDK and `afx` CLI
The `anyformat-sdk` package wraps the workflow API with sync and async clients so you can author, create, run, and await a workflow end-to-end without writing HTTP code. Typed errors (`BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `RateLimited`, `ServerError`, `SDKTimeout`) make integration code easier to write correctly, and a `.raw` escape hatch on every response exposes the underlying JSON when you need it.
```python theme={null}
from pathlib import Path
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key="...")
workflow = (
client.workflow("Invoices")
.parse(mode="agentic")
.extract([
Schema.string("vendor", "The company that issued the invoice"),
Schema.string("total", "The total amount due, including currency"),
])
.create()
)
run = workflow.run(file=Path("invoice.pdf"))
result = run.wait(timeout=300)
print(result.fields["vendor"].value)
```
`AsyncClient` mirrors the sync surface for async codebases.
**\[New]** `afx` CLI for one-shot parse and extract
The SDK ships an `afx` CLI for quick command-line use. Run `afx parse --file invoice.pdf` to get markdown, or `afx extract --file invoice.pdf --field "vendor:vendor company" --field "total:total amount"` to extract structured fields. Pass `--schema fields.json` to load a richer schema, `--json` for machine-readable output, and `-o/--output FILE` to write results to disk.
```
export ANYFORMAT_API_KEY=sk_...
afx parse --file invoice.pdf
afx extract --file invoice.pdf --field "vendor:vendor company" --field "total:total amount"
afx extract --file invoice.pdf --schema fields.json --json -o results.json
```
Spinners, step breadcrumbs, and the Markdown/table renderers all detect TTYs and stay quiet when output is piped or `--json` is set, so the CLI is safe to script.
**\[New]** Workflow versions are now semver-numbered
Workflow versions now display as `v1.0`, `v1.1`, `v2.0` across Studio, version history, and the save toast. Saving a workflow only mints a new version when it actually changes — major bump on graph topology or node config changes, minor bump on extraction-schema changes, and no bump at all when you only move nodes on the canvas or reorder columns in the data viewer.
**\[API]** Save layout and reorder fields without minting a new workflow version
New workflow-centric write endpoints separate cosmetic edits from material ones. Layout drags and column reorders no longer create new versions or break stable links to a specific version of your workflow. The legacy v2 write endpoints continue to work but now respond with `Deprecation: true`.
* `PATCH /api/v3/workflows/{id}/config/` — save (may bump major or minor)
* `PATCH /api/v3/workflows/{id}/layout/?version_id=X` — canvas position only (never bumps)
* `PATCH /api/v3/workflows/{id}/field-order/?version_id=X` — column reorder only (never bumps)
Concurrent edits surface a `409` with `latest_version_id` in the response body so your client can rebase without losing local edits. The deprecated v2 endpoints (`PUT /api/v2/.../workflows/{id}/config/` and `PATCH /api/v2/.../workflows/{id}/reorder-fields/`) still function during the transition window.
**\[New]** `npx anyformat-skill` installs the anyformat Claude Code skill
If you use Claude Code, you can now install the anyformat skill with a single command: `npx anyformat-skill` drops it into `~/.claude/skills/anyformat`, or `npx anyformat-skill --project` installs it into the current project. The skill teaches Claude how to drive the typed-graph workflow API end to end — create a workflow with nodes and edges, upload a file, run extraction, and poll for results.
**\[Fixed]** Multi-tab login no longer fails with a code-verifier error
Logging in from two browser tabs at once could clobber the shared PKCE code verifier and surface as a generic "Login failed" page. The callback now silently retries once on this race and lands you on your original destination URL.
### May 21, 2026
**\[Improved]** Cleaner parsed markdown — no structural metadata wrappers
Parsed markdown saved to S3 and returned from the parse-result endpoints no longer carries `` framing or `` wrappers. Block boundaries are now expressed as inert `` anchors that render as nothing in any markdown viewer, and page boundaries are joined with blank lines. Bounding boxes, page numbers, and confidences remain available via the structured results endpoints — they just stop leaking into the human-readable file body.
**\[New]** Modelo 200 workflow template (Spanish corporate income tax)
A new "Modelo 200" template lands in the workflow gallery, oriented around credit underwriting and portfolio early-warning. It covers balance-sheet and profit-and-loss extraction with 45 root fields (liquidity, leverage, profitability, cash conversion, coverage ratios) plus two table fields for directors and principal shareholders. The template is pre-configured to use agentic parsing with Balanced effort.
**\[Fixed]** Nested table columns no longer dropped by the smart-table extractor
Columns of a list-of-objects field (for example, items inside a product table) could previously be missed because the smart-table planner tried to extract them as standalone scalars. Nested fields are now correctly treated as per-row columns of their parent table and reach the final extraction every time.
**\[Fixed]** Editing object-table cells no longer loses focus mid-typing
Validating a value in an object-field table sometimes stole keyboard focus from the input before you finished typing, occasionally swallowing characters. Cell focus is now preserved across both the validation round-trip and the background refetch, and screen-reader row labels stay in sync after row swaps.
**\[Fixed]** Parse → split workflows now return their splits
Workflows that parse and split a document without a downstream extract were silently dropping the resulting splits. Terminal-splitter workflows now persist and return their splits as expected, surfaced via the file-list endpoint with `?include=splits`.
### May 20, 2026
**\[API]** `POST /v2/workflows/` now takes a typed graph; the old fields-only body is gone
`POST /v2/workflows/` now accepts the typed `nodes` + `edges` graph that previously lived at `POST /v2/workflows/with_graph/`. The legacy fields-only body and the `with_graph` path have been removed — there is one canonical workflow-create endpoint going forward.
The `with_graph/` path no longer exists at `api.anyformat.ai/v2/workflows/`. Send the same payload (`{name, description?, nodes, edges}`) to `POST /v2/workflows/`. The legacy body `{name, fields}` is no longer accepted — wrap your fields in an explicit `parse → extract` graph instead. The legacy `multipart/form-data` upload path (creating a workflow from a downloaded `json_file`) has also been removed; rehydrate the workflow into the typed-graph body client-side.
**\[Changed]** `POST /v2/workflows/{id}/run/` response `status` is now `"pending"` instead of `"success"`
The public run endpoint accepts requests asynchronously (`202 Accepted` — the extraction is enqueued, not complete). The previous `"success"` value conflated acceptance with completion; the new `"pending"` matches the vocabulary used by `WorkflowRunListItem.status` elsewhere on the v2 surface. Clients that polled for results regardless of this field are unaffected. Clients that branched on `"success"` to skip polling should now poll on `"pending"`; semantics match what was always intended.
**\[New]** Per-branch Validation tabs across workflow and file detail pages
The Validate node now gets the same per-branch UX as Extract. The workflow page renders one Validation tab per branch alongside its existing Category and Extraction tabs, and the file detail page mirrors the same layout. Split workflows include a subdocument selector with chevrons and a `1/N` counter, so you can step through per-subdocument validation results without leaving the tab.
**\[Improved]** Clear "Not validated" placeholders for rules added after extraction
When you add or edit a validation rule, files that were processed before the change now render explicit "Not validated" cards and cells with a hover-card explanation, instead of an ambiguous "N/A". This makes it obvious which results reflect the current rule set and which would need a re-run.
**\[Improved]** Redesigned Validate node configuration menu
The Validate node menu now uses a tabbed Config / Rules layout aligned with the Classify menu, with a dedicated rule editor that supports inline field chips inside rule descriptions. The previous template-import button has been removed in favor of the new layout.
**\[Fixed]** Validation verdicts now populate for split workflows
In workflows with a Split node, validation verdicts could come back empty in the file list and detail view because the API only inspected the parent extraction. Validation results now traverse parent and child extractions so per-subdocument verdicts surface correctly across the workflow page Validation tab, the file detail page, and the file results UI.
**\[Fixed]** CSV and master-file uploads decode reliably from non-UTF-8 encodings
Uploaded CSV files and master files saved in cp1252 (Word/Outlook smart quotes, Spanish accents), Shift-JIS, GB18030, KOI8, and similar non-UTF-8 encodings could previously either silently produce mojibake or fail extraction with a Unicode decode error. Uploads are now transcoded to UTF-8 at the API boundary, with binary payloads disguised as text rejected up front, so downstream parsing and lookups receive consistent, correctly-decoded bytes.
The master-file upload endpoint and workflow CSV upload path now run inputs through a strict Western decoding ladder (`utf-8-sig` → `utf-8` → `cp1252`) before falling back to charset detection, and persist the re-encoded bytes to storage with a truthful `Content-Type: …; charset=utf-8` header. Allowed text extensions are `csv`, `txt`, `md`, `markdown`, and `rst`. Non-text payloads (PDF, ZIP, images, executables) are rejected at upload time rather than being mis-decoded.
### May 19, 2026
**\[API]** Fetch parsed blocks with bounding boxes via the new parse-result endpoint
The new parse-result endpoint returns the structured page-nested blocks produced by parsing — including bounding boxes, type, confidence, reading order, and layout — along with a presigned URL for the raw markdown. Studio's PDF viewer and JSON view now use these blocks to render overlays consistently across tabs.
`GET /api/v3/files/{file_id}/parse-result/` returns `{ blocks: [...], markdown_url: "..." }`. Blocks are grouped per page and carry a flat `bbox: { x0, y0, x1, y1 }`. The endpoint accepts an optional `execution_id` to scope results to a specific workflow execution.
**\[API]** Slim split results without nested rows
You can now request slim per-split results that omit nested object rows entirely, and then load nested rows lazily per split via a dedicated endpoint. Each slim split carries a per-split version token so caches can invalidate at the split granularity instead of the whole file.
Pass `?include=splits_lite` on the file results endpoint to receive slim per-split entries (mutually exclusive with `?include=splits`). Use `GET /workflow-versions/{version}/files/{file_id}/splits/{split_id}/object-results/{field_persistent_id}/` to fetch paginated object rows scoped to a single split.
**\[Improved]** Faster Studio category tab on workflows with many splits
The Studio category tab now requests slim payloads and fetches nested object rows lazily per split, so opening a file with many splits no longer waits on full results materialization up front. Bounded prefetching when hovering across many cells also keeps the page responsive.
**\[Improved]** Parse JSON view shows the full block on expand
Expanding a block in the Parse → JSON tab now reveals the complete block object — id, bbox, confidence, content, hyperlinks, rows, and `image_base64` — rendered as inert JSON. Pages are grouped under collapsible sticky headers (collapsed by default) for easier navigation in multi-page documents, and the displayed shape mirrors the public parse-result response exactly.
**\[Improved]** Client-side markdown hydration with embedded images
The Markdown tab now hydrates images directly in the browser using each block's bounding box — including `figure` blocks — so the in-tab view and Markdown downloads stay consistent. The dedicated server-side hydration pipeline has been retired in favor of this lighter approach.
**\[New]** "Beta" badge on Studio nodes still being stabilized
Studio nodes can now display a BETA badge in the sidebar, on the canvas header, and in the sidebar hover card. The **Validate** node is the first to use it, marking it as still being stabilized for general production use.
**\[Fixed]** Workflows created via the typed graph endpoint now run extraction
Workflows created via the typed `with_graph` create endpoint sometimes failed to run extraction and surfaced as `EXTRACTION_FAILED (422)` because the extract node was persisted without an engine. New workflows now always include an engine and execute end-to-end as expected.
`POST /v2/workflows/with_graph/` now persists a default engine on every extract node it creates.
**\[Fixed]** Object rows in sibling extract branches no longer collide
When a workflow had sibling extract branches that defined fields with overlapping names, object rows from one branch could be written against the wrong field. Field schema resolution is now scoped per extract node, so each branch's rows land where they belong.
**\[Fixed]** Parse results reliably appear in the UI for new workflow versions
Some parsed files showed missing pages or stale `parsed_at` because parse callbacks resolved graph nodes by an identifier that wasn't unique across workflow versions. Callbacks now scope to the active workflow version, so parse results are persisted to the correct file every time.
**\[Improved]** Other improvements and fixes
Deleted object rows are now consistently excluded from both per-split and file-level row counts; the file results UI deduplicates parse-result requests when navigating between files; and extraction progress estimates fall back to organization-scoped percentiles instead of cross-tenant numbers.
### May 18, 2026
**\[New]** Handwritten text is now a first-class block type in agentic parsing
Agentic PDF-to-Markdown now recognizes handwritten text as its own block type, with a dedicated extraction strategy that combines a vision-language transcription with OCR-derived hints. Handwritten blocks are labeled "Handwritten text" in the Studio UI, distinct from machine-printed text, and can be tuned independently from regular text extraction.
**\[Improved]** More consistent agentic parsing when text-class blocks have multiple sources
When a text-class block has multiple candidate text sources, the agentic parser now writes the chosen source back into the block's elements and regenerates its segments before rendering. Downstream grounding (overlays, citations) now matches the markdown that's actually rendered.
### May 14, 2026
**\[New]** "Beta" badge on agentic parsing mode
The Parse node mode card now shows a "Beta" badge next to **Agentic parsing**, making it clear that this parse mode is still being stabilized for general production use.
**\[New]** Calibrated confidence scores on smart-table extractions
Both table cells and scalar fields produced by the smart-table extractor now carry a calibrated 0–100 confidence, so review UIs and downstream consumers can prioritize low-confidence values. The scores degrade gracefully when scoring is unavailable, so extractions still complete normally.
Confidence is surfaced on each extracted value's `metadata.confidence` field as an integer in `[0, 100]`. Both judges flip together via the `smart_table_confidence_strategy` setting (`auto` / `judge` / `none`); the default is on.
**\[New]** Review threshold filter for file results
File results now include a confidence-threshold slider that filters Fields and JSON to show only extractions at or below the selected confidence — useful for prioritizing low-confidence rows that need human review. The threshold is persisted across reloads and tabs, and an inline hover card explains how the filter works.
**\[Improved]** Image previews use the PDF viewer toolbar
Image uploads (JPEG, PNG, GIF, BMP, TIFF) now render through the same viewer as PDFs and inherit its zoom, rotate, and full-page controls. Multi-page TIFFs gain a page selector, and the viewer now reports specific errors for unsupported formats or images that exceed browser size and canvas limits instead of failing silently.
**\[Improved]** More reliable dense-table parsing in agentic mode
Agentic PDF-to-Markdown table parsing no longer stalls on dense tables. Reasoning-token budgets are now properly honored across the heal, audit, and sub-region passes; partially valid heal plans are applied operation-by-operation instead of being discarded on a single bad op; and the table-parsing token cap has been raised so large tables are no longer truncated mid-document. Workflows that previously stalled for over 15 minutes now finish in roughly 3.
**\[Improved]** Organization logos stay cached when unrelated settings change
The organization logo URL is now invalidated only when the logo bytes themselves change, not whenever any field on the organization is updated. Renaming the org, toggling support access, or any other non-logo edit no longer forces every client to re-download the logo.
**\[Fixed]** Bulk-download of selected files no longer 400s
`GET /api/v2/saas_manager/workflow-versions/{id}/results/` rejected the `file__id__in` and `file__id__not_in` query parameters with `Field 'id' expected a number but got '...'` whenever the frontend passed UUIDs (the v2 file API now returns file `id` as a UUID string). The filter was still joining against the legacy integer PK; it now joins on the file UUID field, matching the regular file filter. Every bulk-download flow that relies on row selection — and the "select all minus N" pattern — is unblocked.
**\[Fixed]** Stricter validation of logo and workflow file uploads
Logo and workflow file uploads are now validated against their actual bytes via magic-byte sniffing rather than the client-supplied `Content-Type` header, and only file types the parse pipeline can actually process are accepted. Uploading a renamed executable, ZIP, or arbitrary JSON as a workflow file is now rejected at the API boundary instead of silently failing later in the pipeline. Stored objects also carry the correct `Content-Type` on the storage layer instead of being labeled `binary/octet-stream`.
**\[Fixed]** Switching organizations on a resource page no longer reverts
Switching the active organization from a workflow detail page (or any other resource page deep-linked from a different org) could occasionally snap back to the original org after showing a "switched" toast. The selection is now preserved on the first click.
**\[Fixed]** Object-field row counts exclude deleted rows
The paginated object-field results endpoint and the slim workflow-results list both now exclude soft-deleted rows from page contents and the row-count total. Counts shown in the UI and returned by the API now match the rows you can actually see.
**\[Fixed]** Sub-table detail rows no longer clip at the bottom
Expanded sub-tables inside object-field result tables previously cut off the bottom row because the row-height calculation didn't account for the surrounding padding and border. The detail row now renders at full height regardless of how many sub-rows it contains.
**\[Fixed]** Classifier category tabs render again on workflow results
Category tabs on classifier workflow results were rendering empty because the slim results response had stopped including the classifier's `graph_node_id`. The field is back in the response (with a batched prefetch to avoid per-file queries), so files routed by a classifier now appear under their correct tab.
**\[Fixed]** Workflow list updates immediately after create, duplicate, or delete
The home page workflow list previously kept the stale list cached after creating, duplicating, or deleting a workflow, so the change only appeared after a refresh. All three mutations now invalidate the workflow list cache and the list reflects the change right away.
### May 13, 2026
**\[Breaking + API]** Workflow creation no longer accepts file uploads
`POST /api/v2/saas_manager/organizations/{org_id}/workflows/` previously accepted a `multipart/form-data` body to import a workflow from a JSON or XLSX template file. That entry point and its template downloads have been removed; the endpoint now accepts JSON request bodies only.
**Action required:** if you were uploading a JSON or XLSX template to this endpoint, switch to sending the equivalent JSON body directly. The chat-driven and from-scratch flows in Studio are unaffected.
**\[New]** Studio is now generally available
The visual workflow editor (Studio) is now visible to every organization member from the workflow page and the new-workflow page. Write access is still controlled by your workflow role. The legacy schema editor at `/workflows/edit/...` has been removed — the Studio tab is now the single editing surface.
**\[New]** Row-level actions on object-field result tables
Object-field result tables now expose a per-row dropdown with **Validate row**, **Add row above**, **Add row below**, and **Remove row**, plus inline up/down chevrons for adjacent reordering. The trailing-column header has a new **Validate all rows** action that promotes every datapoint in the field to human-verified in a single click.
New endpoints under `/api/v2/datapoints/object-fields/{field_id}/`:
* `POST .../rows/` (with an `index` in `[0, n_live_rows]`) inserts an empty row and shifts live siblings up by one in the same transaction.
* `POST .../rows/{a}/swap/{b}/` exchanges two adjacent live rows.
* `POST .../rows/{position}/validate/` bulk-promotes every datapoint in a row to human-verified.
* `POST .../validate-all/` does the same for every live row of the object field.
All four endpoints require Member role.
**\[New]** Signup vouchers now give live feedback during onboarding
When a user lands on the signup flow with a voucher code, the create-organization screen shows the total welcome credits they will receive (the standard signup grant plus the voucher's value, fetched live from the backend) if the voucher is valid, or an amber "voucher no longer valid" hint if it is expired or unknown. Previously the page was silent in both cases.
`GET /api/v3/iam/signup-vouchers/{code}/` returns a read-only preview of the voucher (`{ code, additional_credits }`) and never consumes a redemption — actual redemption still happens atomically inside organization creation. Returns `404 voucher_not_found` for unknown codes and `410 voucher_expired` for expired or exhausted ones. Responses set `Cache-Control: no-store, private` so admin edits to a voucher are reflected on the next page load.
**\[Improved]** Workflow page list is dramatically faster
The workflow page's file list now uses a slim response that omits per-cell datapoint detail until you actually expand an object row. On a workload of 50 files with 2,000 line-items each, the list now returns in \~240 ms instead of \~6 s and the response payload is roughly 770× smaller. Object cells pre-warm on hover so expanding feels instant; edits to a datapoint refresh the expanded sub-table without a poll cycle.
`GET /api/v2/saas_manager/workflow-versions/{id}/files/?include=results_lite` returns scalar datapoints as `{ id, display, status }` and object fields as `{ row_count }`. Full sub-tables are fetched lazily through `GET /api/v2/saas_manager/workflow-versions/{id}/files/{file_id}/fields/{field_persistent_id}/object-results/`. The existing `include=results` (full) path is unchanged and still available for legacy integrators.
**\[Improved]** Stripe top-up is now open to all users and confirms the purchased amount
The **Top up** button on the Usage page now opens the Stripe checkout dialog for every user — the previous fallback to a `mailto:sales@anyformat.ai` link is gone. After a successful payment, the success page now states how many credits were added and tells the user they can start running workflows immediately.
**\[Improved]** Cleaner extraction on scanned PDFs
The agentic PDF-to-markdown parser now honors the routing model's verdict on which text source to trust (the embedded PDF text layer, OCR, or vision) for every block type, including miscellaneous blocks that previously ignored the verdict. On scanned PDFs where the embedded text layer is corrupted, blocks now render the clean OCR text instead of garbled bytes.
**\[Fixed]** Returning from Stripe checkout no longer fails authentication
If you stayed on Stripe long enough for the short-lived access token to expire, returning to anyformat used to land on an authorization error screen — even though the payment had already succeeded on Stripe's side. The app now silently refreshes the token in the background (and replays the first request that hits a 401), so the top-up success page renders cleanly even after a long checkout.
**\[Fixed]** Cross-tab refresh storm and support-elevation isolation
Opening the app in multiple tabs no longer triggers refetch storms when you navigate inside one tab. As a separate fix, support agents who elevate into a customer organization stay elevated only in the current tab — new tabs default back to their own organization until they explicitly re-elevate.
**\[Fixed]** New users are immediately enabled after signup
Users who created their own organization or requested to join an existing one were previously left in a disabled state until an admin reviewed their request, which blocked the create-org path entirely. Both onboarding paths now enable the profile immediately. Join-request approval is still required to gain membership in the target org.
**\[Improved]** Other improvements and fixes
* Structured-output extractions on Amazon Nova models no longer occasionally fail with an empty-JSON parsing error.
* The Usage page no longer renders the "Page consumption" summary box at the top — the cleaner credit-balance card now stands on its own.
* Long organization lists on the join-or-create page no longer get clipped below the fold.
### May 12, 2026
**\[Breaking + API]** v2 file responses now use UUID hex for file IDs
Every v2 endpoint that previously returned a numeric file ID now emits a 32-character UUID hex string. The OpenAPI schema already declared these fields as UUID strings, so generated SDK clients are unaffected — but raw HTTP integrations that parsed the value as an integer, or that constructed URLs from cached numeric IDs, will need to update. Old integer-shaped URLs return `404`.
**Action required:** treat v2 `file_id` values as opaque strings, and refresh any cached file URLs that embedded integer IDs.
* File detail, upload responses, and file-position (`prev_id` / `next_id`)
* Extraction responses and the verification URL builder
* File filters: `id__in`, `id__not_in`, `file_id__in`, `file_id__not_in` now resolve against UUID hex
**\[New]** Delete rows from object-field result tables
Object-field result tables now have a per-row trash action. Remaining rows are reindexed in place so curated positions stay stable and your verification carries over correctly when the file is re-extracted. Rows the model produced are soft-deleted (so accuracy metrics still reflect the model's miss); rows you added yourself are removed entirely.
`DELETE /api/v2/datapoints/object-fields/{id}/rows/{position}/`. Deletion is transactional. A partial unique constraint allows deleted rows to retain their original index, and the result-building SQL filters soft-deleted rows.
**\[API]** Workflow responses now include `organization_id`
Workflow and workflow-version responses (including the nested `versions` collection on workflow detail) now expose a normalized 32-character hex `organization_id`. Useful when a client needs to identify which org owns a workflow without an extra round-trip.
**\[Improved]** Tax ID and billing address now collected during Stripe Checkout
Top-up Checkout sessions now collect the customer's tax ID and billing address before invoices are finalized. This unblocks invoice correctness in EU/UK jurisdictions. No action required for existing customers — the values are persisted on the next Checkout.
**\[Improved]** Smoother markdown viewer on long documents
The "visual" markdown view is significantly snappier on long documents. Scroll-driven page detection is now frame-throttled and only fires on real page transitions, sections subscribe to fewer state changes, and highlight-scroll listeners share a single event bus instead of one per section.
**\[Improved]** Faster agentic PDF-to-markdown parsing
Agentic PDF→Markdown parsing now parallelizes work per page — single-call vision blocks fan out and agentic blocks run in a small per-page pool, so page latency tracks the slowest block instead of the serial sum. Layout-routing calls were also tuned for lower latency.
**\[Fixed]** Org switcher no longer goes blank for orgs without a logo
The organization switcher trigger now always renders the org's initials as the base layer and only overlays the logo image once it loads successfully — so switching to an org without a logo no longer flashes blank.
**\[Improved]** Parse confidence is now reported even when the model omits logprobs
Per-block parse confidence is now computed and stamped on the rendered markdown regardless of whether the parsing model returns token-level log probabilities. Downstream consumers can read `data-parse-confidence` consistently across all supported parsing providers.
### May 11, 2026
**\[Breaking + API]** v2 SaaS Manager list, create, and member endpoints now require an organization in the URL
The bare `/api/v2/saas_manager/...` list and create paths, and the legacy `current` organization shortcuts, have been removed. Workflow versions, file collections, member management, and suggestion routes must now include the org id in the URL. Suggestion endpoints additionally require Member role — Viewers get `403`.
**Action required:** template the org id into the URL for every v2 SaaS Manager request that previously relied on a bare list/create path or the `current` shortcut.
* `GET / POST /api/v2/saas_manager/workflow-versions/` → `.../organizations/{org_id}/workflow-versions/`
* `GET / POST /api/v2/saas_manager/file-collections/` → `.../organizations/{org_id}/file-collections/`
* `PATCH /api/v2/saas_manager/organizations/current/` → `.../organizations/{id}/`
* `PATCH /api/v2/saas_manager/organizations/current/logo/` → `.../organizations/{id}/logo/`
* `GET / POST /api/v2/saas_manager/organizations/current/members/` → `.../organizations/{org_id}/members/`
* `DELETE / PATCH /api/v2/saas_manager/organizations/current/members/{user_id}/[role/]` → `.../organizations/{org_id}/members/{user_id}/[role/]`
* `POST /api/v2/suggestions/{fields,field-description,workflow-description,workflow-name,upload-sample}/` → `.../organizations/{org_id}/suggestions/{...}/`
**\[New]** Self-service invoice downloads from the usage page
Owners and admins can now open Stripe's hosted Customer Portal from the Usage page to download invoices for past top-ups. Members, reviewers, and viewers don't see the button — invoices are financial documents.
`POST /api/v3/billing/organizations/{org_id}/portal-sessions/` mints a short-lived portal URL bound to the org's billing customer record. Orgs that have never run a top-up return `409 stripe_customer_not_provisioned`; the UI surfaces this as a "Top up first" toast. New environment variable `STRIPE_BILLING_PORTAL_RETURN_URL` falls through to `${FRONTEND_URL}/usage`. Configure the operator side (Invoice History toggle, ToS / privacy URLs, default return link) in the Stripe dashboard.
**\[Improved]** Cross-tenant probes on v3 detail endpoints now return 404
v3 file results, file-collection extraction status, file-collection results, and webhook delete now derive the active organization from the resource itself. URLs are unchanged. Cross-organization requests get a `404` to mask resource existence; same-org callers are unaffected.
**\[Improved]** Hovering a stacked bounding box highlights every overlapping box
When several bounding boxes overlap, hovering reveals each one stacked under the cursor instead of just the topmost. Rapid mouse movement is collapsed to one diff per frame so the UI stays responsive.
**\[Improved]** Consistent extracted-object UI across Define, Refine, and File Results
Object and table fields now render the same way across the schema builder, refine step, and file-results panel — same headers, same skeletons, same status states. Refine correctly shows error / cancelled copy on failed extractions instead of an empty data view.
### May 9, 2026
**\[Breaking + API]** Detail and per-resource v2 endpoints no longer read `X-Current-Org`
Datapoints, manual datapoints, extraction results, workflow-version files, file collections, workflow versions, and workflow detail endpoints now derive the active organization from the resource itself. Non-resource list/create endpoints (API key CRUD, manual-datapoint create, workflow list/create) moved under `/api/v2/saas_manager/organizations///`.
**Action required:** stop sending `X-Current-Org` to v2 endpoints — derive the org from the resource (URLs unchanged for detail) or from the new org-scoped URL for list/create. Non-members on a same-resource probe now get `404`; in-org callers with insufficient role get `403`.
### May 8, 2026
**\[API]** Create workflows with full graph definition in a single call
You can now define a workflow's parse, classify, split, and extract steps in one request — the workflow, its initial version, and all extraction fields are created atomically. The existing workflow-create endpoint is also fully typed, so SDKs get auto-complete and field-level validation.
`POST /v2/workflows/with_graph/` accepts the complete graph plus per-extract-node fields and creates everything in a single transaction. The original `POST /v2/workflows/` body is now declared with discriminated field types.
**\[New]** Python builder for typed workflows
The Python SDK now ships a fluent `Workflow` builder: `.parse().classify(...).extract(...).split(...).build()` produces a fully validated request. `branch=` and `route_from=` accept either an id string or the typed category/rule you registered earlier, so typos fail at the call site instead of at build time.
**\[Improved]** Workflow graph validation catches more errors before run
Non-branching nodes with multiple outgoing edges, missing `route_from` on classify→split chains, and broken graph transitions are now rejected with clear validation errors instead of producing confusing runtime failures. The new typed graph payload also rolls back cleanly when persistence of one node fails partway.
**\[New]** Voucher codes on signup grant extra wallet credit
Organization creation now accepts an optional `voucher_code` that grants extra wallet credit to the new org. Vouchers (expiry, usage cap, amount) are managed in the admin panel; invalid or expired codes surface as a clear `400` instead of breaking signup.
**\[Improved]** Doc-viewer table polish — resize, unified sync toggle, safer persisted state
The object-results table is now vertically resizable with sensible bounds (header + one row min, all-rows-visible max). The PDF toolbar's separate auto-focus and auto-scroll preferences collapse into a single "Sync document and results" toggle, and the lookup-files dialog no longer overflows. Co-mounted views in the same tab stay in sync, and corrupt persisted preferences self-recover instead of breaking the UI on load.
### May 7, 2026
**\[Improved]** Org isolation on v2 detail endpoints is now resource-derived
Detail endpoints (datapoints, manual datapoints, extractions, workflow-version files, file collections, workflow versions, workflows) now authorize the caller's role against the resource's organization instead of a request header. Cross-tenant probes return `404`; in-tenant callers with insufficient role return `403`. URLs are unchanged.
**\[Improved]** More accurate extraction confidence via LLM judge
For extractions where the model doesn't return logprobs (or where you prefer the judge), a separate judge model now scores each extracted value against its source markdown and outputs a calibrated 0–100 confidence. Object and table cells are batched for cost efficiency. Choose the strategy per task with `extraction_confidence_strategy = none | auto | judge`.
**\[Fixed]** Login no longer spins on token-exchange failures
The post-login callback now exits cleanly when token exchange or profile fetch fails: a 15-second watchdog renders an error page with a logout option, OAuth errors surface explicitly, and tokens are purged on error.
**\[New]** Bounding boxes for split + extract workflows
The PDF viewer now renders bounding boxes for split + extract workflows by scoping mapped locations per panel — the matching split when navigating by partition, the file's extraction otherwise. Parse, split, classify, and validation panels paint no boxes, as intended.
**\[Fixed]** Renamed split categories no longer lose historical sub-documents
When you rename a split rule across versions (e.g. "Invoice" → "Bill"), historical sub-documents continue to surface under the renamed tab — the UI now filters by the stable split-node identifier instead of the display string.
### May 6, 2026
**\[New]** Top up credits with Stripe Checkout
You can now purchase wallet credit through Stripe Checkout from the Usage page. Sessions are idempotent (no duplicate charges on retry), and every successful top-up generates an invoice you can download later from the Customer Portal.
`POST /api/v3/billing/organizations/{id}/checkout-sessions/` validates the requested credit count against per-session min/max bounds and returns a hosted-page URL. Inbound `checkout.session.completed` webhooks are HMAC-verified and serialized to guarantee exactly-once credit grant under contention.
**\[New]** File results panel with explicit processing states
The per-file extraction panel now renders explicit Unprocessed, Processing, Error, and Processed states with run and retry actions. Heavy result queries only run once the file is actually processed, removing wasted requests and inconsistent loaders for unprocessed files.
**\[New]** Search inside the PDF viewer
The PDF viewer now has a toolbar search bar with case-sensitive/insensitive matching, prev/next navigation that wraps, a `Cmd/Ctrl-F` shortcut, and cross-page highlighting. Search stays responsive on large PDFs thanks to debounced lazy text extraction.
**\[API]** Workflow results now expose classifications, splits, and flat extractions
`GET /v2/workflows/{wid}/files/{cid}/results/` now returns `classifications`, detailed `splits` (with per-file pages and partitions), and a flat `extractions` list keyed by `(split_name, partition)` alongside the existing fields. The v3 file-collection results payload exposes the same keys in place — no new endpoint required.
**\[Improved]** Org switcher orders by ownership, membership, then support access
Organizations are now returned in three alphabetically-sorted buckets — owned, member, support access — replacing the prior database-discovery order. Helpful for accounts that belong to many orgs.
**\[Fixed]** Bulk extraction status no longer marks processed files as pending
Bulk status resolution now aligns identifier formats and queries root extractions only, so processed parents are correctly reported instead of stuck on "pending".
**\[Improved]** Other improvements and fixes
File status badge in the file-results header, PDF toolbar staying visible on multi-line wrap, clearer toasts when a dropped file's type isn't accepted, and a fix for crashes on empty drag-and-drop selections.
### May 5, 2026
**\[Improved]** Smart Lookup retries based on residuals
Smart Lookup now wraps the worker in an orchestrator + auditor loop that can iterate up to three times if residuals or tool-call signals suggest more work is possible. Search blends BM25 and Jaro-Winkler matching with a per-call query cap to improve recall while keeping runtime bounded.
**\[Improved]** Splits panel is now navigable
Clicking a split or partition now switches the extraction tab, persists the selected partition in a URL search param, and scrolls the PDF viewer to the matching page. Refresh-safe deep links to a specific partition just work.
**\[Fixed]** Duplicating workflows with reused field names no longer fails
Workflows that contain multiple extract nodes reusing the same top-level or nested field names can now be duplicated cleanly — field lookups are keyed per source node.
**\[Fixed]** Classify-only workflows handle unwired LLM categories
When the classifier returns a category with no downstream edge, the run now falls through to the end instead of crashing. Classify-only workflows also persist classification records so results are available afterwards.
**\[Improved]** Smart-table list-of-object rows are page-ordered
Smart-table list-of-object extraction rows are now sorted by source page and block, so output is stable and easy to align with the source document across runs. Rows with missing page info are placed last.
**\[Improved]** Cleaner empty states for splits and category grids
Workflows with no rows now render a clean overlay in the splits and category grids instead of stale placeholders.
### May 4, 2026
**\[Breaking]** Audio uploads are no longer accepted
The dropzone no longer accepts audio files (`.mp3`, `.wav`, etc.). Existing audio files already in your collections remain viewable and downloadable.
**Action required:** if your integration uploaded audio for transcription, switch to a supported format. No deprecation window — audio uploads are rejected starting today.
**\[Fixed]** Authentication errors now return the correct status codes
Unauthenticated requests now return `401` (with a `WWW-Authenticate` header) instead of being coerced to `403`. Missing email-claim or unknown-key authentication paths return `AuthenticationFailed` cleanly instead of `500` errors.
**\[Improved]** Profile-fetch failures show a clear error page
Repeated `/profile` failures after login now surface an error page with a logout option instead of stranding users on a spinner. Global query errors are also reported with the failing endpoint for faster triage.
**\[Fixed]** Corrupted local preferences no longer break the UI on load
Persisted UI preferences (sync toggles, panel sizes, etc.) are now safely parsed; corrupt keys are dropped on initial load and reported to monitoring with the offending storage key.
## April 2026
### April 30, 2026
**\[New]** New organizations get 5,000 free credits
Every new organization is now automatically granted 5,000 credits on creation, recorded as a standard billing transaction.
### April 28, 2026
**\[Breaking + API]** Split confidence is now a 0–100 integer
`SplitRange.confidence` is now an integer in `[0, 100]` end-to-end (graph output, backend validation, database, and frontend rendering) instead of a 0–1 float. Existing values were migrated automatically.
**Action required:** if your integration reads `confidence` from split results, expect integers (`87`) instead of floats (`0.87`). Confidence badges no longer render `0.87%` or `8700%`.
**\[Breaking + API]** Workflow results response shape is now canonical
`GET /v2/workflows/{wid}/files/{cid}/results/` now always returns `{collection_id, verification_url, parse, extraction}` — parse-only workflows return `extraction: null` instead of omitting the key. The internal `field_id` no longer leaks in responses.
**Action required:** if your integration relied on `extraction` being absent for parse-only workflows, check for `null` instead. Cookbook examples and the API reference have been rewritten to match.
**\[New]** Classify nodes return evidence and confidence
Classify nodes now return a structured result with category, evidence, and confidence. The UI surfaces confidence and evidence in the category data viewer, with a dedicated Classify tab at workflow level and a per-file Classify tab with verdict cards. The new `classifications` include option exposes the same data via the file API.
**\[New]** HTML files render inside the document viewer
You can now upload `.html` and `.htm` files and view them in the document viewer. Files render inside a sandboxed, sanitized iframe to block script execution and same-origin access.
**\[API]** Better OpenAPI for SDKs and try-it surfaces
`POST /v2/workflows/` now declares its JSON request body, so SDKs and AI agents stop falling back to raw `extra_body` dicts. Bearer authentication is now declared as a proper API-key security scheme, so try-it panels render the lock icon and treat auth as required. Webhook responses now expose `url` as an `HttpUrl` and `created_at` as a `datetime`.
**\[Improved]** Smart-table search now reaches inside table cells
Smart-table search now scans both page prose and table cells, returning cell matches with `{table, row, column, value}` provenance. Scalar values inside tables are no longer dropped behind prose hits.
**\[Improved]** Per-category accuracy and confidence charts in monitoring
The monitoring tab now groups per-field accuracy and confidence charts into per-category sections for multi-schema workflows, with sensible fallbacks and an improved empty state.
**\[Improved]** Validate view now shows results for classify-routed files
Per-file validation now displays extracted data for files routed via Classify by synthesizing entries from the file's results when pinned to an extract node. The partition selector and sibling tabs are hidden when they don't apply.
**\[Fixed]** Improved processing of `.docx` and `.msg` uploads
`.docx`, `.msg`, and other non-PDF uploads now hydrate from their converted PDF instead of feeding the original binary into the PDF parser. Markdown for these formats is now correct and no longer triggers spurious "corrupt PDF" warnings.
**\[Improved]** Other improvements and fixes
Reliable PDF callback uploads via short-lived presigned URLs (no more "Request Too Big" errors on large rendered PDFs), file-detail columns in splits and per-category tables, `search_text` helper exposed in the smart-table sandbox, object/list extraction fields no longer dropped from v3 results, timeouts and retries on frontend list requests, lighter `/files` list payloads, and `eval` and XFA disabled in the PDF viewer for a smaller attack surface.
### April 27, 2026
**\[Fixed]** Smart-table extraction no longer drops scalar fields
Smart-table extraction plans now must assign every schema field to either a table or a `scalar_fields` list, rejecting incomplete plans so scalar fields can no longer be silently dropped.
**\[Fixed]** Master-file + extract workflows now run without error
Non-split workflows that combine a master-file node with an extract node now run cleanly — master-file nodes can augment an extract branch without triggering single-non-split-node validation.
### April 24, 2026
**\[Improved]** Tabbed data viewer on workflow detail
Reworked the workflow detail data viewer into a tabbed layout with shared grid skeletons and improved loading behavior. Added an inline workflow-name editor and tighter studio reload/loader behavior when workflows change.
**\[Fixed]** Empty splits tab now shows a clean overlay
The splits tab now uses a proper no-rows overlay (resolving the previous "Element type is invalid" error) and filters out splits with no results, so empty entries no longer appear.
**\[Improved]** More content captured during PDF parsing
Text missed by layout segmentation now reaches downstream parsing through orphan OCR detection and synthetic text-block injection. Orphans are clustered into lines, merged into nearby blocks where appropriate, or promoted to new blocks; redundant smaller blocks are absorbed to prevent duplicates.
**\[Fixed]** Date parsing in the smart-table sandbox no longer silently returns `None`
The sandbox now allows `datetime.strptime`'s runtime dependencies, so parsed dates are correctly returned instead of being silently converted to `None`.
**\[Fixed]** Smart-table no longer overwrites populated fields with empty values
Storing an empty list can no longer overwrite an already-populated list field (e.g. a transactions table). Column-statistics summaries now clearly label examples and indicate additional unique values so the worker doesn't mistake samples for full datasets.
**\[Improved]** Better table merging when headers differ
Same-title tables with differing headers are disambiguated to avoid dropping rows during merge, and the block-parsing prompt and markdown assembly capture text the model sees that isn't covered by any block id.
**\[Fixed]** Classifier results survive workflow amendments
Category queries continue to return populated classifications and datapoints after prompt adjustments — the version-compatible result builder now maps datapoints across amended versions by their stable persistent identifier.
### April 23, 2026
**\[Fixed]** Partitioned splits now flow into Validate and Smart Lookup
When a split rule declares a `partition_key` and the downstream extract is wired into a Validate or Smart Lookup, each partition is now processed independently — one validation pass per partition, one lookup pass per partition — and the results attach to the right per-partition child extraction. Previously the downstream node saw nothing.
**\[Improved]** Per-partition fan-out is now bounded
Per-partition fan-out is now capped (default `4` parallel branches) so a 100-partition document running through extract → validate → lookup doesn't saturate the model provider.
### April 20, 2026
**\[Breaking]** Smart Lookup is now part of Extract
The standalone Smart Lookup node has been removed — lookup behavior now lives on the Extract node. You manage lookup files directly from the Extract menu, enable lookups per-field via a checkbox in the field editor, and see inline badges when lookup files are missing.
**Action required:** none for most users — existing workflows migrate automatically and inherit Extract's model settings. If you scripted Smart Lookup as a standalone node, switch to enabling lookup fields on Extract.
**\[Breaking]** Smart Table is now an agentic mode on Extract
The standalone Smart Table node has been removed — replaced by a Standard / Agentic mode selector on the Extract node.
**Action required:** none for most users — existing Smart Table workflows migrate automatically to Extract with `mode="agentic"`.
**\[New]** Splits results tab in workflow results
Added a Split tab in the extraction results UI showing split-group cards and a collapsible per-page table, plus a `document_split_groups` include option on the workflow-version file endpoint with category, partition value, page range, and confidence.
**\[Improved]** Redesigned Classify and Split menus
Both nodes now share a Config / Options tab layout with validated card-based editors, uniqueness checks, reserved "Other" handling for Split, click-to-edit from the Config preview tables, and cleaner Save-from-header behavior that closes open forms first.
**\[API]** Split groups consolidated by category and partition value
`document_split_groups` responses now group rows by `(category, partition_value)` and return consolidated `page_ranges` arrays instead of per-row `page_start` / `page_end` fields.
**\[Improved]** Better figure parsing in PDFs
The figure-enhancement prompt now uses chart-type-specific strategies, handles multi-panel figures, gives statistical-annotation guidance, and applies column-header rules. Segment deduplication now preserves the largest picture blocks to avoid losing visual context.
**\[Improved]** Inline field-level errors in the field editor
Replaced the top-level field-form error banner with inline, field-adjacent errors, column-level highlighting, and deduplicated summary messages for enum options and nested fields.
**\[Improved]** Default extraction model upgraded to `gpt-5.2`
The default extraction model is now `gpt-5.2` across markdown and PDF extraction paths.
**\[Fixed]** OneDrive and Google Drive imports work reliably in production
Fixed the cloud-connector reliability issues that affected OneDrive and Google imports in staging and production: a stricter content-security policy now allows the required SDK scripts and frames, SDK load errors surface instead of failing silently, consumer-vs-business OneDrive accounts are detected correctly, and a popup-polling leak is gone.
**\[Improved]** Other improvements and fixes
Validation tab only appears when the workflow includes a Validate node and the file has an extraction; `latest_version` endpoint returns a clean `400` for invalid workflow UUIDs instead of a noisy `500`; OAuth callback token exchange no longer stalls; Smart Lookup handles nested object fields correctly; agentic-extraction description typo fixed; partition tools only exposed when split rules define a partition key; null figure-parsing responses no longer crash; markdown viewer no longer has an infinite scroll loop and the markdown download button moved to the top of the panel.
### April 14, 2026
**\[New]** Redesigned Workflow Studio
The Studio now ships redesigned Parse and Extract node menus, smart node placement with auto-connect, a way to add connected nodes from an output handle, empty-state guidance, an updated sidebar with hover cards, and save-from-header with per-node validation errors.
**\[New]** Validate node for cross-document rule validation
A new Validate node type lets you express cross-document rules that run after extraction.
**\[New]** XML file support
Added an XML-to-PDF converter for files with embedded base64 attachments, so XML files extract end-to-end.
**\[Improved]** API keys are hashed at rest, with multiple named keys per user
API keys are now hashed at rest, support naming, and you can hold multiple keys per user — better security and easier rotation.
**\[Improved]** Per-cell parse confidence with logprob breakdowns
Parse confidence now uses cell-level logprobs when available, with per-cell and per-row logprob breakdowns for table blocks.
**\[Improved]** Studio warns when leaving with unsaved changes
The Studio now blocks tab switches when configuration is dirty or an inline editor is open, and preserves in-progress work across tab navigation.
**\[Improved]** Faster, paginated billing usage
The billing usage table is now server-side paginated with summary cards, dramatically improving query performance on large datasets.
**\[Improved]** Visual grounding toggle for local-model compatibility
Added a toggle to disable visual grounding for environments where the local model doesn't support it.
**\[API]** Files nested under workflows; multi-node results; richer OpenAPI
File endpoints are now nested under workflows. A new multi-node results endpoint returns results across multiple graph nodes in one call. The OpenAPI spec is now enriched for better SDK documentation, and the billing API gained pagination, filtering, and ordering.
**\[Improved]** Other improvements and fixes
OCR confidence is now included in segment and table-cell metadata; the schema builder uses a local config store for faster field editing; toast colors, mobile sidebar, long descriptions in choice fields, lookup-file display overflow, and schema editor header sizing fixes; corrupt-PDF hydration errors are now downgraded to warnings.
### April 6, 2026
**\[New]** Agentic document splitter
The batch LLM splitter has been replaced by an agentic workflow. You can now define custom split rules (name, description, optional partition key for grouping repeating sections) directly in the Studio SplitPages node, with a built-in "Other" catch-all rule.
**\[Fixed]** Non-figure blocks no longer receive figure wrapping
Generic non-figure blocks are no longer treated as pictures, so only true figures receive screenshot crops and figure markup in PDF-to-markdown output.
**\[API]** Removed duplicate health check endpoint
A duplicate health check endpoint has been removed from the API surface.
### April 1, 2026
**\[Breaking + API]** Workflow results endpoint always returns unified JSON
`GET /v2/workflows/{id}/results/` now always returns unified JSON (parse markdown plus extraction data per file). The `output_format`, `as_lists`, and file-filter query parameters have been removed; filter to a single file with the new `file_id` query parameter.
**Action required:** if your integration relied on `output_format`, `as_lists`, or the legacy file-filter param, remove them and use `file_id` instead.
**\[Improved]** Workflow usage endpoint supports pagination and ordering
The workflow usage endpoint now supports server-side pagination (`page`, `page_size`), ordering, and database-level filtering with the standard `results` response wrapper.
**\[API]** v1 routes restored with deprecation headers
v1 routes (workflows, jobs, uploads/results) are back with `Deprecation` and `Sunset` response headers and `X-API-Key` auth fallback for backward compatibility.
## March 2026
### March 30, 2026
**\[Improved]** Rate limits split between submission and general endpoints
Rate limits are now applied independently to file-submission endpoints (60 RPM) and general endpoints (600 RPM). Submission operations (run, upload, create file collection) have a stricter limit; read and management endpoints share a higher limit. The two tiers use independent counters.
### March 26, 2026
**\[Breaking + API]** Removed deprecated `as_lists` parameter from the v2 extraction endpoint
The `as_lists` query parameter on the v2 extraction endpoint has been removed.
**Action required:** drop `as_lists` from any v2 extraction call. The response shape is the unified default introduced earlier this month.
**\[New]** Paste images into the workflow creation chat
Paste images directly into the workflow creation chat for more contextual AI suggestions (up to 3 images, 3 MB each).
**\[Improved]** Concurrent edit protection on workflow saves
Workflow configuration saves now detect concurrent edits and surface them, preventing accidental data loss when two users edit at once.
**\[Fixed]** Image uploads in workflow suggestions accept files within size limit
Image uploads in workflow suggestions are no longer rejected when they're within the allowed size limit.
**\[Improved]** Other improvements and fixes
Workflow-name loading indicator no longer gets stuck; buttons now show the pointer cursor on hover.
### March 25, 2026
**\[Breaking + API]** v2 extraction is now collection-first
Run extraction now returns a file-collection UUID; poll results via `GET /v2/files/{id}/extraction/` (`412` while pending, `200` when ready).
**Action required:** if your integration parsed extraction results synchronously from the run-extraction response, switch to polling the new collection-status endpoint.
**\[Breaking + API]** Upload endpoint no longer accepts `file_collection_id`
`POST /v2/files/` no longer accepts a `file_collection_id` parameter — collections are immutable after creation.
**Action required:** create a new collection per upload batch instead of appending to an existing one.
**\[Breaking + API]** Deprecated `/jobs/` endpoints removed from the v2 API
The deprecated `/jobs/` endpoints are gone.
**Action required:** use `GET /v2/files/{id}/extraction/` to poll extraction state instead.
**\[Breaking + API]** Simpler v2 pagination shape
v2 pagination responses now contain `count`, `page`, `page_size`. The old `total_pages`, `next`, and `previous` fields are gone.
**Action required:** compute next/previous page numbers from `count`, `page`, and `page_size` client-side, or rely on `page` boundaries (`1` and `ceil(count / page_size)`).
**\[Improved]** Upload multiple files in a single request
`POST /v2/files/` now accepts multiple files in a single request.
**\[API]** Customer-facing docs now show Bearer authentication
Customer-facing API docs now reflect the `Authorization: Bearer ` authentication method.
**\[API]** v2 documentation reflects path-based versioning
All API documentation has been updated for v2 endpoints with path-based versioning (`/v2/` prefix).
### March 19, 2026
**\[New]** EML and MSG email file support
You can now upload `.eml` and `.msg` (Outlook) email files for extraction. Embedded images in EML files are correctly inlined during conversion.
**\[API]** New `allow_extend` parameter to append rows on submit
Submit now supports an `allow_extend` parameter that lets agents append rows to existing submissions instead of replacing them.
**\[API]** v3 types and graceful v2-to-v3 delegation
Updated v3 types, v2 file endpoints now delegate to v3, and v3 errors are translated cleanly for v2 callers.
**\[Improved]** Better OCR text detection
OCR text detection now uses word-count and spatial-coverage heuristics instead of simple truthy checks, and the screenshot is always sent to the model for context.
**\[Improved]** Smart-table resubmit replaces rows instead of skipping
Smart-table resubmit now replaces the previous output instead of skipping, with scoped worker context and a consolidated briefing.
**\[Improved]** Per-user rate limits via API keys
Authentication and rate-limiting are now separated, with per-user rate limits derived from API key identity.
**\[Improved]** HTTP security headers added per pentest findings
Added HTTP security headers across the API per recent pentest recommendations.
**\[Fixed]** Bounding boxes align on pages with different dimensions
Bounding boxes no longer drift on documents that mix page sizes.
**\[Improved]** Other improvements and fixes
Table grounding improvements (HTML cell-id injection now gated and tuned), better login-page rendering with static assets loading correctly, cleaner full-page loader on the auth callback, and PDF viewer fixes for documents that mix page sizes (e.g. US Letter and A4).
### March 18, 2026
**\[New]** v2 API with versioned endpoints, pagination wrappers, and improved docs
A full v2 API surface ships: versioned endpoints, pagination wrappers, workflow routes, list-extractions, provenance API, and richer OpenAPI documentation.
**\[API]** Bearer authentication alongside API key
The API now supports the standard `Authorization: Bearer` header alongside the existing API key header.
**\[API]** API rate limiting
The API now enforces per-endpoint rate limits.
**\[Improved]** Smart-table extraction planning with table-scoped delegates
The smart-table extraction planner now uses table-scoped delegates, agent limits that scale with document complexity, and improved sandbox tools.
**\[Improved]** Better row-level grounding for tables
Row-level grounding for tables is now more robust via token-overlap pre-mapping and accuracy fixes to anchor finders.
**\[Improved]** Faster page rotation
A cascade decision engine with landscape support and optimized center-crop reuse makes page-rotation detection faster.
**\[Improved]** File metadata toggle and filtered counts
A new metadata visibility toggle on the table header persists locally. File counts now reflect filtered results instead of selected rows, and action buttons show loading state.
**\[Fixed]** File-collection creation works in production
A regression that returned `403` for all file-collection creation calls in production has been fixed.
**\[Fixed]** File download naming
Markdown downloads no longer end up with doubled file extensions.
**\[Improved]** Other improvements and fixes
Security update for vulnerable dependencies; a botched migration that assigned the same UUID to all files has been corrected.
### March 5, 2026
**\[New]** Raw JSON viewer alongside validated results
Raw JSON extraction results are now available in a dedicated tab alongside validated results, for easier inspection.
**\[Improved]** Better document layout analysis
Enhanced layout analysis for more accurate table and section detection.
**\[Improved]** Hover cards on simple field results
Simple field results now display hover cards with additional context.
**\[Improved]** Resizable workflow creation chat
The workflow creation chat panel is now resizable.
**\[Improved]** Refined validation result cards
Validation result cards have refined styling that makes validated datapoints easier to identify at a glance.
**\[Fixed]** Confidence handling for undefined values
Confidence scores no longer break when a value is undefined.
**\[Fixed]** Clipboard copy errors now show clear feedback
Clipboard copy errors are handled and surfaced with clear feedback instead of failing silently.
**\[Improved]** Other improvements and fixes
UI styling consistency improvements across result cards and the file viewer.
### March 4, 2026
**\[New]** Production-grade webhook system
You can now create webhook subscriptions, receive real-time event notifications, and configure automatic retries on failure.
**\[Improved]** Better error categorization
Error messages now use clearer categories, making it easier to understand and resolve extraction issues.
**\[Improved]** Schema editor auto-saves
Fields and schema changes now auto-save when switching tabs or editing nested fields, preventing accidental data loss.
**\[Improved]** Validation page with keyboard navigation
The validation page now has improved result cards with keyboard navigation and better loading states.
**\[Improved]** Faster OCR processing
OCR text recognition is now significantly faster on large documents thanks to GPU-accelerated batch processing.
**\[Fixed]** Race conditions in field-form submission
Race conditions in field-form submission and save-state management have been eliminated.
**\[Fixed]** Security vulnerability in a third-party dependency
Fixed a security vulnerability in a third-party dependency.
## February 2026
### February 27, 2026
**\[Improved]** Custom date range filter on the usage page
The usage page now lets you filter by a custom date range instead of a fixed year, giving more precise control over billing and usage reports.
**\[Fixed]** Reverting to the original value marks fields as verified
Editing a field value back to its originally extracted value now correctly marks it as human-verified rather than human-edited.
### February 25, 2026
**\[Improved]** Better OCR accuracy
Upgraded text recognition with better support for Latin languages.
**\[Improved]** LLM cost tracking in billing
Billing transactions now include per-extraction LLM token counts and costs for full cost transparency.
**\[Improved]** More accurate table extraction
Improved accuracy when extracting data from tables with closely spaced rows or surrounding text.
**\[Improved]** Faster processing during peak usage
Extraction now scales to handle high volumes, reducing wait times during busy periods.
**\[New]** Sample workflows on new accounts
New accounts are now prepopulated with sample workflows and documents for a guided onboarding experience.
**\[Fixed]** Smart-lookup crash on empty corpus
Smart lookup no longer crashes when the search corpus is empty.
**\[Improved]** Faster extraction callbacks and field-list endpoints
Performance improvements on extraction callbacks and field-list endpoints.
**\[Fixed]** Readable file download filenames
File download filenames are now readable by the frontend.
### February 21, 2026
**\[Improved]** Visual grounding 2.0
Completely revamped evidence highlighting with block-level and element-level precision, making it easier to trace extracted values back to their exact location in the source document.
**\[New]** Billing dashboard
New usage dashboard with remaining credits, weekly summaries, per-workflow usage breakdown, and CSV export.
**\[Improved]** Advanced document layout analysis enabled in production
The advanced document segmentation pipeline is now enabled in production for improved table and section detection.
**\[Fixed]** Bounding-box highlights no longer stuck after navigating fields
Highlights are now cleared correctly when you move between fields.
**\[Fixed]** Confidence calibration
Fixed calibration configuration for confidence scoring.
### February 14, 2026
**\[New]** Redesigned home page
A personalized home with recent workflows, at-a-glance file-status indicators, and a streamlined workflow creation experience with templates.
**\[New]** Redesigned schema builder
A new two-step Define & Refine flow with AI-powered field suggestions, inline file previews, and drag-and-drop uploads.
**\[Improved]** Faster page rotation
About 2× faster page-orientation detection per page.
**\[Improved]** More reliable AI field suggestions
Field suggestions now use an upgraded model for higher reliability.
**\[Fixed]** Smart-lookup settings saved on publish
Smart-lookup settings are now correctly saved when publishing a workflow version.
**\[Fixed]** Workflow studio sidebar overflow
Fixed sidebar overflow in the workflow studio.
### February 7, 2026
**\[Improved]** New dashboard visual design
Refreshed visual design across the platform with updated components, colors, and layout.
**\[Improved]** Faster, more reliable evidence highlighting
Evidence highlighting now uses an event-driven architecture, making interactions faster and more reliable across all views.
**\[Improved]** Files and organization logos served securely through the backend
Files and organization logos are now served securely through the backend, improving security and simplifying local development.
**\[New]** AI workflow-name suggestions
New AI-powered workflow-name suggestions based on your description and uploaded files.
**\[Improved]** File status counts in workflow listings
Workflow listings now show at-a-glance counts of files by processing status.
**\[Fixed]** Document-element classification
Fixed a classification regression that could cause incorrect layout detection in some documents.
### February 3, 2026
**\[New]** Smart Lookup
New feature to enrich extracted data with external reference files using AI-powered matching.
**\[Improved]** Field source tracking
Fields now track their source (extraction vs. smart lookup) for better traceability.
**\[Fixed]** Manual field creation processing
Field data processing now correctly handles manually created fields.
## January 2026
### January 27, 2026
**\[New]** Usage-based billing
Introduced billing for data extraction operations with transparent per-page pricing.
**\[Improved]** Faster, more reliable field suggestions
Field suggestions are now faster and more reliable when creating workflows.
**\[Fixed]** Field value upload formats
Resolved an issue where certain field value formats could cause upload errors.
### January 20, 2026
**\[Improved]** Faster OCR processing
Document processing speed improved with optimized text recognition.
**\[New]** Extraction review from the dashboard
Administrators can now review extractions directly from the dashboard.
**\[Improved]** Organization credits management
Added the ability to manage organization credits for usage billing.
### January 13, 2026
**\[Improved]** Faster document processing
Significantly improved processing speed for large documents.
**\[New]** Filter results by row count
Filter extraction results by the number of rows extracted.
**\[New]** Configurable per-workflow page rotation
Configure custom page-rotation strategies per workflow.
### January 6, 2026
**\[Improved]** Enhanced results filtering on download
Enhanced results download with additional filtering options.
## December 2025
### December 15, 2025
**\[Improved]** Improved reliability
Enhanced system stability and faster deployments.
### December 1, 2025
**\[Improved]** Faster results retrieval for workflows with many documents
Optimized results retrieval for workflows that span many documents.
**\[Improved]** Detailed usage tracking
Added more detailed usage tracking for extraction operations.
## November 2025
### November 24, 2025
**\[New]** Create data rows from specific extractions
You can now create new data rows directly from specific extractions.
### November 17, 2025
**\[Improved]** More accurate table extraction
Improved accuracy when extracting data from complex tables.
**\[API]** New `/user/is-enabled` endpoint
A new endpoint lets you check account status.
**\[Improved]** User info on file validation responses
File validation responses now include the user who performed the validation.
### November 10, 2025
**\[Improved]** Better handling of long documents
Better handling of documents with many pages via smart chunking.
**\[Improved]** Validation tracking
Track which values have been manually validated or edited.
**\[Improved]** More accurate extraction
Enhanced extraction accuracy with an updated default model.
### November 3, 2025
**\[Improved]** Better extraction on documents over the context limit
Improved extraction quality for documents exceeding context limits.
**\[Improved]** Highest-confidence merge
When merging extractions, the highest-confidence value is now preserved.
## October 2025
### October 27, 2025
**\[Improved]** Performance improvements across the platform
General performance improvements across the platform.
### October 13, 2025
**\[Fixed]** CSV export field ordering
Fixed field ordering in CSV exports to match workflow configuration.
**\[Improved]** Consistent results ordering
Extraction results now consistently respect field order.
### October 6, 2025
**\[API]** Endpoint to reorder fields within object-type fields
A new endpoint lets you reorder fields within object-type fields.
**\[Improved]** Verification URLs on job results
Job results now include verification URLs for easy access.
**\[Fixed]** Fields with similar names in nested structures
Fixed handling of fields with similar names in nested structures.
### September 30, 2025
**\[Improved]** Redesigned extraction pipeline
Redesigned extraction workflow for better reliability.