---
title: "SDK exception hierarchy, problem details, and retry behavior"
description: "The Duale AI SDK exception hierarchy and RFC 9457 problem details envelope define how to classify, retry, and report task and Library failures."
lang: en
status: public-preview
lastUpdated: 2026-09-04
url: https://duale.ai/en/docs/sdk/errors
---

## AI-generated summary

Typed SDK exceptions, RFC 9457 problem details, document ingestion failure codes, retry ownership, and a troubleshooting table for Duale AI task and Library errors.

- DualeError is the base class; catching it catches every typed SDK exception subclass.
- Branch on error_code from the RFC 9457 envelope; check retryable before retrying.
- Document ingestion failures carry stable error codes that determine whether re-uploading the same bytes helps.
- A process-local circuit breaker opens after ten consecutive retryable terminal failures; one probe closes it.
- Task streams retry up to ten times; Library REST requests run once per call.

Summaries were generated by AI. Generative AI is experimental.

---

Task failures and `LibrariesClient` HTTP failures that the SDK translates use typed exceptions. Local input, filesystem, polling, object-store upload, and task-scoped attachment transport failures can raise other types. Platform problem details carry a machine-readable error code; branch on that code, not on a message string.

## Exception hierarchy

Every exception in the table below is importable from `duale`. `DualeError` is the base of the SDK's typed errors; catching it catches every subclass below. Common paths outside that hierarchy include:

- A submission rejected by the open task circuit breaker raises `RuntimeError`.
- Invalid client configuration raises `pydantic.ValidationError` when you construct `DualeConfig` directly. When
  `DualeSDK()` creates an invalid default config from the environment, it wraps that failure in `RuntimeError`.
- Missing attachment paths raise `FileNotFoundError`. Invalid Library update or polling arguments raise `ValueError`.
- `sdk.libraries.wait_for_document()` raises the built-in `TimeoutError` when the whole polling operation exceeds its client-side timeout.
- A cached `@activity` that exceeds `job_timeout` also raises the built-in `TimeoutError`.
- Local file and object-store upload failures raise `LibraryUploadError`. Its `attachment_key` and optional `part_number` identify the failed input; `completed_receipts` contains receipts that finished earlier in the same batch.

| Exception              | Raised when                                                                                                             |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `DualeError`           | Base class. A platform task failure is raised as `DualeError` with problem details attached, except `TaskStoppedError`. |
| `ValidationError`      | A result does not match the response model passed through `res=`.                                                       |
| `DualeAuthError`       | The server rejected the token or the caller lacks the required permission.                                              |
| `BusinessError`        | A platform request was valid HTTP but failed a domain rule, including Library `4xx` errors other than `401` and `403`.  |
| `LibraryUploadError`   | A local file or object-store upload failed.                                                                             |
| `ConfigurationError`   | A Library operation needs configuration, such as `DUALE_TENANT_ID`, that is missing.                                    |
| `MessagingError`       | Base class for SDK transport failures.                                                                                  |
| `DualeConnectionError` | A task stream, network request, or Library `5xx` response failed (subclass of `MessagingError`).                        |
| `TaskStoppedError`     | An agent stopped the task before it produced a result. Read `exc.reason` for the reason that agent gave.                |

Catch `MessagingError` to also catch `DualeConnectionError`.

`TaskStoppedError` carries no problem details: a stop is a decision an agent made, not a failure of the task. It reports `reason` and `task_id`, and nothing else.

`RoutingError`, `TaskTimeoutError`, `ActivityTimeoutError`, `TaskSubmissionError`, `AgentRegistrationError`, `CacheError`, `CacheConnectionError`, and `CacheSerializationError` remain importable from `duale` for compatibility. Current task, activity, registration, and cache paths do not raise those classes, so do not use them to classify current runtime failures.

## Problem details

Platform failures use one machine-readable envelope. Its seven decision fields tell your handler how to classify, retry,
or report the failure.

When the platform ends a task with a failure, `await response.model()` raises `DualeError` and attaches the RFC 9457
envelope on `exc.problem_details`. Library HTTP failures attach the same envelope to `DualeAuthError`, `BusinessError`,
or `DualeConnectionError`. Client-side errors such as local validation, configuration, or file reads have no server
envelope, so `problem_details` is `None`.

| Field                   | Meaning                                                                                                                                             |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `error_code`            | Stable wire identifier, for example `TASK_DEADLINE_EXCEEDED`, `AUTHORIZATION_FAILED`, `BILLING_LIMIT_EXCEEDED`, `CONTENT_FILTERED`. Branch on this. |
| `detail`                | Human-readable description of the failure.                                                                                                          |
| `retryable`             | Whether a client retry can succeed. Check this before retrying.                                                                                     |
| `retry_after_seconds`   | Suggested wait before a retry, when present.                                                                                                        |
| `owner_action_required` | The failure needs an account or configuration change, not a retry.                                                                                  |
| `error_category`        | One of `transient`, `upstream`, `integrity`, or `config`.                                                                                           |
| `errors[]`              | Per-error details, each with a field path and optional `ai_hints` for automated handling. `ai_hints` is not an envelope-level field.                |

