---
title: "Jobs"
description: "One view of every job your organization is running - list, read, wait for and cancel training, auto-annotation, workflow runs, evaluations, exports and imports."
section: "API Reference"
order: 17.7
---
Long-running work in Pictograph comes in many kinds: training runs, batch
auto-annotation, workflow runs, model evaluations, exports, and the background jobs
behind imports, video frame extraction and bulk image operations. Each kind has its own
status call on its own resource. `client.jobs` answers "what am I waiting on?" across all
of them at once.

Every job reads the same way, whatever its kind:

- `type` - `training`, `auto_annotate`, `workflow_run`, `evaluation`, `export`, or a
  background job's own type (`connector_import`, `public_dataset_import`,
  `video_extract`, `batch_move`, ...).
- `status` - one vocabulary for every kind: `queued` and `running` are active,
  `completed`, `failed` and `cancelled` are finished.
- `progress` - 0 to 100.
- `detail` - what only that kind has: a training run's epochs, a workflow run's frames,
  a background job's item counts.
- `cancel_path` - the API route that cancels the job, or `None` when it is finished or
  its kind cannot be cancelled through the API.

The list is scoped to the API key's organization, and any role can read it. Finished
background jobs (imports, extractions, bulk operations) are kept 30 days; training runs,
workflow runs, batch auto-annotation and evaluations keep their history.

## list

Your organization's jobs, newest first. Queued and running jobs by default.

