> ## Documentation Index
> Fetch the complete documentation index at: https://docs.anyformat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> The @anyformat/sdk package: a fluent builder over the typed workflow graph, run and result handles, and typed errors.

`@anyformat/sdk` is the TypeScript client for [API v3](/api-reference-v3/introduction). The wire types and the low-level HTTP client are generated from the API's OpenAPI document; the layer you use (`Anyformat`, `Schema`, `WorkflowBuilder`, `Workflow`, `Run`, `Result`) is hand-written and mirrors the [Python SDK](/api-reference-v3/sdks/python) method for method.

## Install and auth

Requires Node.js 18 or later. The package ships ESM and CJS bundles with types. The 1.0 line is a release candidate on the npm `rc` tag; plain `npm install @anyformat/sdk` still resolves to the 0.x (v2) line until 1.0.0 final ships.

```bash theme={null}
npm install @anyformat/sdk@rc
```

Pass the API key to the constructor. The client does not read the environment for you.

```typescript theme={null}
import { Anyformat } from "@anyformat/sdk";

const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
```

See [Authentication](/api-reference-v3/introduction#authentication) for how to mint a key.

## Client

`new Anyformat(options)` is the entry point. Every method returns a `Promise`.

| Option    | Default                    | What it does                              |
| --------- | -------------------------- | ----------------------------------------- |
| `apiKey`  | required                   | Your `af_...` key.                        |
| `baseUrl` | `https://api.anyformat.ai` | Override for a non-production deployment. |

```typescript theme={null}
import { Anyformat, Schema } from "@anyformat/sdk";

const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });

const workflow = await af
  .workflow("Invoices")
  .parse()
  .extract([Schema.string("vendor", "Vendor name on the invoice.")])
  .create();

const run = await workflow.run(new File([bytes], "invoice.pdf"));
const result = await run.wait();
console.log(result.field("vendor")?.value);
```

## Build a workflow

`af.workflow(name, description?)` returns a `WorkflowBuilder`. Chain one method per node, then `await .create()` to register the workflow and get a `Workflow` handle, or `await .update(workflowId)` to replace an existing workflow's graph. `.build()` returns the request body without a network call.

The builder assigns node ids (`parse_1`, `extract_1`, ...) and wires the edges. Pass `{ id }` in the options to choose the id yourself. Each method builds the same typed node the API accepts, so the [node schema](/api-reference-v3/node-schemas) is the reference for every option. A builder mistake, such as `extract()` before `parse()`, throws `WorkflowBuilderError` before any request is sent.

The builder has methods for Parse, Classify, Split, Extract and Validate. For a graph with an [Edit](/guides/nodes/edit), [If/Else](/guides/nodes/if-else), [Slack alert](/guides/nodes/slack-alert) or [Knowledge](/guides/nodes/knowledge) node, write the `nodes` and `edges` arrays yourself and send them with `af.updateWorkflow(workflowId, definition)`, or call the [create endpoint](/api-reference-v3/workflows/create) directly. The wire types (`Edge`, `ClassifyCategory`, `SplitterRule`, `ValidationRule`, the field and check types) are exported for that.

### parse

Adds the one [Parse](/guides/nodes/parse) node. Call it first and once. The options are a `mode`-discriminated union: each tier only accepts its own knobs, so a wrong-tier knob is a compile-time error.

```typescript theme={null}
af.workflow("Read only").parse();                                        // mode "standard"
af.workflow("Dense tables").parse({ mode: "agentic", effort: "accurate" });
af.workflow("Born-digital").parse({ mode: "flash", scannedPages: "skip" });
```

| Option              | Applies to                  | Default                                                              |
| ------------------- | --------------------------- | -------------------------------------------------------------------- |
| `mode`              | all                         | `"standard"`. One of `"standard"`, `"agentic"`, `"lite"`, `"flash"`. |
| `promptHint`        | standard, agentic, flash    | unset                                                                |
| `figureEnhancement` | all (only standard uses it) | `false`                                                              |
| `cache`             | all                         | `true`                                                               |
| `effort`            | agentic                     | `"mid"` on the server. One of `"low"`, `"mid"`, `"accurate"`.        |
| `scannedPages`      | flash                       | `"ocr"` on the server. One of `"ocr"`, `"skip"`, `"fail"`.           |

`lite` takes no knob of its own.

### classify

Adds a [Classify](/guides/nodes/classify) node after Parse. Takes an array of `ClassifyCategory` objects (`id`, `name`, `description`).

```typescript theme={null}
const invoice = { id: "INVOICE", name: "Invoice", description: "A vendor invoice." };
const receipt = { id: "RECEIPT", name: "Receipt", description: "A point-of-sale receipt." };

const builder = af.workflow("Invoice or receipt").parse().classify([invoice, receipt]);
```

Every `extract()` that follows needs `{ branch }`.

### split

Adds a [Split](/guides/nodes/split) node. Takes an array of `SplitterRule` objects (`id`, `name`, `description`, optional `partition_key`). After `classify()`, pass `{ routeFrom }` to name the category that feeds the splitter.

```typescript theme={null}
const statement = { id: "STATEMENT", name: "Statement", description: "A bank statement.", partition_key: "account_number" };
const check = { id: "CHECK", name: "Check", description: "A scanned check." };

const builder = af.workflow("Statement batch").parse().split([statement, check]);
```

### extract

Adds an [Extract](/guides/nodes/extract) node. Repeatable: one per branch. `fields` is an array built with `Schema`.

```typescript theme={null}
const builder = af
  .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
```

| Option                  | Default                    | What it does                                                                                                                                                       |
| ----------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `branch`                | unset                      | The category or rule this extract hangs off. Required after `classify()` or `split()`, forbidden otherwise. A `ClassifyCategory`, a `SplitterRule`, or its id.     |
| `mode`                  | `"standard"` on the server | The extraction tier. One of `"standard"`, `"agentic"`, `"lite"`.                                                                                                   |
| `useImages`             | `false` on the server      | Send each PDF page's rendered image alongside its parsed text. Standard and lite only.                                                                             |
| `lookupFiles`           | unset                      | Paths of [Smart Lookup](/guides/nodes/smart-lookup) reference files. The SDK reads each one from disk (Node.js only) and sends it inline as `lookup_file_uploads`. |
| `lookupSuggestion`      | unset                      | Free-form hint for the lookup matcher.                                                                                                                             |
| `lookupReasoningEffort` | unset                      | `"minimal"`, `"low"`, `"medium"` or `"high"`. Used only when the node has a lookup field.                                                                          |
| `id`                    | auto                       | Node id.                                                                                                                                                           |

`Schema` has one factory per [field type](/concepts/field-types): `string`, `integer`, `float`, `boolean`, `date`, `datetime`, `enum(name, description, options)`, `multiSelect(name, description, options)`, and `object(name, description, fields)`. `Schema.option(name, description)` builds an enum option. Every factory takes a trailing `{ source, persistentId }`: `source: "smart_lookup"` or `"lookup_if_missing"` for a lookup field, `persistentId` to pin a field's identity across updates.

### validate

Adds a [Validate](/guides/nodes/validate) node after the most recent `extract()`, or after the extract on `{ branch }`. Takes an array of `ValidationRule` objects.

```typescript theme={null}
const builder = af
  .workflow("Invoice with checks")
  .parse()
  .extract([
    Schema.float("subtotal", "Subtotal before tax."),
    Schema.float("tax", "Tax amount."),
    Schema.float("total", "Grand total."),
  ])
  .validate([
    { id: "vendor-legit", description: "The vendor is a real, named company." },
    {
      id: "totals-add-up",
      kind: "deterministic",
      check: { type: "arithmetic", operands: ["subtotal", "tax"], equals: "total", tolerance: 0.01 },
    },
  ]);
```

## Run and read

### Upload and run

`workflow.run(files, opts?)` uploads one `File` or an array of up to 10 as one [document packet](/concepts/document-packets) and starts a run. Every file needs a name so the server can detect its type: pass a `File`, not a bare `Blob`.

```typescript theme={null}
const run = await workflow.run(new File([bytes], "invoice.pdf"));
const run = await workflow.run([fileA, fileB]);                       // one packet, one document
const run = await workflow.run(file, { idempotencyKey: "ingest-2026-08-25-0001" });
```

Filenames are unique within a workflow. A colliding name is rejected with `409`; pass `{ onConflict: "rename" }` to auto-rename instead.

To upload without spending credits, use `workflow.upload(files, opts?)`. It returns an `Upload` with `documentPacketId` and `files`; call `upload.run()` later. `workflow.uploadFromUrls(urls, opts?)` imports up to 10 HTTPS URLs server-side into one packet.

```typescript theme={null}
const upload = await workflow.upload([fileA, fileB]);
const run = await upload.run({ idempotencyKey: "run-2026-08-25-0001" });

const imported = await workflow.uploadFromUrls(["https://example.com/invoices/april.pdf"]);
```

Retrying with the same `idempotencyKey` replays the original upload or run instead of creating and billing a second one.

### wait

`run.wait({ timeoutMs, pollMs })` polls `GET /v3/runs/{run_id}/` until the status is terminal and resolves with a `Result`. Defaults: 300 000 ms budget, 3 000 ms between polls. A transient `429` or `5xx` while polling is retried with backoff and honours `Retry-After`. `wait()` rejects with `ExtractionFailedError` on `error`, `ExtractionCancelledError` on `cancelled`, and `SDKTimeoutError` when the budget runs out.

```typescript theme={null}
const result = await run.wait({ timeoutMs: 600_000 });
```

### Result

| Member                    | Type                                           | What it holds                                                                                                                                                                                                                                                                             |
| ------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `result.field(name)`      | `ExtractedField \| ExtractedRows \| undefined` | One top-level field of the untagged extraction. A scalar field is an `ExtractedField` with `.value`, `.confidence` (0 to 100) and `.evidence`; an object field is an array of row records. `undefined` only when the field is absent. Type a scalar with `result.field<number>("total")`. |
| `result.extractions`      | `Extraction[]`                                 | Every extraction, including per-partition ones from a Split node (`split_name`, `partition`, `fields`).                                                                                                                                                                                   |
| `result.parse`            | `ParseView \| null`                            | The Parse node's output: `.markdown`, `.text`, `.blocks`, `.parseConfidence`, `.layoutConfidence`.                                                                                                                                                                                        |
| `result.documentPacketId` | `string`                                       | The packet this result belongs to.                                                                                                                                                                                                                                                        |
| `result.raw`              | `ResultsResponseV3`                            | The full results envelope, including `classifications`, `splits` and `edits`.                                                                                                                                                                                                             |

```typescript theme={null}
const result = await run.wait();

const vendor = result.field<string>("vendor");
console.log(vendor?.value, vendor?.confidence);

for (const row of result.field("line_items") as ExtractedRows) {
  console.log(row.sku.value, row.amount.value);
}

for (const extraction of result.extractions) {
  if (extraction.partition) console.log(extraction.split_name, extraction.partition);
}

for (const c of result.raw.classifications ?? []) console.log(c.category, c.confidence);
```

`ParseView` has no `draw()`; rendering pages needs PDF rasterising, which only the Python SDK ships.

### Quick parse

For a one-off parse with no workflow, `af.parse(file)` runs the atomic parse operation and returns a job id. `af.getParseMarkdown(jobId)` returns the markdown once done, or `null` while it is still processing. This is the one call still served by the v2 operations surface.

```typescript theme={null}
const jobId = await af.parse(file);
let markdown = await af.getParseMarkdown(jobId);
while (markdown === null) {
  await new Promise((r) => setTimeout(r, 3000));
  markdown = await af.getParseMarkdown(jobId);
}
```

## Document packets and runs

Packets and runs stay addressable after `wait()` resolves.

| Method                                                    | Resolves to                           | Notes                                                            |
| --------------------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------- |
| `af.getDocumentPacket(packetId)`                          | `DocumentPacketDetailV3`              | `.status`, `.files`, `.latest_run_id`.                           |
| `af.runDocumentPacket(packetId, { idempotencyKey? })`     | `Run`                                 | Re-run on the workflow's latest version. Each call is a new run. |
| `af.deleteDocumentPacket(packetId)`                       | `string`                              | Irreversible.                                                    |
| `af.listDocumentPackets(workflowId, { limit?, cursor? })` | `Page<DocumentPacketSummaryV3>`       | Newest first.                                                    |
| `af.getRun(runId)`                                        | `RunDetailV3`                         | `.status`, and `.results` inline once `processed`.               |
| `af.listRuns(workflowId, { limit?, cursor? })`            | `Page<RunSummaryV3>`                  | Newest first.                                                    |
| `af.iterRuns(workflowId, { limit? })`                     | `AsyncIterableIterator<RunSummaryV3>` | Follows the cursor for you.                                      |

Packet statuses: `not_started`, `queued`, `in_progress`, then `processed`, `error` or `cancelled`. Run statuses are the same set without `not_started`.

```typescript theme={null}
const packet = await af.getDocumentPacket(run.documentPacketId);
console.log(packet.status, packet.latest_run_id);

for await (const summary of af.iterRuns(workflow.id)) {
  console.log(summary.id, summary.status);
}
```

### Manage workflows

| Method                                      | Resolves to                       | Notes                                                                                                                                       |
| ------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `af.getWorkflow(workflowId)`                | `WorkflowDetailV3`                | The typed graph inline. Fields carry their `persistent_id`.                                                                                 |
| `af.updateWorkflow(workflowId, definition)` | `WorkflowDetailV3`                | Replaces the graph. `definition` is `{ name, description, nodes, edges }`. Echo each `persistent_id` to keep field identity across renames. |
| `af.listWorkflows({ limit?, cursor? })`     | `Page<Workflow>`                  | Each item is a `Workflow` you can `.run()`.                                                                                                 |
| `af.iterWorkflows({ limit? })`              | `AsyncIterableIterator<Workflow>` | Follows the cursor.                                                                                                                         |
| `af.deleteWorkflow(workflowId)`             | `string`                          | Deletes its packets and runs too.                                                                                                           |

```typescript theme={null}
const detail = await af.getWorkflow(workflow.id);
for (const node of detail.nodes) {
  if (node.type === "extract") {
    for (const field of node.extraction_schema.fields) {
      if (field.name === "vendor") field.name = "vendor_name";   // persistent_id travels with it
    }
  }
}
await af.updateWorkflow(workflow.id, detail);
```

## Knowledge ask

The TypeScript client has no `ask()` method yet. Call [Ask](/api-reference-v3/knowledge/ask) with `fetch`:

```typescript theme={null}
const res = await fetch(`https://api.anyformat.ai/v3/workflows/${workflow.id}/knowledge/ask`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ANYFORMAT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ question: "Which contracts renew before March?" }),
});
const answer = await res.json();
```

## Evals and datasets

A [dataset](/concepts/evals) holds documents with known answers. Upload one document, with optional ground truth, as one packet:

```typescript theme={null}
const entry = await workflow.uploadToDataset(new File([bytes], "invoice.pdf"), {
  groundTruth: { vendor: "ACME", total: "42.00", line_items: [{ sku: "A1", qty: "2" }] },
});
console.log(entry.documentPacketId, entry.groundTruthSaved);
```

`groundTruth` keys are the schema's field identifiers. A scalar maps to `string | null`; an object field maps to an array of row objects. The call is atomic: a rejected file or a ground-truth failure stores nothing. Bulk upload loops one atomic call per document:

```typescript theme={null}
await workflow.uploadDocumentsToDataset([
  { files: new File([a], "a.pdf"), groundTruth: { vendor: "ACME" } },
  { files: [new File([b1], "b1.pdf"), new File([b2], "b2.pdf")] },
]);
```

Both are also on the client: `af.uploadToDataset(workflowId, files, opts)` and `af.uploadDocumentsToDataset(workflowId, documents)`. The client has no eval methods yet; launch and read evals through the [endpoints](/api-reference-v3/evals/launch).

## Pagination

Every list resolves to a `Page<T>` with `items` and `nextCursor`. Pass `nextCursor` back as `cursor` until it is `null`. There are no totals or page numbers; `limit` is capped at 100.

```typescript theme={null}
let cursor: string | undefined;
do {
  const page = await af.listRuns(workflow.id, { limit: 100, cursor });
  for (const run of page.items) console.log(run.id, run.status);
  cursor = page.nextCursor ?? undefined;
} while (cursor);
```

The `iter*` methods (`iterWorkflows`, `iterDocumentPackets`, `iterRuns`) do this loop for you and fetch each page lazily.

## Errors and retries

Every HTTP failure rejects with a subclass of `APIError` carrying the v3 [error envelope](/api-reference-v3/introduction#errors): `.status`, `.errorCode`, `.retryable`, `.requestId` and the raw `.body`.

| Status | Class                  | Retry?                                        |
| ------ | ---------------------- | --------------------------------------------- |
| 400    | `BadRequestError`      | no. `VALIDATION_ERROR` or `TOPOLOGY_INVALID`. |
| 401    | `UnauthorizedError`    | no                                            |
| 402    | `PaymentRequiredError` | no. Top up credit first.                      |
| 403    | `ForbiddenError`       | no                                            |
| 404    | `NotFoundError`        | no                                            |
| 422    | `APIError`             | no                                            |
| 429    | `RateLimitedError`     | yes, after `Retry-After`                      |
| 5xx    | `ServerError`          | yes                                           |

Outside HTTP, all subclasses of `AnyformatError`: `RunFailedError` when `wait()` sees a terminal failure, subclassed as `ExtractionFailedError` (`error`) and `ExtractionCancelledError` (`cancelled`), each with `.runId` and `.status`; `SDKTimeoutError` when `wait()` runs out of budget; `WorkflowBuilderError` for builder misuse.

```typescript theme={null}
import { APIError, RateLimitedError, RunFailedError, SDKTimeoutError } from "@anyformat/sdk";

