> 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/assets-and-custom-metadata.md).

# Assets and custom metadata

Upload reusable files and folders to Unitlab Assets with the Python SDK. Set destination paths, tags and custom metadata, then attach ready data to projects.

Use Assets for source data that will be organized, grouped, curated or reused across projects. Use [project uploads](https://docs.unitlab.ai/api-sdk-cli/sdks/project-uploads) when you want to send media directly into one project.

## Before you start

Install the [Python SDK](https://docs.unitlab.ai/api-sdk-cli/get-started/install-the-python-sdk-and-cli) and [configure your workspace API key](https://docs.unitlab.ai/api-sdk-cli/get-started/authentication-and-configuration). These examples use SDK 3.1.3 and start from this local folder:

```
working-directory/
└── images/
    ├── camera-7/
    │   ├── camera7-001.jpg
    │   └── camera7-002.jpg
    └── camera-8/
        └── camera8-001.jpg
```

Run Python from `working-directory` so `./images` resolves correctly. Use real supported media files. `FOLDER_ID`, `ASSET_ID`, `PROJECT_ID`, and `CLOUD_STORAGE_ID` below are placeholders for IDs from your workspace, not display names.

The upload, processing, and dataset sections form the main workflow. The folder-selection tabs, hierarchy example, cloud example, and CSV/LiDAR examples are alternatives; running every upload example would upload media more than once.

## Upload files or a folder

```python
from unitlab import UnitlabClient

client = UnitlabClient()
result = client.assets.upload(
    "./images",
    folder="Raw data",
    tags=["incoming"],
    custom_metadata={"collection": "road-inspection", "shift": "night"},
)

print(result.folder_id, result.uploaded)
for failure in result.failed:
    print(failure.path, failure.error)
```

`source` can be one file or a folder. Folder collection is recursive and includes supported media files. Unsupported files, including ordinary JSON and ZIP archives, are skipped. See [supported formats](https://docs.unitlab.ai/documentation/data/data-upload#supported-source-files).

`AssetUploadResult` contains `folder_id`, `folder_name`, `uploaded`, `failed`, `assets` and the raw responses. Partial success is preserved. Check failures before creating a dataset from the result.

For the three-image example, expect `result.uploaded == 3` and an empty `result.failed` list. This call puts the files into one Unitlab folder:

```
Raw data in Unitlab
├── camera7-001.jpg
├── camera7-002.jpg
└── camera8-001.jpg
```

This shows the Assets folder's contents, not a change to your local files. The default upload collects files recursively but does not reproduce the `camera-7` and `camera-8` subfolders.

## Choose a folder and path

{% tabs %}
{% tab title="Folder name" %}

```python
result = client.assets.upload("./images", folder="Raw data")
```

Creates or reuses the named destination folder.
{% endtab %}

{% tab title="Existing folder" %}

```python
result = client.assets.upload("./images", folder_id="FOLDER_ID")
```

Targets the folder by its stable ID.
{% endtab %}

{% tab title="Project-backed path" %}

```python
result = client.assets.upload(
    "./images/camera-7",
    folder_id="PROJECT_ROOT_FOLDER_ID",
    path="collection-01/camera-7",
)
```

The `path` option is honored only when `folder_id` identifies the root Assets folder that backs a project. It creates or reuses that path beneath the project root. For ordinary Assets folders, `path` is ignored.
{% endtab %}
{% endtabs %}

Use `folder` or `folder_id`, not both. If neither is supplied, the default destination is `Untitled upload`.

Ordinary upload does not automatically mirror local subdirectories: it sends each file with its basename. To organize ordinary Assets folders, create real subfolders with `create_subfolder()` and upload each local source folder to the corresponding `folder_id`. For a project-backed root, `path` selects one destination for the whole call. Avoid duplicate basenames when later annotation matching relies on filenames.

To preserve the two camera folders instead of using the flat upload above:

```python
from pathlib import Path

library = client.assets.create_folder("Road collection")
for source_folder in sorted(Path("./images").iterdir()):
    if not source_folder.is_dir():
        continue
    destination = library.create_subfolder(source_folder.name)
    branch = client.assets.upload(source_folder, folder_id=destination.id)
    if branch.failed:
        raise RuntimeError(f"Resolve upload failures in {source_folder.name}.")
```

This example handles the two camera directories shown above. Each upload still collects its own descendants recursively. For deeper hierarchy, create and target the corresponding Unitlab folder at every level you want to preserve.

## Wait for Assets to be ready

```python
for asset in result.assets:
    status = asset.wait(timeout=25200)
    if status.failed:
        print(asset.id, status.error)
```

An accepted upload can still require video, medical, slide or raster processing. Check terminal status and failures before attaching the data. To inspect an Asset later:

```python
asset = client.assets.asset("ASSET_ID")
print(asset.status())
```

`asset.wait()` returns the terminal processing status; it does not automatically treat every failed item as a Python exception.

For the main upload example, verify the folder contents as well:

```python
uploaded_folder = client.assets.folder(result.folder_id)
for asset in uploaded_folder.list_items():
    print(asset.id, asset.file_name, asset.upload_status)
```

Confirm all three expected filenames are present. Folder listing confirms membership; the processing checks above confirm whether each file is ready.

## Tags and custom metadata

Tags and the upload's `custom_metadata` object apply to every uploaded Asset in that call. Use metadata for your own collection context, such as camera, batch, location or source system.

```python
asset = client.assets.asset("ASSET_ID")
asset.update_custom_metadata({
    "source": "camera-7",
    "shift": "night",
    "approved": True,
})
```

A metadata update replaces the whole object. Include every key you want to retain. To clear it:

```python
asset.update_custom_metadata(None)
```

You can also call `client.assets.set_custom_metadata("ASSET_ID", metadata)`.

Custom metadata such as `approved` is your own data. Setting it does not approve an annotation or move an item through the review workflow.

## Create and inspect folders

```python
folder = client.assets.create_folder("Road collection")
subfolder = folder.create_subfolder("Camera 7")

print(client.assets.folders())
print(client.assets.all_folders())
print(folder.children())
print(folder.list_items())
```

For a saved cloud connection:

```python
folder = client.assets.create_cloud_folder(
    "Incoming road images",
    "CLOUD_STORAGE_ID",
    prefix="incoming/",
)
print(folder.sync_cloud())
```

See [Cloud storage](https://docs.unitlab.ai/api-sdk-cli/sdks/cloud-storage) for connection setup and browsing.

## CSV and LiDAR options

```python
rows = client.assets.upload(
    "./records.csv",
    folder="Records",
    generic_type="tabular",
    primary_column="text",
)

scenes = client.assets.upload(
    "./recordings",
    folder="LiDAR",
    generic_type="pointcloud",
)
```

CSV without a declared family remains unconfigured until its project context is selected. Pointcloud mode uploads detected scenes and preserves their scene-file structure. Use [LiDAR data preparation](https://docs.unitlab.ai/documentation/annotations/lidar-annotation#prepare-lidar-data) for scene manifests and supported folders.

## Publish and attach a reusable dataset

After resolving upload and processing failures, continue with `result` from the main upload example. Replace `PROJECT_ID` with the intended destination project. If you used the hierarchy alternative, use `library.id` in `folder_ids` instead of `result.folder_id`:

```python
dataset = client.datasets.create(
    "Road inspection dataset",
    folder_ids=[result.folder_id],
)
version = dataset.publish_version("Initial collection")
project = client.projects.get("PROJECT_ID")
attached = client.attach_dataset(
    project, dataset, version=version.version_number
)
if attached.batch_queue_id:
    status = project.batch_queue(attached.batch_queue_id).wait()
    if status.failed:
        raise RuntimeError("Inspect failed attached items.")
```

Use [Data Group automation](https://docs.unitlab.ai/api-sdk-cli/sdks/data-group-automation) before dataset publication when related files should become one grouped work item. Annotation JSON is imported into existing project media through [annotation imports](/api-sdk-cli/sdks/annotation-imports.md), not through Assets upload.

Continue with [dataset versions](https://docs.unitlab.ai/api-sdk-cli/sdks/dataset-versions) or [Unitlab's data curation platform](https://unitlab.ai/en/data-curation).
