---
title: "Manage Libraries and documents with the SDK"
description: "The Duale AI SDK manages a persistent Library and its documents, covering upload, ingestion polling, state inspection, and deletion."
lang: en
status: public-preview
lastUpdated: 2026-09-03
url: https://dev.duale.ai/en/docs/sdk/manage-libraries
---

## AI-generated summary

Manage a persistent Library with the SDK: create it, upload documents, poll ingestion state, share access, and delete resources.

- create() returns an active Library or creates a new one with agent access.
- wait_for_document() polls every five seconds with a 360-second default timeout.
- Documents move through queued, processing, ready, or failed states; failed is terminal.
- A deleted document stays in Recently deleted for 30 days before permanent removal.
- Grants carry subject, action, and effect fields; a refusal beats every allow.

Summaries were generated by AI. Generative AI is experimental.

---

Manage a persistent Library, its path, and its documents. You can upload files, wait until they are readable, inspect current state, and delete resources. Task attachments use the same Library resource through a conventional task-scoped path.

## Before you manage a Library

Have these values and the test file ready before the first call:

- Set `DUALE_TOKEN` to a token for your tenant.
- Set `DUALE_TENANT_ID` to the tenant path segment used by Library routes.
- Existing Libraries require a matching grant for each operation. When the SDK creates a Library, it grants the token's agent access to that new Library.
- Put a non-empty `contract.pdf` file beside the script below.

The calls below can create a Library. They do not grant access to an existing one.

## Plan the Library layout

Group a Library by who can read it, and grant each agent only the Libraries it needs. A grant covers a whole Library, so two sets of documents with different readers belong in two Libraries.