The envelope also carries the standard RFC 9457 members `type`, `title`, `status`, and `instance`, plus the correlation
identifiers `request_id`, `trace_id`, and `span_id`.

Branch on `error_code`. Before you retry, require `retryable` and honor `retry_after_seconds` when present. If you report
the failure to support, include the correlation identifiers.

## Document ingestion failures

A document can fail after upload, and its stable error code tells you which recovery decision to make. Use the table
below before you upload the same bytes again.

`wait_for_document()` returns a failed document; it does not raise. `document.failure` carries an
RFC 9457 envelope, but not the `retryable`, `retry_after_seconds`, or `error_category` fields above,
so read this table to decide whether to retry. `error_code` is normally one of the values below;
treat an unlisted code as an unknown terminal cause.

The state is terminal: the document never becomes `ready` on its own. Upload again only when the
cause is one a new upload can change.

| `error_code`                         | What happened                                                                                                        | Your next action                                                                                                                                                                                              |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FORMAT_UNSUPPORTED`                 | The platform does not support this file type.                                                                        | Convert the file to a supported type, then upload the result.                                                                                                                                                 |
| `CORRUPT_DOCUMENT`                   | The file is damaged and cannot be parsed.                                                                            | Re-export the file from its source application.                                                                                                                                                               |
| `ENCRYPTED_DOCUMENT`                 | The file is an encrypted legacy `.doc` or `.ppt`, or an encrypted `.epub`.                                           | Remove the password, then upload the file again. A protected `.docx`, `.xlsx`, or `.pptx` reaches `ready` instead—see [Troubleshoot](#troubleshoot).                                                          |
| `DOCUMENT_TOO_LARGE`                 | The file passed the 500 MiB upload limit but tripped a scanning or extraction limit.                                 | Upload a smaller or simpler source. Splitting helps only when plain size was the cause.                                                                                                                       |
| `MALWARE_DETECTED`                   | The upload scan matched a malware signature.                                                                         | Do not upload it again. The platform keeps the scanned file with the failed document. Investigate the file at its source.                                                                                     |
| `HASH_MISMATCH`, `SIZE_MISMATCH`     | The stored bytes did not match what the upload declared.                                                             | Upload the file again from an unchanged local copy.                                                                                                                                                           |
| `EXPIRED_UPLOAD`                     | The upload session expired before the file finished uploading.                                                       | Start a new upload and finish it within the session.                                                                                                                                                          |
| `LIBRARY_DELETED`                    | The Library was deleted while the document was still being processed.                                                | Upload into a Library that is not deleted.                                                                                                                                                                    |
| `PERMISSION_DRIFT`                   | The uploading agent lost `library:write` on that Library while the document was being processed.                     | Grant `library:write` back to that agent, then upload again.                                                                                                                                                  |
| `EXTRACTION_FAILED`                  | The file produced neither searchable text nor a searchable picture, or extraction failed for a cause `detail` names. | Read `detail`: it separates an empty document from a parse failure. Check that the file opens and contains readable text or a picture the platform can decode. The same file rarely gives a different result. |
| `PROCESSING_TIMEOUT`                 | Extraction ran past its deadline.                                                                                    | Upload a smaller or simpler source.                                                                                                                                                                           |
| `INDEX_WRITE_FAILED`, `SEGMENT_LOST` | Processing failed inside the platform.                                                                               | Upload the same file again. Report it with the `request_id` if it repeats.                                                                                                                                    |

A failed document still occupies its Library until you delete it.

## Handle a failed task

Catch `DualeError`, read the code, and decide from the envelope (`response` and the handlers are your code):

```python
from duale import DualeError

try:
    result = await response.model()
except DualeError as exc:
    details = exc.problem_details
    if details is not None and details.error_code == "CONTENT_FILTERED":
        # Agent-boundary rejection. The platform marks it non-retryable.
        handle_rejected_task(details)
    elif details is not None and details.retryable:
        await schedule_retry(after=details.retry_after_seconds)
    else:
        raise