Source: [`Jobs.list`](https://github.com/pictograph-io/pictograph-sdk/blob/v1.69.160/src/pictograph/resources/jobs.py)

| Arg | Type | Default | Notes |
|---|---|---|---|
| `status` | `str` | `"active"` | `active`, `finished`, `all`, or one of `queued`, `running`, `completed`, `failed`, `cancelled` |
| `type` | `str \| None` | `None` | Only jobs of this kind |
| `page_size` | `int` | `50` | Jobs per request, maximum 100 |
| `max_total` | `int \| None` | `None` | Stop after this many jobs |

```python
for job in client.jobs.list():
    print(job.type, job.status, job.progress, job.name)

failed = list(client.jobs.list(status="failed", type="training", max_total=20))
```

```bash
pictograph jobs list
pictograph jobs list --status failed --type training --limit 20
```

```bash
curl -s "https://api.pictograph.io/api/v1/developer/jobs?status=failed&type=training&limit=20&offset=0" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"
```

**Returns** `OffsetPager[Job]`

<details>
<summary><code>Job</code> &middot; 12 fields</summary>

```python
class Job(BaseModel):
    """One job, whatever kind of work it is."""
    id: str
    type: str
    status: str
    progress: float | None = None
    name: str | None = None
    dataset_id: str | None = None
    error: str | None = None
    created_at: datetime | None = None
    updated_at: datetime | None = None
    completed_at: datetime | None = None
    cancel_path: str | None = None
    detail: dict[str, Any] = {}
```

</details>

The API reads at most the newest 1,000 jobs of one filter (`offset + limit` up to 1000);
narrow with `status` or `type` past that.

## get

One job, by the id the work answered when it started: a training run's id, an import's
id, a workflow run's id, and so on. Either spelling of a uuid works.

Source: [`Jobs.get`](https://github.com/pictograph-io/pictograph-sdk/blob/v1.69.160/src/pictograph/resources/jobs.py)

| Arg | Type | Default | Notes |
|---|---|---|---|
| `job_id` | `str` | required | The job's id |

```python
job = client.jobs.get(
    job_id="c1c1c1c1-0000-4000-8000-000000000002",
)
print(job.type, job.status, job.progress, job.error)
```

```bash
pictograph jobs get c1c1c1c1-0000-4000-8000-000000000002
```

```bash
curl -s "https://api.pictograph.io/api/v1/developer/jobs/c1c1c1c1-0000-4000-8000-000000000002" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"
```

**Returns** `Job`

<details>
<summary><code>Job</code> &middot; 12 fields</summary>

```python
class Job(BaseModel):
    """One job, whatever kind of work it is."""
    id: str
    type: str
    status: str
    progress: float | None = None
    name: str | None = None
    dataset_id: str | None = None
    error: str | None = None
    created_at: datetime | None = None
    updated_at: datetime | None = None
    completed_at: datetime | None = None
    cancel_path: str | None = None
    detail: dict[str, Any] = {}
```

</details>

Another organization's job returns 404, like an id that does not exist.

## wait

Poll a job until it finishes. Returns the completed job; raises when it failed or was
cancelled.

Source: [`Jobs.wait`](https://github.com/pictograph-io/pictograph-sdk/blob/v1.69.160/src/pictograph/resources/jobs.py)

| Arg | Type | Default | Notes |
|---|---|---|---|
| `job_id` | `str` | required | The job's id |
| `poll_interval` | `float` | `3.0` | Seconds between status checks |
| `timeout` | `float` | `3600.0` | Seconds to wait. The job keeps running after it |
| `sleep` | `Callable \| None` | `None` | Injectable sleep (tests) |

```python
from pictograph.exceptions import ApiError, PollTimeoutError

try:
    job = client.jobs.wait(
        job_id="c1c1c1c1-0000-4000-8000-000000000002",
        timeout=1800,
    )
except ApiError as err:
    print(err.code, err.response["error"])  # job_failed or job_cancelled
except PollTimeoutError:
    print("still running - wait again later")
```

```bash
pictograph jobs wait c1c1c1c1-0000-4000-8000-000000000002 --timeout 1800
```

```bash
# REST: poll the job until its status is completed, failed or cancelled
curl -s "https://api.pictograph.io/api/v1/developer/jobs/c1c1c1c1-0000-4000-8000-000000000002" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"
```

**Returns** `Job`

<details>
<summary><code>Job</code> &middot; 12 fields</summary>

```python
class Job(BaseModel):
    """One job, whatever kind of work it is."""
    id: str
    type: str
    status: str
    progress: float | None = None
    name: str | None = None
    dataset_id: str | None = None
    error: str | None = None
    created_at: datetime | None = None
    updated_at: datetime | None = None
    completed_at: datetime | None = None
    cancel_path: str | None = None
    detail: dict[str, Any] = {}
```

</details>

The CLI exits 0 when the job completed, non-zero when it failed or was cancelled, and 10
when it was still running at `--timeout`.

## cancel

Cancel a queued or running job. The cancel goes through the route that cancels its
kind, with that kind's own rules: a training run is refunded what it did not use, and a
cancelled workflow run keeps its outputs so far, its time on its endpoint billed as
uptime. The answer is the job as it reads after the cancel.

Source: [`Jobs.cancel`](https://github.com/pictograph-io/pictograph-sdk/blob/v1.69.160/src/pictograph/resources/jobs.py)

| Arg | Type | Default | Notes |
|---|---|---|---|
| `job_id` | `str` | required | The job's id |

```python
job = client.jobs.cancel(
    job_id="c1c1c1c1-0000-4000-8000-000000000002",
)
print(job.status)
```

```bash
pictograph jobs cancel c1c1c1c1-0000-4000-8000-000000000002
```

```bash
# REST: read the job, then POST to its cancel_path
curl -s -X POST "https://api.pictograph.io/api/v1/developer/training/c1c1c1c1-0000-4000-8000-000000000002/cancel" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"
```

**Returns** `Job`

<details>
<summary><code>Job</code> &middot; 12 fields</summary>

```python
class Job(BaseModel):
    """One job, whatever kind of work it is."""
    id: str
    type: str
    status: str
    progress: float | None = None
    name: str | None = None
    dataset_id: str | None = None
    error: str | None = None
    created_at: datetime | None = None
    updated_at: datetime | None = None
    completed_at: datetime | None = None
    cancel_path: str | None = None
    detail: dict[str, Any] = {}
```

</details>

Training runs, batch auto-annotation, workflow runs, model evaluations and V7 / Roboflow
imports can be cancelled through the API. A job whose `cancel_path` is `None` cannot:
the call raises `ValidationError` without sending anything.