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

# Files

When a workflow input is a file, the SDK uploads it as `multipart/form-data` (the same shape as `curl -F`). No base64, no payload doubling.

## Browser

```ts theme={null}
import { EigenpalClient } from '@eigenpal/sdk';

const fileInput = document.querySelector<HTMLInputElement>('input[type=file]')!;
const client = new EigenpalClient({ apiKey });

await client.run('workflows.extract-invoice', {
  contract_document: fileInput.files![0], // File from <input type="file">
});
```

## Node, from disk

```ts theme={null}
import { readFile } from 'node:fs/promises';

const buffer = await readFile('contract.pdf');
await client.run('workflows.extract-invoice', {
  contract_document: {
    content: buffer,
    filename: 'contract.pdf',
    mimeType: 'application/pdf',
  },
});
```

## Node, from a Blob

```ts theme={null}
const blob = new Blob([buffer], { type: 'application/pdf' });
await client.run('workflows.extract-invoice', { contract_document: blob });
```

## Multiple files

```ts theme={null}
await client.run('workflows.compare-versions', {
  original: file1,
  revised: file2,
  reference: file3,
});
```

Each file becomes a top-level form field. Mix files and scalar inputs freely; scalars ride along in a single `_json` text field automatically.

## Nested files aren't extracted

Only top-level file values become multipart fields. Files inside arrays or nested objects stay in the JSON sidecar and the server won't see them as uploads:

```ts theme={null}
// DON'T — `documents` becomes a JSON array of `{}` objects, no upload.
await client.run('workflows.compare', { documents: [file1, file2] });

// DO — flatten to top-level keys, your workflow accepts them by name.
await client.run('workflows.compare', { document_0: file1, document_1: file2 });
```

## Don't base64 yourself

```ts theme={null}
// Don't do this. Doubles the payload size and skips the optimised path.
await client.run('workflows.extract-invoice', {
  contract_document: btoa(buffer.toString('binary')),
});
```

The SDK picks multipart whenever it sees a `File`, `Blob`, or `{ content, filename, mimeType }`. Plain strings pass through as scalar inputs.
