Python SDK for the AnyFormat API
Project description
anyformat
Python SDK for the AnyFormat API (v3). Build a document workflow with a fluent builder, run a file through it, and read typed results — including drawing the parser's layout boxes back onto the page.
pip install --pre anyformat
Release candidate. The v3 SDK ships as
1.0.0rc1; pip skips pre-releases unless--pre(or an explicit pin) is given, so plainpip install anyformatkeeps resolving to the stable 0.7.x (v2) line until 1.0.0 final ships after the RC bake period.
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key="af_...") # or set ANYFORMAT_API_KEY
workflow = (
client.workflow("Invoices")
.parse()
.extract([Schema.string("vendor", "Vendor name on the invoice.")])
.create()
)
run = workflow.run("invoice.pdf") # uploads a document packet + starts a run
result = run.wait() # polls the run until it is processed
print(result.fields["vendor"].value) # -> "Acme Corp"
result.parse.draw("out/") # -> out/page_1.png with boxes drawn
The client reads ANYFORMAT_API_KEY from the environment if you omit api_key.
base_url defaults to https://api.anyformat.ai.
Concepts in 30 seconds
client.workflow(name)returns a fluent builder. Chain verbs (.parse(),.classify(),.split(),.extract(),.validate()) then.create()to register it and get aWorkflow.workflow.run(file)uploads the file(s) as one document packet and triggers aRun— one execution attempt; re-running the same packet creates a new run.run.wait()pollsGET /v3/runs/{run_id}/until the status is terminal and returns aResult.- Run statuses:
queued→in_progress→processed(success) |error|cancelled.wait()raisesExtractionFailedonerrorandExtractionCancelledoncancelled(both subclassRunFailed, so anexcept RunFailedstill catches either), and transparently retries a transient 429/5xx while polling. There is no 412 polling in v3. workflow.upload(file)stages a packet without running it (no credits spent); call.run()on the returnedUploadlater.- Lists are keyset-paginated: they return
Page(items, next_cursor); passnext_cursorback ascursor=until it isNone. No totals, newest first. result.fieldsis the extracted data;result.parsethe parser's output (markdown + per-block layout);result.rawthe full envelope.- All ids are hyphenated UUIDs.
Cookbook
Every recipe below is a complete, runnable script. They share this header:
from anyformat.sdk import Client
from anyformat.workflow import Schema, ClassifyCategory, SplitterRule, ValidationRule
client = Client(api_key="af_...")
Quick parse: one call → markdown
For a fast, no-setup parse, client.parse() runs the platform's atomic /parse
operation (a fast lite parse) and returns a handle. Poll get_markdown() for
the parsed markdown — no workflow to author or manage.
run = client.parse("contract.pdf")
while True:
try:
print(client.get_markdown(run.file_id)) # the parsed markdown
break
except StillParsing:
time.sleep(3)
1. Upload → run → results (the full v3 flow, step by step)
workflow.run(file) fuses these steps; here they are separately:
workflow = (
client.workflow("Invoice extraction")
.parse()
.extract([Schema.string("vendor", "Vendor name."), Schema.float("total", "Grand total.")])
.create()
)
upload = workflow.upload("invoice.pdf", idempotency_key="ingest-2026-07-04-0001")
print(upload.document_packet_id, [f.name for f in upload.files])
run = upload.run(idempotency_key="run-2026-07-04-0001")
result = run.wait(timeout=300, poll_interval=3)
print(result.fields["vendor"].value)
packet = client.get_document_packet(upload.document_packet_id)
print(packet.status, packet.latest_run_id) # latest_run_id == run.id
Retrying a request with the same Idempotency-Key replays the original
upload/run — no duplicate packet, no second (billed) extraction.
A packet can hold up to 10 files treated as one document:
run = workflow.run(files=["invoice.pdf", "annex.pdf"])
2. Poll a run yourself / list runs
detail = client.get_run(run.id)
print(detail.status) # queued | in_progress | processed | error | cancelled
if detail.result: # inline once status == "processed"
print(detail.result.fields)
cursor = None
while True:
page = client.list_runs(workflow.id, limit=50, cursor=cursor)
for r in page.items:
print(r.id, r.status, r.document_packet_id)
if page.next_cursor is None:
break
cursor = page.next_cursor
list_workflows() and list_document_packets(workflow_id) paginate the same
way.
To walk every row without cursor bookkeeping, use the auto-paging
iterators — they follow next_cursor for you and fetch each page lazily:
for run in client.iter_runs(workflow.id):
print(run.id, run.status)
all_workflows = list(client.iter_workflows())
for packet in client.iter_document_packets(workflow.id):
...
3. Edit a workflow without losing field identity (persistent_id)
GET returns the typed graph whose fields carry a server-assigned
persistent_id. Echo it back on update — including across renames — and
analytics, ground truth and quality metrics stay attached to the field.
Omit it for new fields (an omitted id falls back to name-matching).
definition = client.get_workflow(workflow.id) # typed WorkflowDefinition
for node in definition.nodes:
if node.type == "extract":
for field in node.extraction_schema.fields:
if field.name == "vendor":
field.name = "vendor_name" # rename; persistent_id kept
client.update_workflow(workflow.id, definition) # PATCH /v3/workflows/{id}/
When authoring from scratch, Schema factories accept the knob directly:
Schema.string("vendor_name", "Vendor name.", persistent_id="0686bb97-8c30-70f0-8000-97669e00aaaa")
4. Extract fields (linear parse → extract)
Schema is the field factory. Mix any field types in one extract.
workflow = (
client.workflow("Invoice extraction")
.parse()
.extract([
Schema.string("vendor", "Vendor name on the invoice."),
Schema.float("total", "Grand total amount."),
Schema.date("issued_on", "Invoice issue date."),
Schema.boolean("is_paid", "Whether the invoice is marked paid."),
Schema.integer("line_count", "Number of line items."),
Schema.enum("currency", "Currency of the totals.", options=[
Schema.option("USD", "US Dollar."),
Schema.option("EUR", "Euro."),
]),
Schema.object("line_items", "One row per line item.", fields=[
Schema.string("sku", "Stock keeping unit."),
Schema.string("description", "Line description."),
Schema.float("amount", "Line amount."),
]),
])
.create()
)
result = workflow.run("invoice.pdf").wait()
field = result.fields["vendor"]
print(field.value, field.confidence, [e.text for e in field.evidence])
Field types: string, integer, float, boolean, date, datetime,
enum, multi_select, object (nested).
5. Classify, then extract per category
invoice = ClassifyCategory(id="INVOICE", name="Invoice", description="A vendor invoice.")
receipt = ClassifyCategory(id="RECEIPT", name="Receipt", description="A point-of-sale receipt.")
workflow = (
client.workflow("Invoice or Receipt")
.parse()
.classify(invoice, receipt)
.extract([Schema.string("vendor", "Vendor name.")], branch=invoice) # object form
.extract([Schema.string("merchant", "Merchant name.")], branch="RECEIPT") # id form
.create()
)
result = workflow.run("doc.pdf").wait()
for category in result.raw["classifications"]:
print(category["category"], category["confidence"])
6. Split a multi-document file, then extract
statements = SplitterRule(id="STMT", name="Statement", description="A bank statement.")
checks = SplitterRule(id="CHECK", name="Check", description="A scanned check.")
workflow = (
client.workflow("Statement bundle")
.parse()
.split(statements, checks)
.extract([Schema.string("account", "Account number.")], branch="STMT")
.extract([Schema.float("amount", "Check amount.")], branch="CHECK")
.create()
)
result = workflow.run("bundle.pdf").wait()
for split in result.raw["splits"]:
print(split["name"], split["files"])
7. Validate extracted fields
workflow = (
client.workflow("Invoice with checks")
.parse()
.extract([
Schema.float("subtotal", "Subtotal before tax."),
Schema.float("tax", "Tax amount."),
Schema.float("total", "Grand total."),
])
.validate(
ValidationRule(id="totals", description="total must equal subtotal + tax.", severity="error"),
ValidationRule(id="positive", description="All amounts must be positive.", severity="warning"),
)
.create()
)
8. Different file inputs
run() and upload() accept a path string, a Path, raw bytes, or a list
via files=.
from pathlib import Path
workflow.run("invoice.pdf") # path string
workflow.run(Path("scans") / "page.png") # Path (PDF or image)
workflow.run(open("invoice.pdf", "rb").read()) # raw bytes
workflow.run(files=["a.pdf", "b.pdf"]) # one packet, many files
workflow.run(text="Plain text body") # text, synthesised as a .txt file
9. Async client
AsyncClient mirrors the sync API with await on create, run, and wait.
import asyncio
from anyformat.sdk import AsyncClient
from anyformat.workflow import Schema
async def main():
client = AsyncClient(api_key="af_...")
workflow = await (
client.workflow("Async invoices")
.parse()
.extract([Schema.string("vendor", "Vendor name.")])
.create()
)
run = await workflow.run("invoice.pdf")
result = await run.wait()
print(result.fields["vendor"].value)
await client.aclose()
asyncio.run(main())
Both clients are context managers, which close the underlying connection pool for you — prefer this over a bare module-level client that leaks connections:
with Client(api_key="af_...") as client:
result = client.workflow("Invoices").parse().extract([...]).create().run("invoice.pdf").wait()
async with AsyncClient(api_key="af_...") as client:
...
10. Enrich values with smart-lookup
Mark the field with source="smart_lookup" and pass the reference file to
extract(..., lookup_files=[...]). A smart-lookup field is overwritten by the lookup;
use source="lookup_if_missing" to extract it from the document too and let the lookup
fill only the slots extraction left empty:
workflow = (
client.workflow("Invoice + vendor lookup")
.parse()
.extract(
[
Schema.string("vendor_name", "Vendor as printed on the invoice."),
Schema.float("total", "Grand total amount."),
Schema.string("vendor_id", "Canonical vendor code from the catalog, joined on vendor_name.", source="smart_lookup"),
],
lookup_files=["vendor_catalog.csv"],
lookup_suggestion="Match the extracted vendor_name against the vendor_name column; return vendor_id.",
)
.create()
)
result = workflow.run("invoice.pdf").wait()
print(result.fields["vendor_id"].value) # -> "V-0001", resolved from the catalog
Errors
Every v3 error carries {error, detail, error_code, retryable, request_id};
the raised exception exposes status_code, error_code, detail,
retryable, and request_id.
| Status | Exception | error_code |
Retryable? |
|---|---|---|---|
| 400 | BadRequest |
VALIDATION_ERROR / TOPOLOGY_INVALID |
no |
| 401 | Unauthorized |
AUTH_FAILED |
no |
| 402 | PaymentRequired |
PAYMENT_REQUIRED (out of extraction credit) |
no |
| 403 | Forbidden |
ACCESS_DENIED |
no |
| 404 | NotFound |
NOT_FOUND |
no |
| 422 | APIError |
VALIDATION_ERROR |
no |
| 429 | RateLimited (.retry_after) |
RATE_LIMITED |
yes |
| 5xx | ServerError |
INTERNAL_ERROR / GATEWAY_TIMEOUT |
yes |
Non-HTTP: RunFailed (run ended in a terminal failure — .run_id, .status),
subclassed as ExtractionFailed (error) and ExtractionCancelled
(cancelled) so you can branch on the exact outcome while except RunFailed
still catches both; SDKTimeout (wait() budget exhausted); MissingResults
(processed run with no inline results — .run_id); StillParsing (parse op not
done). A transient 429/5xx during wait() polling is retried with backoff
(honouring Retry-After), not surfaced.
CLI
The package installs an afx command for one-off parses without writing code:
export ANYFORMAT_API_KEY=af_...
afx parse invoice.pdf
API quick reference
| Call | Returns | Notes |
|---|---|---|
Client(api_key, base_url=...) |
Client |
ANYFORMAT_API_KEY if api_key omitted |
client.workflow(name) |
builder | chain verbs, then .create() |
.parse(mode="standard"|"agentic"|"lite") |
builder | exactly one per workflow |
.classify(*categories) |
builder | branching; needs parse() first |
.split(*rules, route_from=...) |
builder | route_from required after classify() |
.extract(fields, branch=..., lookup_files=..., lookup_suggestion=...) |
builder | branch required after classify/split |
.validate(*rules, branch=...) |
builder | follows extract() |
.create() / .update(workflow_id) |
Workflow |
create POSTs, update PATCHes /v3/workflows/{id}/ |
client.get_workflow(id) |
WorkflowDefinition |
typed graph; fields carry persistent_id |
client.update_workflow(id, definition) |
Workflow |
PATCH — echo persistent_id to keep field identity |
client.list_workflows(limit, cursor) |
Page[Workflow] |
keyset; follow next_cursor |
client.iter_workflows(limit) |
Iterator[Workflow] |
auto-pages every workflow, lazily |
client.delete_workflow(id) |
str |
deletes packets + runs too |
workflow.run(file, files=, text=, idempotency_key=) |
Run |
upload + run in one call |
workflow.upload(...) |
Upload |
stage without running |
upload.run(idempotency_key=) |
Run |
trigger extraction |
run.wait(timeout=300, poll_interval=3) |
Result |
polls run status; retries transient 429/5xx; ExtractionFailed/ExtractionCancelled on terminal failure |
client.get_run(run_id) |
RunDetail |
.status, .result inline when processed |
client.list_runs(workflow_id, limit, cursor) |
Page[RunSummary] |
newest first |
client.iter_runs(workflow_id, limit) |
Iterator[RunSummary] |
auto-pages every run, lazily |
client.get_document_packet(id) |
DocumentPacketDetail |
.files, .latest_run_id |
client.delete_document_packet(id) |
str |
irreversible |
client.run_document_packet(id, idempotency_key=) |
Run |
re-run on latest workflow version |
client.list_document_packets(workflow_id, limit, cursor) |
Page[DocumentPacketSummary] |
slim rows |
client.iter_document_packets(workflow_id, limit) |
Iterator[DocumentPacketSummary] |
auto-pages every packet, lazily |
client.parse(file) / client.get_markdown(job_id) |
Run / str |
atomic parse op (v2 surface) |
result.fields |
dict[str, ExtractedField] |
.value, .confidence, .evidence |
result.parse |
ParseView | None |
.blocks, .markdown, .text, .draw() |
result.raw |
dict |
full results payload |
Node configuration knobs
Every knob is a first-class .parse() / .extract() keyword argument.
.parse(...) — mode ("standard"/"agentic"/"lite"), prompt_hint, figure_enhancement,
cache; agentic-only effort ("low"/"mid"/"accurate"); lite-only ocr_effort
("medium"/"high"), skip_routing_review, figures, text_formatting, routing_effort.
Mode-discriminated via per-mode @overloads — a wrong-mode knob is a type error.
.extract(...) — mode ("standard"/"agentic"/"lite"), use_images,
lookup_suggestion, lookup_reasoning_effort ("minimal"/"low"/"medium"/"high"),
lookup_file_uploads (inline base64).
Schema.<type>(...) — source="smart_lookup" (lookup overwrites) or source="lookup_if_missing" (extract too; lookup only fills empty slots), persistent_id= (pin field identity
across updates).
Releasing
Cut on demand — a maintainer bumps the version in a PR, then pushes a sdk-python-vX.Y.Z tag on the release commit. Full flow + one-time setup: ../RELEASING.md. Break-glass: ./publish.sh [--build-only|--dry-run].
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file anyformat-1.0.0rc1.tar.gz.
File metadata
- Download URL: anyformat-1.0.0rc1.tar.gz
- Upload date:
- Size: 50.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6053a9f1a07c677834a511a9e76052f182bc1d918c09c9d190ca98a3cb08530
|
|
| MD5 |
abe27f3dc4ef6a7e87d6313e42a8d025
|
|
| BLAKE2b-256 |
e011d4a6a50f5556b9ae0a40a5d4195e0a366aaf1ed6fae4bc664de1c7317d0a
|
Provenance
The following attestation bundles were made for anyformat-1.0.0rc1.tar.gz:
Publisher:
publish-releases.yml on anyformat-ai/anyformat-monorepo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
anyformat-1.0.0rc1.tar.gz -
Subject digest:
b6053a9f1a07c677834a511a9e76052f182bc1d918c09c9d190ca98a3cb08530 - Sigstore transparency entry: 2206260688
- Sigstore integration time:
-
Permalink:
anyformat-ai/anyformat-monorepo@a1f021fc1cb4d4d0b4232bc5b7122cf94ee43f4b -
Branch / Tag:
refs/tags/sdk-python-v1.0.0rc1 - Owner: https://github.com/anyformat-ai
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
publish-releases.yml@a1f021fc1cb4d4d0b4232bc5b7122cf94ee43f4b -
Trigger Event:
push
-
Statement type:
File details
Details for the file anyformat-1.0.0rc1-py3-none-any.whl.
File metadata
- Download URL: anyformat-1.0.0rc1-py3-none-any.whl
- Upload date:
- Size: 61.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9646f7c1c840ee2384529eacc027511c4b6219c8f02a56b175f08a387d54f498
|
|
| MD5 |
18c0bba37eadc01858181467d132cc12
|
|
| BLAKE2b-256 |
4945d512a1fc0ee57fb1be2ecc611a6de3c860b6006b9785ae3a12267cef1592
|
Provenance
The following attestation bundles were made for anyformat-1.0.0rc1-py3-none-any.whl:
Publisher:
publish-releases.yml on anyformat-ai/anyformat-monorepo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
anyformat-1.0.0rc1-py3-none-any.whl -
Subject digest:
9646f7c1c840ee2384529eacc027511c4b6219c8f02a56b175f08a387d54f498 - Sigstore transparency entry: 2206260703
- Sigstore integration time:
-
Permalink:
anyformat-ai/anyformat-monorepo@a1f021fc1cb4d4d0b4232bc5b7122cf94ee43f4b -
Branch / Tag:
refs/tags/sdk-python-v1.0.0rc1 - Owner: https://github.com/anyformat-ai
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
publish-releases.yml@a1f021fc1cb4d4d0b4232bc5b7122cf94ee43f4b -
Trigger Event:
push
-
Statement type: