> For the complete documentation index, see [llms.txt](https://docs.roboflow.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.roboflow.com/datasets/create-and-upload/upload-a-dataset.md).

# Upload a Dataset

Upload a structured dataset of images and annotations to a Roboflow project with the Python SDK.

This page covers bulk import of an existing labeled dataset (images plus their annotations) using the Python SDK, the HTTP API, or the CLI. To upload and manage individual images through the API, see [Manage Images](/datasets/manage/manage-images.md).

## Python SDK

`Workspace.upload_dataset()` uploads a structured dataset (images + matching annotations) to a Roboflow project. The project will be created if it doesn't exist; otherwise the new images get added to the existing project.

```python
import roboflow

rf = roboflow.Roboflow(api_key="YOUR_API_KEY")
workspace = rf.workspace()

workspace.upload_dataset(
    "./dataset/",                  # path to a structured dataset directory
    "my-detector",                 # project id (created if it doesn't exist)
    num_workers=10,
    project_license="MIT",
    project_type="object-detection",
    batch_name=None,
    num_retries=0,
    is_prediction=False,           # True for model-generated annotations awaiting review
)
```

### Parameters

* `dataset_path` (str) - path to the dataset root.
* `project_name` (str) - destination project's id. Created if it doesn't exist.
* `num_workers` (int, default `10`) - concurrent uploads. We recommend not exceeding 25.
* `project_license` (str, default `"MIT"`) - license for a newly-created project. Set to `"Private"` for private projects.
* `project_type` (str, default `"object-detection"`) - type for a newly-created project. Ignored if the project already exists.
* `batch_name` (str, optional) - group these uploads under a named batch. Useful for tracking the source of a labeling round.
* `num_retries` (int, default `0`) - retry transient upload failures.
* `is_prediction` (bool, default `False`) - set to `True` to upload annotations as model predictions awaiting review rather than ground truth.

### Where predictions land

When you save a prediction on an image that is still in an upload batch, Roboflow moves that image into a review job for the batch. Open the job to approve or fix the labels before they become ground truth.

If the image is already assigned to an annotation job, it stays in that job.

### Expected directory layout

For a COCO dataset:

```
my_dataset/
├── train/
│   ├── image1.jpg
│   └── _annotations.coco.json
├── valid/
│   ├── image2.jpg
│   └── _annotations.coco.json
└── test/
    ├── image3.jpg
    └── _annotations.coco.json
```

The folder that holds each image sets its split. Only these exact folder names count: `train`, `training`, `valid`, `validation`, `val`, `test`, and `testing`. Case does not matter, and file names are ignored, so an image named `test_image.jpg` does not land in the test split and a folder named `train2017` sets no split. When no folder matches, the image falls back to the upload's default split: for zip uploads that is the `split` value in your request, which defaults to `train`.

For VOC, drop matching `.xml` files alongside each image. For YOLO, drop matching `.txt` files plus a `data.yaml` describing the class list.

### Note on SHA-256 dedup (v1.3.6+)

As of `roboflow` 1.3.6, the SDK uploads original image bytes rather than re-encoding via Pillow. This brings parity with the web uploader and lets the Roboflow server deduplicate uploads by SHA-256 - re-uploading the same image (e.g. into a different batch) succeeds without consuming additional storage credits.

## HTTP API

### Upload a Dataset Zip

Upload a whole dataset as one ZIP file. Roboflow processes it in the background, so you do not hold a connection open while it runs. The ZIP can hold images and annotations in any supported format (ex: COCO, YOLO, Pascal VOC). For classification, folder names become class labels.

The upload takes three steps:

1. Start the upload. The response has a signed URL and a `taskId`.
2. PUT the ZIP to the signed URL with `Content-Type: application/zip`. Processing starts when the upload finishes.
3. Poll the task until its status is `completed` or `failed`.

{% openapi src="/files/vu4wzVM1cecgsHoKz1kz" path="/{workspace}/{project}/upload/zip" method="post" %}
[dataset-zip-upload.yaml](https://2252499186-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNVfRxNsvCh4GOyqrjmzX%2Fuploads%2Fgit-blob-f2c68858b502ade8fa2d75fef681a9413bb70b7b%2Fdataset-zip-upload.yaml?alt=media)
{% endopenapi %}

The signed URL is on Google Cloud Storage, not the Roboflow API, so send the file there without your API key:

```bash
curl -X PUT "$SIGNED_URL" \
  -H "Content-Type: application/zip" \
  --upload-file ./my-dataset.zip
```

By default, an image already in the project keeps its annotations, and the ZIP's annotation for it is skipped. Set `annotationOverwrite` to `true` to replace them.

A ZIP with annotations lands in one annotation job named after `batchName`. Every upload gets its own job, even when you reuse a name. If the workspace has [Review Mode](/datasets/annotate/annotate/team-collaboration.md#review-mode) on, the images wait in Review and join the dataset once a reviewer approves them.

{% openapi src="/files/vu4wzVM1cecgsHoKz1kz" path="/{workspace}/upload/zip/{taskId}" method="get" %}
[dataset-zip-upload.yaml](https://2252499186-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNVfRxNsvCh4GOyqrjmzX%2Fuploads%2Fgit-blob-f2c68858b502ade8fa2d75fef681a9413bb70b7b%2Fdataset-zip-upload.yaml?alt=media)
{% endopenapi %}

PDFs inside the ZIP are skipped with an unsupported-format warning. Videos are skipped the same way, except in Action Recognition projects.

In an Action Recognition project, each MP4 or MOV video up to 3 GB in the ZIP is added as a video, like a [video upload](/datasets/create-and-upload/adding-data.md#upload-a-video). Images in the ZIP are skipped with a warning. Videos get the upload's tags, batch, and split, and a `train`, `valid`, or `test` folder sets the split the same way it does for images. Use each ID in `result.videoIds` to [check that video's status](/datasets/create-and-upload/adding-data.md#upload-a-video). Video labels inside the ZIP are not imported.

## CLI

You can upload datasets with images and/or annotations using the Roboflow CLI.

We have prepared a video that walks through how to upload a dataset:

{% embed url="<https://www.loom.com/share/19637984033a466b831af56f9404fa89>" %}

### Upload a Directory

Use `roboflow image upload` with a directory path to bulk-upload a dataset with parallel per-image uploads:

```bash
roboflow image upload /path/to/dataset/folder -p PROJECT_ID
```

Or use the shorthand alias:

```bash
roboflow upload /path/to/dataset/folder -p PROJECT_ID
```

The CLI auto-detects whether the path is a file, directory, or `.zip` file. A directory triggers a bulk import with parallel per-image uploads unless you use `--zip-upload`.

#### Options

| Flag                  | Description                               |
| --------------------- | ----------------------------------------- |
| `-p`, `--project`     | Project ID (required)                     |
| `-c`, `--concurrency` | Number of parallel uploads (default: 10)  |
| `-b`, `--batch`       | Batch name for grouping uploads           |
| `-r`, `--retries`     | Retry failed uploads N times (default: 0) |
| `-s`, `--split`       | Override split for all uploaded images    |

Example with options:

```bash
roboflow upload ./my-dataset -p my-project -c 20 -b "april-batch" -r 3
```

### Upload a Zip File

Use zip uploads for larger datasets or when your dataset is already packaged as a `.zip` file. Zip uploads use Roboflow's asynchronous zip upload flow. By default, the CLI uploads the zip file and waits for processing to finish.

This is the same flow documented in [HTTP API](#http-api) above. The CLI handles the signed URL upload and task polling for you.

To upload an existing zip file:

```bash
roboflow image upload /path/to/dataset.zip -p PROJECT_ID
```

To zip a local directory client-side and upload it with the async zip flow, use `--zip-upload` with the primary command:

```bash
roboflow image upload /path/to/dataset/folder -p PROJECT_ID --zip-upload
```

#### Options for Zip Uploads

| Flag              | Description                                                                              |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `-p`, `--project` | Project ID (required)                                                                    |
| `--zip-upload`    | Zip a directory client-side and upload it with the async zip upload flow                 |
| `--no-wait`       | Return immediately after the zip is uploaded instead of waiting for processing to finish |
| `-b`, `--batch`   | Batch name for grouping uploads                                                          |
| `-s`, `--split`   | Split set: train, valid, or test                                                         |
| `-t`, `--tag`     | Comma-separated tag names                                                                |

Example with zip upload options:

```bash
roboflow image upload ./my-dataset.zip -p my-project -s train -t "outdoor,daytime" -b "april-batch"
```

To start processing and return a task ID immediately:

```bash
roboflow image upload ./my-dataset.zip -p my-project --no-wait --json
```

The JSON response includes the asynchronous task ID:

```json
{
  "status": "pending",
  "task_id": "task-123",
  "path": "./my-dataset.zip",
  "project": "my-project",
  "result": {
    "task_id": "task-123",
    "status": "pending"
  }
}
```

Zip uploads do not support `--is-prediction`. Use the regular per-image upload flow for prediction uploads.

### Supported Project Types

You can upload data for the following project types:

* Object Detection
* Single-Label Classification
* Multi-Label Classification
* Instance Segmentation
* Semantic Segmentation
* Keypoint Detection

### Supported data.yaml Formats

The CLI supports both list-style and key-value pair formatted class name mappings in `data.yaml` files during dataset uploads:

```yaml
nc: 3
names: ['Paper', 'Rock', 'Scissors']
```

OR

```yaml
nc: 3
names:
  0: Paper
  1: Rock
  2: Scissors
```

### JSON Output

For automation, use `--json`:

```bash
roboflow upload ./my-dataset -p my-project --json
```

```json
{"status": "uploaded", "path": "./my-dataset", "project": "my-project"}
```

## MCP Server

Connect your AI agent to the [MCP Server](https://docs.roboflow.com/agents/mcp-server) and it can upload a dataset with these tools: The zip holds images only, so labels are saved in a second step.

<table data-search="false"><thead><tr><th width="290">Tool</th><th>Description</th></tr></thead><tbody><tr><td><code>image_upload</code></td><td>Upload local image files to a project via a zip. Images only, no annotation files.</td></tr><tr><td><code>image_upload_status</code></td><td>Check the status of an image zip upload task.</td></tr><tr><td><code>annotations_save</code></td><td>Save an annotation for an image that is already uploaded.</td></tr></tbody></table>

## Next steps

* Label any unannotated images so they can be used for training. See [Introduction to Roboflow Annotate](/datasets/annotate/annotate/annotation-tools.md).
* Turn your images into a trainable snapshot. See [Create a Dataset Version](/datasets/versions/dataset-versions/create-a-dataset-version.md).
