Skip to main content
The anyformat package is the Python client for API v3. It is hand-written on httpx. You build a workflow with a fluent builder, run a file through it, and read typed results. The TypeScript SDK mirrors it method for method. Two import paths: anyformat.sdk holds the client, the handles and the errors; anyformat.workflow holds the schema factories and the node types.

Install and auth

Requires Python 3.10 or later. The 1.0 line is a release candidate, so pip needs --pre to pick it up; plain pip install anyformat still resolves to the 0.x (v2) line until 1.0.0 final ships.
Pass the API key to the client. The client does not read the environment for you.
See Authentication for how to mint a key. The package also installs the afx command, which does read ANYFORMAT_API_KEY.

Client

Client is synchronous. AsyncClient has the same methods with await. Both take the same arguments. Both clients are context managers that close the connection pool. Prefer that over a bare module-level client.
In the async client, create(), update(), run(), upload(), wait() and every client method are awaitable. The builder verbs (parse, extract, and so on) are not.

Build a workflow

client.workflow(name, description=None) returns a builder. Chain one verb per node, then .create() to register the workflow and get a Workflow handle, or .update(workflow_id) to replace an existing workflow’s graph. .build() returns the typed WorkflowDefinition without a network call. The builder assigns node ids (parse_1, extract_1, …) and wires the edges for you. Pass id= to any verb to choose the id yourself. Each verb constructs the same typed node the API accepts, so the node schema is the reference for every argument. A builder mistake, such as extract() before parse(), raises WorkflowBuilderError before any request is sent. The builder has verbs for Parse, Classify, Split, Extract, Validate and Edit. For a graph with an If/Else, Slack alert or Knowledge node, build a WorkflowDefinition from the node classes in anyformat.workflow.nodes and pass it to client.create_workflow(definition).

parse

Adds the one Parse node. Call it first and once. mode selects the tier, and the type checker only offers the knobs that belong to that tier.
lite takes no knob of its own. All arguments are keyword-only.

classify

Adds a Classify node after Parse. Takes one or more ClassifyCategory objects.
Every extract() that follows needs branch=.

split

Adds a Split node. Takes one or more SplitterRule objects. After classify(), pass route_from= to name the category that feeds the splitter.

extract

Adds an Extract node. Repeatable: one per branch. fields is a list built with Schema.
Schema has one factory per field type: string, integer, float, boolean, date, datetime, enum(name, description, options=[Schema.option(...)]), multi_select, and object(name, description, fields=[...]). Every factory accepts source="smart_lookup" or source="lookup_if_missing", and persistent_id= to pin a field’s identity across updates.
client.workflow(...).extract() does not take mode, use_images or lookup_reasoning_effort yet. Set them through the API JSON, in Studio, or with client.get_workflow() + client.update_workflow() on the typed definition.

validate

Adds a Validate node after the most recent extract(), or after the extract on branch=. Takes one or more ValidationRule objects.

edit

Adds an Edit node after Parse. Call it once.
upload_edit_references(workflow_id, files) uploads the standing data an Edit node fills from (.csv, .txt, .md, .rst, .pdf). A PDF comes back pending while it is parsed once; poll list_edit_references(workflow_id) until ready. delete_edit_reference(workflow_id, document_id) removes one. The builder does not set font or output_mode; set those in the API JSON.

Run and read

Upload and run

workflow.run(...) uploads the document as one document packet and starts a run. It accepts a path string, a Path, raw bytes, a list of up to 10 files via files=, or text=. Exactly one of the three.
Filenames are unique within a workflow. A colliding name is rejected with 409; pass on_conflict="rename" to auto-rename instead. To upload without spending credits, use workflow.upload(...). It returns an Upload with document_packet_id and files; call upload.run() later.
Retrying with the same idempotency_key replays the original upload or run instead of creating and billing a second one.

wait

run.wait(timeout=300, poll_interval=3) polls GET /v3/runs/{run_id}/ until the status is terminal and returns a Result. A transient 429 or 5xx while polling is retried with backoff and honours Retry-After. wait() raises ExtractionFailed on error, ExtractionCancelled on cancelled, and SDKTimeout when the budget runs out.

Result

result.parse.draw("out/") renders every block’s bounding box onto its page, coloured by confidence, and writes one PNG per page. It uses the file you ran as the background; pass source= to use another.

Quick parse

For a one-off parse with no workflow, client.parse(file) runs the atomic parse operation and returns a handle. Poll client.get_markdown(run.file_id) until it stops raising StillParsing. This is the one call still served by the v2 operations surface.

Document packets and runs

Packets and runs stay addressable after wait() returns. Run statuses: queued, in_progress, then processed, error or cancelled.

Manage workflows

Knowledge ask

A workflow with a Knowledge node answers questions about what its documents say. workflow.ask(question) and client.ask(workflow_id, question) call Ask and return an Answer.
Answer has .answer, .citations (each with path, quote, file_id, block_id, page, bbox), .steps_used and .thread_id. Pass thread_id="kb-..." to ask a follow-up in the context of earlier questions on that thread. A workflow without a Knowledge node raises APIError with error_code KNOWLEDGE_NOT_ENABLED.

Evals and datasets

A dataset holds documents with known answers. Upload one document, with optional ground truth, as one packet:
ground_truth keys are the schema’s field identifiers. A scalar maps to str | None; an object field maps to a list of row dicts. The call is atomic: a rejected file or a ground-truth failure stores nothing. Bulk upload loops one atomic call per document:
Then launch an eval over the whole dataset and poll it:
launch_eval(workflow_id, version_id=None, idempotency_key=None) evaluates the current version unless you pass one. list_evals(workflow_id, limit, cursor) and iter_evals(workflow_id) list past evals. See Launch eval.

Pagination

Every list returns a Page(items, next_cursor). Pass next_cursor back as cursor= until it is None. There are no totals or page numbers; limit is capped at 100.
The iter_* methods (iter_workflows, iter_document_packets, iter_runs, iter_evals) do this loop for you and fetch each page lazily.

Errors and retries

Every HTTP failure raises a subclass of APIError carrying the v3 error envelope: .status_code, .error_code, .detail, .retryable and .request_id. Outside HTTP: RunFailed when wait() sees a terminal failure, subclassed as ExtractionFailed (error) and ExtractionCancelled (cancelled), each with .run_id and .status; SDKTimeout when wait() runs out of budget; MissingResults when a processed run carries no results; StillParsing while a quick parse is not done; WorkflowBuilderError for builder misuse.
wait() retries a transient 429 or 5xx itself, so those never reach your except while polling.

Version pins

Upgrading from 0.x changes uploads, runs, pagination and workflow updates. Follow the 0.x to 1.0 migration guide.