---
title: "Attach files to a task using the SDK"
description: "The Duale AI SDK attachment workflow prepares, uploads, and submits files to a task, resolving them into a conventional Library path."
lang: en
status: public-preview
lastUpdated: 2026-09-04
url: https://duale.ai/en/docs/sdk/attachments
---

## AI-generated summary

Attach files to tasks by preparing, uploading, and submitting them through the SDK, which creates a Library at the conventional task path.

- Supports 143 extensions across nine categories including documents, spreadsheets, images, videos, and code.
- Each file must be non-empty and no larger than 500 MiB.
- The task Library outlives the task and must be deleted explicitly to stop storage metering.
- Continuations inherit original attachment references but cannot add new files; start a new task instead.
- File bytes go to a separate storage host over HTTPS; allow it in egress policies.

Summaries were generated by AI. Generative AI is experimental.

---

Give a task a file to read. You prepare each attachment, upload it, then submit the task that references it. The SDK creates or resolves a normal Library at the conventional path `agent/{agent_id}/task/{task_id}` and returns stable Library and document ids.

## Before you attach files

Confirm these inputs before the upload:

- `DUALE_TENANT_ID` set. Attachment uploads go through the tenant's Library.
- `DUALE_TOKEN` set to an API token for the provisioned agent.
- A provisioned agent identifier. Set `DUALE_AGENT_ID` and pass it to `upload_attachments` as shown below.
- One or more non-empty files, each no larger than 500 MiB.
- Each file in a supported type.

The platform extracts text from files in nine categories. It supported 143 extensions on 2026-08-30:

| Category      | Extensions | Examples                       |
| ------------- | ---------- | ------------------------------ |
| Documents     | 25         | `.pdf`, `.docx`, `.md`, `.txt` |
| Spreadsheets  | 10         | `.xlsx`, `.csv`, `.ods`        |
| Presentations | 4          | `.pptx`, `.key`, `.odp`        |
| Images        | 20         | `.png`, `.jpg`, `.heic`        |
| Videos        | 11         | `.mp4`, `.mov`, `.mkv`         |
| Email         | 4          | `.eml`, `.mbox`, `.msg`        |
| Subtitles     | 9          | `.srt`, `.vtt`, `.ttml`        |
| Data          | 4          | `.xml`, `.json`, `.jsonl`      |
| Code          | 56         | `.py`, `.ts`, `.sql`, `.yaml`  |

From a video the platform reads the text visible in frames, not speech. Supply a recording as a
transcript or as one of the nine subtitle formats, such as `.srt` or `.vtt`.

Nothing checks the type before ingestion, so an unsupported file uploads normally and then fails
with `FORMAT_UNSUPPORTED`. This endpoint is the authority for the current extensions and media types
by category; read it to populate a file picker:

```text
GET {DUALE_ENDPOINT}/libraries/v1/tenants/{tenant_id}/document-uploads/supported-formats
```

Use this response, not the dated examples above, when your application validates a current file type.

## Prepare, upload, submit

`prepare_attachments` reads file sizes without loading file contents and raises `FileNotFoundError` for a missing path. Create one `task_id` and upload under it. Use the same id as `request_id` so logs and the conventional Library path correlate with the task:

```python runnable
import asyncio
from pathlib import Path
from uuid import uuid4

from duale import (
    LibraryDocumentGetRequest,
    LibraryResponseDocumentStatus,
    ask,
    create_sdk,
)

async def main() -> None:
    async with create_sdk() as sdk:
        attachments = sdk.prepare_attachments(
            [(Path("contract.pdf"), "Client contract for Q2")],
        )
        task_id = str(uuid4())

        receipts = await sdk.upload_attachments(
            task_id,
            attachments,
            agent_id=sdk.agent_id,
        )
        receipt = receipts[attachments[0].key]
        document = await sdk.libraries.wait_for_document(
            LibraryDocumentGetRequest(
                library_id=receipt.library_id,
                document_id=receipt.document_id,
            ),
        )
        if document.status is LibraryResponseDocumentStatus.failed:
            print(document.failure)
            return

        # Submit only after the document is readable.
        response = await ask(
            action="Summarize the attached contract.",
            attachments=attachments,
            request_id=task_id,
            sdk=sdk,
        )
        print(receipt.document_id)
        print(await response.model())

asyncio.run(main())
```

