> For the complete documentation index, see [llms.txt](https://docs.unitlab.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.unitlab.ai/api-sdk-cli/sdks/create-and-download-releases.md).

# Create and download releases

Create versioned annotation exports with the Unitlab Python SDK, verify workflow completion, download labels and source files, and understand customer S3 delivery.

Use the Python SDK to create a versioned export of a project's eligible annotations, wait for preparation, and download labels or source files. Release scope can include the whole project or selected Batch Queues and data types.

{% hint style="info" %}
**Approval and export readiness are different.** Releases include eligible latest annotations. They do not automatically filter for human-approved work. Verify review completion for your selected project or Batch Queues before creating a delivery release.
{% endhint %}

![Project release configuration dialog](https://292810646-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FGjVLUz4wthGkGlRKM6rM%2Fuploads%2FHLzEBrP9ddsrTUbQ4ovz%2Frelease-create-modal.png?alt=media\&token=70baced4-91a7-4c40-bf7b-417177d75b90)

*The SDK uses the same project, Batch Queue, data-type, format, split and token choices as the project release dialog.*

## Create and download an annotation release

This example exports a verified image Batch Queue to UUEF, with 80% in `train` and 20% in `validation`. It uses `unitlab==3.1.3`; complete [installation](https://docs.unitlab.ai/api-sdk-cli/get-started/install-the-python-sdk-and-cli) and [authentication](https://docs.unitlab.ai/api-sdk-cli/get-started/authentication-and-configuration) first.

Run these steps in one Python session. Replace `PROJECT_ID` and `BATCH_QUEUE_ID` with real UUIDs. The local `./exports/` path is where the SDK will save the downloaded annotation artifact.

{% stepper %}
{% step %}

### Prepare the verified delivery scope

```python
from unitlab import UnitlabClient

PROJECT_ID = "PROJECT_ID"
BATCH_QUEUE_ID = "BATCH_QUEUE_ID"

client = UnitlabClient()
project = client.projects.get(PROJECT_ID)
```

Before creating the release, verify that every eligible item in the selected queue passed the required review. Use [the review checks below](#verify-review-completion-before-export). Keep that scope unchanged while its snapshot is prepared.
{% endstep %}

{% step %}

### Create and wait for the release

```python
release = client.releases.create(
    project,
    export_type="UUEF",
    split_ratios={"train": 80, "validation": 20},
    upload_sessions=[BATCH_QUEUE_ID],
    data_types=["image"],
    include_download_tokens=False,
)
print(release.id, release.version, release.status, release.data_item_count)
```

On success, retain the release ID and version. The returned release has completed preparation because this call waits by default. A preparation failure raises an error; do not start a new release until you have inspected the existing one.
{% endstep %}

{% step %}

### Download the annotations

```python
annotation_path = release.download(dest="./exports")
print(annotation_path)
```

The result is an absolute local path. With no `split`, this requests the combined archive for all populated splits. Source media is a separate download.
{% endstep %}

{% step %}

### Check the delivery before training

Open the downloaded artifact. Compare its represented item IDs and counts with your delivery record, inspect sample geometry and properties, and verify the split membership. Keep the release ID and version with the artifact. Export readiness does not certify review approval or downstream schema compatibility.
{% endstep %}
{% endstepper %}

`create()` waits by default until preparation succeeds or fails. Omit `upload_sessions` to include the whole project's eligible data. Omit `data_types` to include all available concrete types. `data_types=["multimodal"]` also includes all concrete types in the selected scope.

| Input                     | Behavior                                                                                                         |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `project`                 | Project handle or project ID.                                                                                    |
| `export_type`             | Export format, default `UUEF`. Use `standard_bundle` for per-type formats.                                       |
| `split_ratios`            | Nonnegative integer percentages for `train`, `validation` and/or `test`, totaling 100. Default `{"train": 100}`. |
| `upload_sessions`         | Optional Batch Queue handles or IDs.                                                                             |
| `data_types`              | Optional concrete families, such as `image`, `video` or `pointcloud`.                                            |
| `bundle_formats`          | Per-concrete-type format choices for `standard_bundle`.                                                          |
| `license_id`              | Dataset license ID when required for the workspace.                                                              |
| `include_download_tokens` | Include persistent Unitlab source-item download URLs. Default `False`.                                           |
| `completed_only`          | Release only items whose current workflow status is Complete. Default `False`. SDK 3.1.2 and later.              |
| `wait`                    | Wait for preparation by default. Set `False` to receive the pending release.                                     |
| `timeout`                 | Maximum preparation wait in seconds, default 7,200.                                                              |

## Verify review completion before export

Inspect current workflow states and the required Review-stage history before selecting a delivery cohort:

```python
for stage in project.workflow.stages:
    print(stage.id, stage.name, stage.type)

completed = project.data_units(
    status="complete",
    batch_queue="BATCH_QUEUE_ID",
)

for unit in completed:
    if not unit.workflow_task_id:
        continue
    task = project.workflow.get_task(unit.workflow_task_id)
    timeline = task.get_timeline()
    print(unit.id, task.stage_id, timeline["latest_history_id"])
    for event in timeline["events"]:
        print(
            event["edge_name"],
            event["from_stage_id"],
            event["actor_id"],
            event["at"],
        )
```

`status="complete"` filters current workflow state. `review` means work currently in review; `approved` and `reviewed` are not supported status aliases. A custom stage name does not change these state values. Discover stage IDs from `project.workflow.stages` when a workflow has multiple review stages.

An `approve` event identifies a Review-stage approval transition. Check the expected stage, reviewer and sequence of later events for your policy. A task can reach Complete through a workflow without human review or through a manager's direct move, so Complete alone is not an approval certificate.

The code above inspects workflow evidence; it does not restrict the release to the returned unit IDs. `client.releases.create()` has no `approved_only`, stage or item-ID filter; its only status filter is `completed_only` (below). Use a project or Batch Queue scope whose eligible annotations have all passed the required review. Keep that cohort stable until its release is frozen, and reconcile the exported content against your delivery record.

Data-unit and workflow-task responses contain state and metadata, not full annotation payloads. Download a release to retrieve exported labels. See [Workflow stages and tasks](https://docs.unitlab.ai/api-sdk-cli/sdks/workflow-stages-and-tasks) and [Review, rework and escalation](https://docs.unitlab.ai/documentation/workflows/review-rework-and-escalation).

### Release only completed items

```python
release = client.releases.create(project, export_type="UUEF", completed_only=True)
```

`completed_only=True` (SDK 3.1.2 and later; in the REST request, `"completed_only": true`) releases only the items whose current workflow status is **Complete** (for a Data Group, the group's own status), with their latest labels.

{% hint style="warning" %}
This filter checks the status at the moment the release is created. It does not prove that an item was reviewed or approved: an item can reach Complete through a route that has no Review stage.
{% endhint %}

* If no completed item has annotations, the request is refused with "No completed items with annotations to release."
* On a server that does not support the option, the SDK raises an error (`completed_only_unsupported`) instead of releasing every item.
* The option is available in the SDK and the API only. The release dialog in the web app and the CLI do not have it.

## Wait, resume and inspect

For a long-running export, use this alternative to the waiting create call above. It uses the same `client` and `project`; do not run it merely to recheck a release you already created:

```python
release = client.releases.create(
    project,
    export_type="UUEF",
    upload_sessions=["BATCH_QUEUE_ID"],
    wait=False,
)
print(release.id, release.status)

# Later, reuse the returned ID rather than creating another release.
release = client.releases.get("RELEASE_ID")
release.wait(timeout=7200)
print(release.status, release.data_item_count)
```

Preparation states are `pending`, `exporting`, `ready` and `failed`. `progress` is a percentage. Item count can be zero before the snapshot is frozen. Older ready releases can return `status=None`.

`release.refresh()` reloads metadata. `client.releases.list()` lists ready releases; keep the ID returned by creation to inspect an in-progress release. A second create while a release is still preparing can return `ConflictError` with code `release_in_progress`. A timeout does not cancel the server job. Reuse the existing ID instead of immediately creating another release.

`ready` means the export files are prepared. It does not mean the annotations were human-approved. `release.wait()` raises `UnitlabError` with code `release_failed` when preparation fails; inspect `status_error` before retrying.

## Choose an export format

| Data family                         | Supported formats                   |
| ----------------------------------- | ----------------------------------- |
| Image, video, medical and pathology | UUEF, COCO, YOLOv8, YOLOv5          |
| Geospatial                          | UUEF, GeoJSON, COCO, YOLOv8, YOLOv5 |
| Document and LiDAR (`pointcloud`)   | UUEF                                |
| Text, HTML, tabular and time series | UUEF, JSONL                         |
| Audio                               | UUEF, Audio JSON, RTTM              |
| Multiple concrete families          | UUEF or Standard Bundle             |

UUEF is Unitlab's unified export format. Choose a downstream format only after checking that it preserves the geometry, properties, relationships and temporal information your consumer needs. YOLO annotation exports do not include a ready-to-train source-image tree.

For mixed data, use UUEF or choose a compatible format for each concrete type in a Standard Bundle:

```python
release = client.releases.create(
    project,
    export_type="standard_bundle",
    data_types=["image", "audio"],
    bundle_formats={"image": "COCO", "audio": "Audio JSON"},
    split_ratios={"train": 100},
)
```

`bundle_formats` applies to Standard Bundle and uses concrete types, not `multimodal`. A mixed release cannot use a single native format such as COCO. See [Export formats and source inclusion](https://docs.unitlab.ai/documentation/releases/export-formats-and-source-inclusion).

## Download annotations and source files

Use an existing `client` and replace `RELEASE_ID` with the ID returned at creation. These are alternative download choices for that same release:

```python
release = client.releases.get("RELEASE_ID")
release.wait()

# One ZIP containing all populated splits.
all_splits = release.download(dest="./exports/all")

# One populated split, in its format-specific file or archive.
train_split = release.download(split="train", dest="./exports/train")

# Separate source-file download for the data represented by this release.
source_directory = release.download_files(dest="./exports/source-files")
```

`download()` returns the local absolute file path. Request only splits populated by the release. `download_files()` returns a local directory and retrieves represented source files separately from annotations. Video and medical exports can expose extracted frames or slices; inspect the downloaded layout for the selected modality.

The snapshot is taken when the server worker freezes the release, which may be later than the create request. It captures eligible annotation content, ontology and relevant metadata at that point. Edits before freeze can enter the release. Later project changes do not rewrite that frozen annotation snapshot. Access to customer-owned raw files still depends on the original objects and connection remaining available.

All tiles in a Data Group stay in one split. Loose items are divided within each concrete data type. Splits do not automatically keep every related patient, recording session or site together unless your grouping and source preparation establish those boundaries.

## Deliver annotation exports to Amazon S3

### Built-in output to connected source storage

When released data comes from a connected customer cloud-storage source, Unitlab saves generated annotation artifacts into that connection. For an S3 connection, this places the output in the connected bucket under its configured prefix:

```
<prefix>/unitlab-<workspace-prefix>/releases/<release-id>/<split>/...
<prefix>/unitlab-<workspace-prefix>/releases/<release-id>/all/...
```

Per-split artifacts are generated during preparation. A combined all-splits bundle is generated when requested. The connection needs permission to read source data and write generated output; replacing a combined bundle can also require deletion permission. Cloud-sourced releases remain private.

This output location comes from the released sources. Connecting a bucket alone does not redirect an export of Unitlab-hosted data. If the release contains several cloud connections, the server selects the first connection represented in the release. There is no per-release SDK parameter to choose a separate output bucket or prefix. Use a single-connection delivery scope when that destination matters. See [Cloud storage](https://docs.unitlab.ai/api-sdk-cli/sdks/cloud-storage).

### Copy to a different destination from your application

For a separate output bucket, download the annotation artifact and transfer it using your own AWS integration. Use the authenticated `client` from the first example; replace `RELEASE_ID` and `YOUR_OUTPUT_BUCKET`. This is a client-side transfer, separate from Unitlab's built-in storage behavior:

```python
from pathlib import Path
import boto3

release = client.releases.get("RELEASE_ID")
release.wait()
archive = Path(release.download(dest="./exports"))

destination_bucket = "YOUR_OUTPUT_BUCKET"
destination_key = f"unitlab-exports/{release.id}/{archive.name}"
boto3.client("s3").upload_file(
    str(archive),
    destination_bucket,
    destination_key,
)
```

Install `boto3` separately and configure AWS credentials through your application's normal credential chain. The example transfers the annotation artifact only. Use a new release-specific key and verify the uploaded object before downstream processing. The method is documented in the [Amazon S3 Boto3 reference](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/upload_file.html).

## Download URL lifetime

Annotation download responses use temporary signed URLs. `include_download_tokens=True` instead concerns persistent Unitlab source-item URLs embedded or returned for release data. It does not turn the annotation archive's signed URL into a permanent link or copy original source files into a backup.

Keep token URLs private and fetch a fresh download response when a signed archive URL expires.

## Related guides

* [Create a release in the product](https://docs.unitlab.ai/documentation/releases/create-a-release)
* [Releases API](https://docs.unitlab.ai/api-sdk-cli/apis/releases-api)
* [Annotation imports](https://docs.unitlab.ai/api-sdk-cli/sdks/annotation-imports)
* [Unitlab data curation](https://unitlab.ai/en/data-curation)

## Model predictions to a reviewed delivery

Follow [Pre-labels, review, and export](https://docs.unitlab.ai/api-sdk-cli/sdks/prelabels-review-and-export) for the complete workflow and the limits of approved-only filtering.