try {
  const result = await (await workflow.run(file)).wait({ timeoutMs: 120_000 });
} catch (err) {
  if (err instanceof RateLimitedError) {
    // back off, then retry
  } else if (err instanceof RunFailedError) {
    console.error(`run ${err.runId} ended ${err.status}`);
  } else if (err instanceof SDKTimeoutError) {
    // still running; poll af.getRun() later
  } else if (err instanceof APIError) {
    console.error(err.status, err.errorCode, err.requestId, err.body);
  } else {
    throw err;
  }
}
```

`wait()` retries a transient `429` or `5xx` itself, so those never reach your `catch` while polling.

## Version pins

| Package                | Version      | API                                                                                         |
| ---------------------- | ------------ | ------------------------------------------------------------------------------------------- |
| `@anyformat/sdk@rc`    | `1.0.0-rc.1` | v3. Release candidate on the `rc` dist-tag.                                                 |
| `@anyformat/sdk` 0.2.x | stable       | v2. Keeps working through the [v2 deprecation window](/api-reference-v3/migrating-from-v2). |

Upgrading from 0.x changes uploads, runs, pagination and workflow updates. Follow the [0.x to 1.0 migration guide](/api-reference-v3/sdk-migration).

## Links

* [npm](https://www.npmjs.com/package/@anyformat/sdk)
* [Python SDK](/api-reference-v3/sdks/python): the same shape in Python
* [Node schemas](/api-reference-v3/node-schemas): every option the builder maps to