Each `PreparedAttachment` carries a `key` that connects the prepared item to its upload receipt and later attachment metadata; the description you pass orients the agent. `upload_attachments` returns each queued document receipt under that same key. Task submission sends the attachment metadata, so you do not pass a Library id to `ask()`.

To attach several files, pass more `(path, description)` tuples to `prepare_attachments`. Wait for every receipt before submitting a task that must read every file. Use [Manage libraries](https://duale.ai/en/docs/sdk/manage-libraries.md) when your application must choose the path or manage the Library independently of task submission.

## Images in a document

When an attachment contains images, such as charts, diagrams, or scanned figures, the
agent can read them as input. It does not rely only on their extracted text. Your
workspace must include an image-capable model target whose image-count, byte,
and dimension limits admit the complete image set. A document that is images
only, such as a scan with no extractable text, still works when one configured
target admits it.

Nothing in the attachment API changes. You upload and reference the document
the same way. When a Library tool result includes image references, routing
prefers models that can carry the complete image set.

If no configured model can carry the images, the platform does not refuse the
continuation. The request runs on the text the results carry. The model
receives an `[image unavailable]` marker in place of each missing image. The
task completes.

Losing one figure degrades a single answer; failing the request
would destroy the whole task. Verify the working path by asking for a value
that appears only inside a figure.

## Verify the attached document

The run prints the created document id, then the model's summary of the uploaded document.

## Limits and access to watch

Plan these lifecycle and access boundaries before you attach production data:

- **The shared task identifier is a convention.** It names the automatic Library path and correlates the upload with the task. Library discovery and authorization do not depend on `request_id` equality.
- **The call-scoped path is not an access rule.** The server resolves permissions from the token and a grant or policy that covers the resolved Library.
- **The task Library outlives the task.** The Library at `agent/{agent_id}/task/{task_id}` is an
  ordinary one: nothing expires it, and its documents are metered as storage until someone deletes
  them, as [Pricing and access](https://duale.ai/en/product/pricing.md) describes. Many tasks with attachments leave
  one Library for each. Pass `LibraryDeleteRequest` to `sdk.libraries.delete()` once a task's files
  are no longer needed; [Manage libraries](https://duale.ai/en/docs/sdk/manage-libraries.md) covers that call.
- **Continuation inherits the original attachment references.** A follow-up can
  use the same files without another upload. The continuation API cannot add a
  new attachment set; start a new task when a later request needs new files.
- **File bytes leave your process for a separate storage host.** The SDK sends them over HTTPS from your process
  straight to that host, so allow it in any egress policy. Each upload URL is signed and time-limited: treat it as a
  credential for its lifetime, and keep it out of application logs.
  [Data protection](https://duale.ai/en/docs/security/data-protection.md) gives that lifetime. Library management calls run over HTTPS
  with your API token. They do not use the task stream's extra encryption layer from RFC 9180.

Before production, test the storage-host path, confirm the Library grant, and assign deletion ownership for each task
Library.

## Related content

- [Application-facing SDK API reference](https://duale.ai/en/docs/sdk/reference.md)
- [Manage Libraries and documents with the SDK](https://duale.ai/en/docs/sdk/manage-libraries.md)
- [SDK exception hierarchy, problem details, and retry behavior](https://duale.ai/en/docs/sdk/errors.md)
- [SDK task lifecycle, routing, and streaming](https://duale.ai/en/docs/sdk/concepts.md)
- [Protect data across every boundary](https://duale.ai/en/docs/security/data-protection.md)
- [Declare and serve Python tools with the SDK](https://duale.ai/en/docs/sdk/tools.md)

---

## Sitemap

See the full [Markdown sitemap](https://duale.ai/sitemap.md) for all pages.