[Design your Libraries](https://dev.duale.ai/en/docs/libraries/design.md) states how many to create and where to draw the line between them.
[Limits](https://dev.duale.ai/en/docs/libraries/limits.md) states what bounds a Library. Decide the layout before you run the call below, because
a grant is easier to plan than to unpick.

## Create a Library and upload a document

`create()` returns an active Library at the requested path when the token can
change it. Otherwise, it creates a new Library there and grants the token's
agent access. Paths are non-unique display metadata. An inaccessible or deleted
Library does not reserve its path. This operation never changes access to
another Library at the same path.

```python runnable
import asyncio
from pathlib import Path

from duale import (
    LibraryCreateRequest,
    LibraryDocumentGetRequest,
    LibraryPatchRequest,
    LibraryResponseDocumentStatus,
    LibraryUpdateRequest,
    create_sdk,
)

async def main() -> None:
    async with create_sdk() as sdk:
        create_request = LibraryCreateRequest(path="projects/contracts")
        library = await sdk.libraries.create(create_request)
        library = await sdk.libraries.update(
            LibraryUpdateRequest(
                library_id=library.id,
                patch=LibraryPatchRequest(
                    tags={"project": "contracts", "year": 2026},
                ),
            ),
        )

        attachments = sdk.prepare_attachments(
            [(Path("contract.pdf"), "Signed client contract")],
        )
        receipts = await sdk.libraries.upload(library.id, attachments)
        receipt = receipts[attachments[0].key]

        document = await sdk.libraries.wait_for_document(
            LibraryDocumentGetRequest(
                library_id=library.id,
                document_id=receipt.document_id,
            ),
        )
        if document.status is LibraryResponseDocumentStatus.failed:
            print(document.failure)
            return

        print(library.id, document.document_id, document.status.value)

asyncio.run(main())
```

The request carries only `path`. The server creates the Library id and returns
it on `library.id`. If the call fails before it returns a response, call
`sdk.libraries.create(create_request)` again. The retry returns the writable
Library that the first attempt created. It creates one only when that attempt
left none.

`upload()` returns one receipt per attachment, keyed by the matching
`PreparedAttachment.key`. Each receipt includes the stable `library_id`, new
`document_id`, initial `queued` status, and polling location.

Use `library_id` and `document_id` with SDK methods. Ignore the receipt's `location` field.

`wait_for_document()` performs a `GET` every five seconds until the document reaches `ready` or `failed`. A 360-second timeout bounds the whole polling operation by default; pass a positive `timeout=` value to change that bound.

Three rules follow from that default:

- **Raise `timeout=` for large files.** The platform allows 30 minutes to extract a document and 15
  to index it, so the 360-second default is shorter than a large file can legitimately take.
- **A `TimeoutError` bounds the client only.** Ingestion continues, and the document still reaches
  `ready` or `failed`.
- **Poll with `get_document()` rather than uploading again.** Each `upload()` creates a new document
  instead of replacing the one in flight.

After a client timeout, keep polling the same document to a terminal state. Do not upload a copy to test the first one.

## Verify document ingestion

A successful run prints the Library id, document id, and terminal status:

```text
0192f7c1-6e2a-7c31-9a4d-2b6f8c1d4e90 0192f7d9-712b-7c31-8f08-845c30a42a31 ready
```

The returned `PublicIndexedDocument` also carries `progress_pct`, `page_count`, tags, and problem details in `failure`. A failed document always carries them; [Errors and reliability](https://dev.duale.ai/en/docs/sdk/errors.md) lists the causes.

## What the platform checks before a document is readable

Between upload and `ready`, the platform verifies each file and can reject it. Two checks fail the
document rather than the request, so they surface as a terminal `failed` status with a code rather
than as an error on `upload()`.

- **Integrity.** The platform recomputes the file's SHA-256 and size and compares them with what the
  upload declared. A mismatch fails the document with `HASH_MISMATCH` or `SIZE_MISMATCH`.
- **Malware.** Every file is scanned before extraction. A signature match fails the document with
  `MALWARE_DETECTED`, and the platform keeps the file with that failed document rather than
  extracting or indexing it.

[Errors and reliability](https://dev.duale.ai/en/docs/sdk/errors.md) lists every terminal code and what to do about it.

## File types the platform extracts

The platform extracts text from documents, spreadsheets, presentations, images, videos, email, subtitles, data, and code. From a video it reads the text visible in the frames, not the speech.

The platform reads text, so supply a recording as a transcript or as a subtitle file. It accepts nine subtitle formats, among them `.srt` and `.vtt`.

Documents include `.pdf`, `.docx`, `.doc`, `.rtf`, `.odt`, `.epub`, `.html`, `.md`, `.txt`, `.tex`, `.pages`, `.xps`, `.cbz`, and `.mhtml`. Data covers `.xml`, `.json`, `.jsonl`, and `.ndjson`. [Limits](https://dev.duale.ai/en/docs/libraries/limits.md) counts the extensions in each of the nine categories.

One route holds the current list. It needs `library:upload`, and it changes only with a release, so you can cache its response. Call it to populate a file picker:

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

Use the response, not the examples above, to decide whether the current deployment accepts a file.

## Document states

A document moves through four states, and `status` reports which one it is in: `queued`, `processing`, `ready`, or
`failed`. Branch on those four values and nothing else.

The diagram shows two more positions than `status` has values. Deleting a document leaves its `status` unchanged and adds
`deleted_at`; a document deleted permanently returns nothing. Neither position is a value your code can read.

```mermaid
flowchart LR
    accTitle: States a document moves through
    accDescr {
      An upload enters the queued state, then processing, where the platform
      verifies the file and scans it for malware before extracting and indexing
      it. Processing ends either at ready, where the document is searchable, or
      at failed, which is terminal and carries an error code. Deleting a ready
      document moves it to Recently deleted, where it stays for 30 days. From
      there a restore sends it back through queued, or the platform deletes it
      permanently after the window, which cannot be reversed. Deleting the whole
      Library also makes the document unrecoverable immediately, from either the
      ready state or the Recently deleted state. Physical removal follows asynchronously.
    }
    Queued[queued] --> Processing[processing]
    Processing --> Ready[ready: searchable]
    Processing --> Failed[failed: terminal, carries a code]
    Ready -->|delete_document| RecentlyDeleted[Recently deleted, 30 days]
    RecentlyDeleted -->|restore| Queued
    RecentlyDeleted -->|after 30 days| Gone[deleted permanently, unrecoverable]
    RecentlyDeleted -->|delete_library| Gone
    Ready -->|delete_library| Gone
```

`failed` carries a code and never becomes `ready` on its own. Nothing recovers a document once the platform has deleted
it permanently, so read `status` after every upload and before every recovery action.

## Replace a document

Upload the new version, wait until it is readable, then delete the old one. A document supports three operations: create it, change its tags, and delete it. None of them replaces a file in place, so a new version of a file is a new document with its own id.

Use this order:

1. **Upload the new version**

   Call `upload()` with the new file. The receipt carries a new `document_id`.
2. **Wait until it is readable**

   Call `wait_for_document()` on that id until the document reaches `ready`.
3. **Delete the old version**

   Call `delete_document()` on the old `document_id`.

The order decides what an agent reads in between:

| What you do          | What an agent reads until you finish                              |
| -------------------- | ----------------------------------------------------------------- |
| Keep the order above | Both versions answer between steps 1 and 3, and one version after |
| Delete first         | No version answers until the new document reaches `ready`         |
| Stop after step 2    | Both versions keep answering until someone deletes one            |

Step 3 stays reversible for 30 days. [Delete a document or Library](#delete-a-document-or-library) states that window and the restore path.

## Reuse and inspect a Library

`await sdk.libraries.list()` returns the Libraries that the token can access.
Pass `LibraryGetRequest` to `get()` for the current path and tags.

Pass `LibraryUpdateRequest` with a nested `LibraryPatchRequest` to `update()`.
The update records a revision of the Library's path and tags. The revision also
records who made the change and when. It never versions a document. A Library
id stays stable when its path changes.

`tags=` replaces the complete tag map; it does not merge keys. Read the current tags first when you need to preserve keys, or pass `tags={}` to clear them.

Pass `LibraryDocumentListRequest` to `list_documents()` to list documents that
are not deleted. Pass `LibraryDocumentGetRequest` to `get_document()` to read
one document's current status without polling. A list request must set `limit`.
It must set `cursor` to the prior page cursor, or to `None` for the first page.

## Share a Library

Creating a Library grants your agent every action on it. Another agent, person, or group reaches it
only through a grant you add.

A grant carries three fields:

| Field     | What it holds                                                                           |
| --------- | --------------------------------------------------------------------------------------- |
| `subject` | The agent, user, or group the grant applies to. A trailing `*` matches a prefix.        |
| `action`  | One Library action. A trailing `*` matches a prefix, so `library:*` covers all of them. |
| `effect`  | `allow` or `deny`.                                                                      |

**A refusal beats every allow.** [Access and isolation](https://dev.duale.ai/en/docs/libraries/access.md) states what a
refusal holds against and the order the platform applies to one request.

Adding and revoking grants needs `library:admin` on that Library, which its creator holds. The core
Python client does not wrap the grant routes: add and revoke them in the Dashboard, on the Library's
share control. [Library actions](https://dev.duale.ai/en/docs/sdk/reference.md#library-actions) lists what each action
covers.

A grant does not:

- **Narrow what a built-in role already reaches.** The editor and viewer roles carry Library access
  across the whole tenant. [Agents and access](https://dev.duale.ai/en/docs/agents-and-access.md) lists what each built-in
  role can do.
- **Scope what one task reads.** An agent's document tools reach every Library it can read, not only
  the one a task names. [Agent harness](https://dev.duale.ai/en/docs/agent-harness.md) states that reach.

## Delete a document or Library

Pass `LibraryDocumentDeleteRequest` to `delete_document()` to move one document
into the 30-day Recently deleted window. Repeating the call succeeds. Pass
`LibraryDeleteRequest` to `delete()` to stop normal reads and writes and remove
its documents. That call also succeeds when the Library is already absent.

**The core client cannot reverse either operation.** A deleted document stays in
Recently deleted for 30 days. During that time, a caller with `library:write` on
the Library can restore it in the Dashboard or through the platform restore
route.

Restoring re-indexes the document. It returns through `queued` and `processing`
before it is readable again. After 30 days, the document becomes eligible for
permanent removal and no interface can recover it. A deleted Library has no
restore route. Its record and grants become eligible for removal after 24 hours.

Deleting a document does not erase it at once. The platform keeps its bytes
through the 30-day window, then reclaims eligible records and content
asynchronously. Do not use the end of the window as an exact physical-removal
deadline.

**Deleting the Library ends that window early.** It makes every document in the
Library unrecoverable, including documents in Recently deleted. Record, grant,
and content reclamation then runs asynchronously.

Images inside an uploaded document reach an image-capable model as input when image support is enabled for it—see [Images in a document](https://dev.duale.ai/en/docs/sdk/attachments.md).

Use [Attach documents](https://dev.duale.ai/en/docs/sdk/attachments.md) when you want the SDK to choose the conventional path and submit the files with a task. See the [API reference](https://dev.duale.ai/en/docs/sdk/reference.md) for method signatures and [Errors and reliability](https://dev.duale.ai/en/docs/sdk/errors.md) for authorization and transport failures.

## Related content

- [SDK exception hierarchy, problem details, and retry behavior](https://dev.duale.ai/en/docs/sdk/errors.md)
- [How the platform protects and removes your documents](https://dev.duale.ai/en/docs/libraries/security.md)
- [Application-facing SDK API reference](https://dev.duale.ai/en/docs/sdk/reference.md)
- [Attach files to a task using the SDK](https://dev.duale.ai/en/docs/sdk/attachments.md)
- [Declare and serve Python tools with the SDK](https://dev.duale.ai/en/docs/sdk/tools.md)
- [Govern AI agents while they move to production](https://dev.duale.ai/en/solutions/governance.md)

---

## Sitemap

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