```

`CONTENT_FILTERED` does not expose the classifier's reasoning. Always check `problem_details.retryable` before applying your own retry logic.

## Retries and timeouts

Retry ownership depends on the operation:

- **Task stream.** The HTTP transport makes at most ten stream attempts. It reconnects a dropped event stream with exponential backoff and jitter, resuming from the last received event.
- **Task runtime.** The runtime retries a failing task within its policy and returns a terminal result.
- **Library REST.** Each management request runs once. `wait_for_document()` repeats successful non-terminal reads, but a failed read ends the poll.
- **Presigned upload.** Each part of a file upload retries network and timeout failures plus HTTP `500`, `502`, `503`, and `504`, for at most five attempts.

Every task carries an absolute deadline. Without an explicit `deadline`, the SDK sets one 1800 seconds (30 minutes)
ahead. A task that passes its deadline ends as a failure with `error_code` `TASK_DEADLINE_EXCEEDED`.

The SDK also runs a process-local circuit breaker. Ten consecutive retryable terminal failures open it; 30 seconds later it admits one task as a recovery probe, and only that task's success closes it. Cancellation, validation, business, user-tool, and explicitly non-retryable failures are neutral and do not open the breaker.

## Task circuit and activity capacity

Task submissions start immediately unless the process-local dependency circuit is open. The SDK does not put tasks through a local queue or token bucket. `max_jobs` and the activity fields in `BackpressureConfig` bound cached activities; the same config also sets the task circuit's failure threshold and recovery delay. See the [API reference](https://duale.ai/en/docs/sdk/reference.md).

## Troubleshoot

Confirm any fix by re-running the failing call: recovery means the symptom is gone—`model()` returns a result, a submission returns an `AgentResponse`, or `serve()` keeps running.

| Symptom                                                                      | Cause                                                                 | Fix                                                                                                                                                                                                                                                                                  |
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `await response.model()` has not returned when expected                      | The task has not reached a terminal event.                            | `model()` has no separate timeout. Keep `response.task_id` and compare elapsed time with the deadline (30 minutes by default). Follow [Tasks and results](https://duale.ai/en/docs/sdk/concepts.md#tasks-and-results) to stop the platform task or only cancel the local wait.                         |
| `ValidationError` from `model()`                                             | The result did not match the response model.                          | Align the model with the task, or drop `res` and read the raw completion.                                                                                                                                                                                                            |
| `DualeAuthError` with status `401`                                           | The server rejected the token.                                        | Ask the workspace administrator to reissue or rotate it. A missing or malformed token raises `pydantic.ValidationError` from direct `DualeConfig()` construction, or `RuntimeError` when `DualeSDK()` builds the config. Set `DUALE_TOKEN` to a `duale_` value of 10–256 characters. |
| `DualeAuthError` with status `403`                                           | The token's caller lacks the required Library grant.                  | Find the action the operation checks in [Library actions](https://duale.ai/en/docs/sdk/reference.md#library-actions), which also names who grants it, then repeat the operation.                                                                                                                       |
| `DualeConnectionError` during a task stream                                  | The task-stream connection dropped and did not recover.               | The task transport retries automatically; if it persists, check `DUALE_ENDPOINT` and network egress.                                                                                                                                                                                 |
| `BusinessError` during a Library call                                        | The Library rejected the operation with a domain `4xx`.               | Read `problem_details.error_code` and correct the request or resource state.                                                                                                                                                                                                         |
| `DualeConnectionError` during a Library call                                 | The network failed or the Library returned `5xx`.                     | Check `DUALE_ENDPOINT` and network egress. Retry only when the problem details mark the failure retryable.                                                                                                                                                                           |
| `LibraryUploadError`                                                         | A local read or object-store part failed.                             | Read `attachment_key` and `part_number`; preserve `completed_receipts`, then retry or reconcile only unfinished files.                                                                                                                                                               |
| `BusinessError` with `error_code` `DOCUMENT_UPLOAD_TOO_LARGE` (status `413`) | A document exceeds the 500 MiB upload limit.                          | Reduce or split the document before preparing and uploading it again.                                                                                                                                                                                                                |
| A `ready` document preview shows only an encryption type or rights label     | A password or a rights label protects the file.                       | The platform made only that protection metadata searchable. Remove the protection, upload the file again, and confirm that the preview now contains the source text.                                                                                                                 |
| A `ready` scan or figure gives no visual answer                              | No configured route carried the picture, or a route limit dropped it. | Configure an image-capable target whose limits admit the pictures, then repeat a question that only one picture answers. [Images in a document](https://duale.ai/en/docs/sdk/attachments.md#images-in-a-document) covers the setup.                                                                    |
| Built-in `TimeoutError` from an `@activity`                                  | The activity exceeded the SDK's `job_timeout`.                        | Treat the side effect as uncertain. Cancellation cannot roll back an external action that already completed.                                                                                                                                                                         |
| `error_code` is `CONTENT_FILTERED`                                           | The agent boundary rejected the task; non-retryable.                  | Do not retry. Inspect `problem_details` and adjust the request.                                                                                                                                                                                                                      |
| A submission raises `RuntimeError`                                           | The task dependency circuit is open.                                  | Wait for the reported recovery delay, then allow one probe task.                                                                                                                                                                                                                     |
| Serving tools raises an error about `agent_id`                               | No agent identifier is configured.                                    | Set `DUALE_AGENT_ID` to a provisioned agent before `sdk.serve()`.                                                                                                                                                                                                                    |

If the repeated call fails with the same symptom, use [Problem details](#problem-details) to branch on its stable code and
collect the correlation identifiers for support.

## Related content

- [Manage Libraries and documents with the SDK](https://duale.ai/en/docs/sdk/manage-libraries.md)
- [Application-facing SDK API reference](https://duale.ai/en/docs/sdk/reference.md)
- [Python SDK for bounded agent work with typed results](https://duale.ai/en/docs/sdk.md)
- [Attach files to a task using the SDK](https://duale.ai/en/docs/sdk/attachments.md)
- [SDK task lifecycle, routing, and streaming](https://duale.ai/en/docs/sdk/concepts.md)
- [Integrate model routing in an application](https://duale.ai/en/docs/model-routing/application-integration.md)

---

## Sitemap

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