---
title: Jobs
description: Jobs in the dltHub Runtime Python SDK (dlthub_sdk)
keywords: [dlthub, runtime, python sdk, dlthub_sdk, api]
---
# Jobs

<!-- generated from dlthub-client 0.28.8a3, do not edit -->

New to the SDK? [Getting started](getting-started.md) shows how to connect and use it.

## `Jobs` {#jobs}

The jobs of one workspace.

### `Jobs.trigger()` {#jobs-trigger}

```python
trigger(
    *,
    refs: Sequence[str] | None = None,
    selectors: Sequence[str] | None = None,
    profile: str | None | Keep = KEEP,
    refresh: bool = False,
    dry_run: bool = False,
) -> tuple[TriggerResult, ...]
```

Ask several jobs to run, by ref or by selector.

Unlike [`Job.start_run()`](#job-start_run), this does not take a trigger of its own.
A job named in `refs` fires its `default_trigger` (or a manual
trigger when it has none); a job matched by `selectors` fires the
trigger it matched, preferring its `default_trigger` when that matched
too.

**Arguments**

| Name | Type | Description |
| --- | --- | --- |
| `refs` | <code>Sequence[str] &#124; None</code> | Job refs to trigger. Each resolves to exactly one job. |
| `selectors` | <code>Sequence[str] &#124; None</code> | Trigger patterns to match, `fnmatch`-style — for example `"tag:backfill"`, `"schedule:*"`, `"manual:jobs.mod.*"`. |
| `profile` | <code>str &#124; None &#124; <a href="client#keep">Keep</a></code> | Profile for every run started. Omit for each job's default. |
| `refresh` | <code>bool</code> | Start the jobs that do start as a refresh, as for [`Job.start_run()`](#job-start_run). Downstream jobs that depend on their freshness are reset too. Ignored for a job whose `refresh_propagation` is `"block"`. |
| `dry_run` | <code>bool</code> | Report what would be triggered without starting anything. |

**Returns:** <code>tuple[<a href="#triggerresult">TriggerResult</a>, ...]</code> — One result per matched job; awaitable in async mode. Started jobs come first, then skipped ones, each in the platform's dependency order. A job the platform declined is present too, with its reason — see [`TriggerResult`](#triggerresult).

**Raises**

| Exception | When |
| --- | --- |
| [`Conflict`](errors.md#conflict) | The current state does not allow the call, e.g. the workspace is archived; `code` and `message` say why. |
| [`NotAuthorized`](errors.md#notauthorized) | The caller lacks the access this call needs; `message` says why. |
| [`NotFound`](errors.md#notfound) | A ref names no job in this workspace, or only an archived one. |
| [`ScopeMissing`](errors.md#scopemissing) | Reached without a workspace in scope. |
| `ValueError` | Neither `refs` nor `selectors` was given. |

### `Jobs.deploy_manifest()` {#jobs-deploy_manifest}

```python
deploy_manifest(
    manifest: DeployManifest,
    *,
    manifest_hash: str,
    deployment_module: str | None = None,
    description: str | None | Keep = KEEP,
    dry_run: bool = False,
) -> DeployReport
```

Reconcile the workspace's jobs against a deployment manifest.

The job definitions are passed through as the manifest carries them.
Upload the code with [`Deployments.upload()`](deployments.md#deployments-upload) first — this registers
what runs, not what it runs from.

**Arguments**

| Name | Type | Description |
| --- | --- | --- |
| `manifest` | <code><a href="#deploymanifest">DeployManifest</a></code> | The manifest to deploy, as dlt built it. Its jobs and engine version travel together, so the two cannot be crossed. |
| `manifest_hash` | <code>str</code> | Content hash of that manifest, from dlt's `generate_manifest_hash`. |
| `deployment_module` | <code>str &#124; None</code> | The deployment module this deploy claims. `None`, the default, is an ad-hoc deploy: jobs are added and updated but never archived. Naming one lets the platform archive the jobs that module owns and the manifest no longer lists. |
| `description` | <code>str &#124; None &#124; <a href="client#keep">Keep</a></code> | Workspace description, applied only when `deployment_module` is `"__deployment__"`. Omit, or pass `None`, to leave it as it is. |
| `dry_run` | <code>bool</code> | Report the plan without persisting it. Jobs in a dry-run report that are not yet deployed carry a placeholder `id` and cannot be acted on. |

**Returns:** <code><a href="#deployreport">DeployReport</a></code> — What was added, updated, left alone and archived; awaitable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`BadRequest`](errors.md#badrequest) | The request was rejected as invalid; `message`, `code` and `fields` say why. |
| [`Conflict`](errors.md#conflict) | The current state does not allow the call, e.g. the workspace is archived; `code` and `message` say why. |
| [`NotAuthorized`](errors.md#notauthorized) | The caller lacks the access this call needs; `message` says why. |
| [`ScopeMissing`](errors.md#scopemissing) | Reached without a workspace in scope. |
| `ValueError` | The manifest lists no jobs. |

### `Jobs.get()` {#jobs-get}

```python
get(*, id: str) -> Job
```

Return one job by id.

**Arguments**

| Name | Type | Description |
| --- | --- | --- |
| `id` | <code>str</code> | The job's uuid. To look a job up by its ref, use `get_by_ref()`. |

**Returns:** <code><a href="#job">Job</a></code> — The job; awaitable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`BadRequest`](errors.md#badrequest) | The request was rejected as invalid; `message`, `code` and `fields` say why. |
| [`NotFound`](errors.md#notfound) | No such job in this workspace. |
| [`ScopeMissing`](errors.md#scopemissing) | Reached without a workspace in scope. |

### `Jobs.get_by_ref()` {#jobs-get_by_ref}

```python
get_by_ref(ref: str) -> Job
```

Return one job by its ref, archived or not.

**Arguments**

| Name | Type | Description |
| --- | --- | --- |
| `ref` | <code>str</code> | The job ref, e.g. `jobs.load`. |

**Returns:** <code><a href="#job">Job</a></code> — The job; awaitable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`NotFound`](errors.md#notfound) | No job in this workspace has that ref. |
| [`ScopeMissing`](errors.md#scopemissing) | Reached without a workspace in scope. |

### `Jobs.count()` {#jobs-count}

```python
count(*, archived: bool | Keep = KEEP) -> int
```

Return how many jobs the workspace has, without fetching them.

**Arguments**

| Name | Type | Description |
| --- | --- | --- |
| `archived` | <code>bool &#124; <a href="client#keep">Keep</a></code> | As for `list()`. |

**Returns:** <code>int</code> — The job count; awaitable in async mode. Saturates at 10,001, so a value equal to that means "at least this many".

**Raises**

| Exception | When |
| --- | --- |
| [`ScopeMissing`](errors.md#scopemissing) | Reached without a workspace in scope. |

### `Jobs.list()` {#jobs-list}

```python
list(
    *,
    archived: bool | Keep = KEEP,
    limit: int | None = None,
    offset: int = 0,
) -> Iterable[Job] | AsyncIterable[Job]
```

Yield the jobs in the workspace, paging lazily.

Platform errors are raised when iteration begins, not by this call.

**Arguments**

| Name | Type | Description |
| --- | --- | --- |
| `archived` | <code>bool &#124; <a href="client#keep">Keep</a></code> | Keep only archived jobs, or only live ones. Omit for both, which is what the platform returns when nothing filters. |
| `limit` | <code>int &#124; None</code> | Return at most this many jobs. `None` walks to the end, or to row 10,100, past which the platform does not page. |
| `offset` | <code>int</code> | Skip this many jobs, server-side. |

**Returns:** <code>Iterable[<a href="#job">Job</a>] &#124; AsyncIterable[<a href="#job">Job</a>]</code> — An iterable of jobs; async-iterable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`ScopeMissing`](errors.md#scopemissing) | Reached without a workspace in scope. |
| `ValueError` | A negative `limit` or `offset`. |

## `Job` {#job}

A deployed job in a workspace.

**[Collections](getting-started.md#collections-and-attributes)**

| Name | Type | Example | Description |
| --- | --- | --- | --- |
| `job.runs` | <code><a href="job-runs#jobruns">JobRuns</a></code> | `job.runs.list()` | This job's runs. |

**Attributes**

| Name | Type | Description |
| --- | --- | --- |
| `job_ref` | <code>str</code> | Canonical job reference, unique per workspace. |
| `id` | <code>str</code> | Unique ID (a UUID). |
| `name` | <code>str &#124; None</code> | Display name for the job. |
| `job_type` | <code><a href="#jobtype">JobType</a></code> | The type of the job: batch, interactive, or stream. |
| `is_paused` | <code>bool</code> | Whether the job's scheduled runs are paused. The scheduler does not start it, including a scheduled run that was waiting for its upstream jobs; manual runs, runs started by a trigger request, and runs started by another job's success or failure still run. Only a job with `next_scheduled_run_at` can be paused, and a deploy that removes the schedule clears the pause. |
| `is_archived` | <code>bool</code> | Whether the job is archived. Archived jobs cannot be run and their public URL stops working. |
| `version` | <code>int</code> | Version number, starting at 1 and incremented by each update to the job: a job's current version, or a job version's own number. |
| `public_url` | <code>str &#124; None</code> | The public URL where the job can be accessed without authentication; `None` when the job is not published. It does not work while the job is archived. |
| `job_url` | <code>str &#124; None</code> | The URL of the job in the web app for an interactive job; `None` otherwise. |
| `default_trigger` | <code>str &#124; None</code> | The trigger fired when a run is started without naming one, computed from the job definition. |
| `triggers` | <code>tuple[str, ...]</code> | Every trigger the job answers to, computed from its definition; selectors match against these. |
| `next_scheduled_run_at` | <code>datetime &#124; None</code> | When the job is next scheduled to run; `None` if it has no schedule. |
| `job_definition` | <code>Mapping[str, Any]</code> | The job definition from the manifest, migrated to the platform's current format. Left opaque: it is dlt's own nested structure, and the SDK models none of it — as with the definitions `Jobs.deploy_manifest` accepts. |
| `created_at` | <code>datetime</code> | When it was created. |

### `Job.pause()` {#job-pause}

```python
pause() -> Job
```

Suspend the job's schedule.

Pausing a paused job changes nothing.

**Returns:** <code><a href="#job">Job</a></code> — A fresh snapshot with `is_paused` set; awaitable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`BadRequest`](errors.md#badrequest) | The request was rejected as invalid; `message`, `code` and `fields` say why. |
| [`Conflict`](errors.md#conflict) | The current state does not allow the call, e.g. the workspace is archived; `code` and `message` say why. |
| [`NotFound`](errors.md#notfound) | The job no longer exists. |

### `Job.resume()` {#job-resume}

```python
resume() -> Job
```

Resume the job's schedule.

Resuming a job that is not paused changes nothing.

**Returns:** <code><a href="#job">Job</a></code> — A fresh snapshot with `is_paused` cleared; awaitable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`Conflict`](errors.md#conflict) | The current state does not allow the call, e.g. the workspace is archived; `code` and `message` say why. |
| [`NotFound`](errors.md#notfound) | The job no longer exists. |

### `Job.publish()` {#job-publish}

```python
publish() -> Job
```

Expose the job on a public trigger URL.

Calling it again issues a new URL and invalidates the previous one.

**Returns:** <code><a href="#job">Job</a></code> — A fresh snapshot carrying `public_url`; awaitable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`Conflict`](errors.md#conflict) | The current state does not allow the call, e.g. the workspace is archived; `code` and `message` say why. |
| [`NotFound`](errors.md#notfound) | The job no longer exists. |

### `Job.unpublish()` {#job-unpublish}

```python
unpublish() -> Job
```

Withdraw the job's public trigger URL.

Unpublishing a job that is not published changes nothing.

**Returns:** <code><a href="#job">Job</a></code> — A fresh snapshot with `public_url` cleared; awaitable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`Conflict`](errors.md#conflict) | The current state does not allow the call, e.g. the workspace is archived; `code` and `message` say why. |
| [`NotFound`](errors.md#notfound) | The job no longer exists. |

### `Job.archive()` {#job-archive}

```python
archive() -> Job
```

Retire the job so it can no longer run, manually or on schedule.

Its schedule, default trigger and pause are cleared. List it with
`archived=True`. Archiving an archived job changes nothing.

**Returns:** <code><a href="#job">Job</a></code> — A fresh snapshot with `is_archived` set; awaitable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`Conflict`](errors.md#conflict) | The current state does not allow the call, e.g. the workspace is archived; `code` and `message` say why. |
| [`NotFound`](errors.md#notfound) | The job no longer exists. |

### `Job.unarchive()` {#job-unarchive}

```python
unarchive() -> Job
```

Let an archived job run again.

Its schedule and default trigger are restored from its definition; a
pause is not. Unarchiving a live job changes nothing.

**Returns:** <code><a href="#job">Job</a></code> — A fresh snapshot with `is_archived` cleared; awaitable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`Conflict`](errors.md#conflict) | The current state does not allow the call, e.g. the workspace is archived; `code` and `message` say why. |
| [`NotFound`](errors.md#notfound) | The job no longer exists. |

### `Job.start_run()` {#job-start_run}

```python
start_run(
    *,
    trigger: str = MANUAL_TRIGGER,
    profile: str | None | Keep = KEEP,
    refresh: bool = False,
    skip_freshness: bool = False,
) -> JobRun
```

Start a run of this job.

A run the platform declines to start raises, with its `code` naming why
and the platform's reasons in `fields`.

**Arguments**

| Name | Type | Description |
| --- | --- | --- |
| `trigger` | <code>str</code> | Which trigger to fire, in canonical form. Defaults to `"manual:"`. Only the form is validated, so a trigger the job definition does not declare is still honoured. |
| `profile` | <code>str &#124; None &#124; <a href="client#keep">Keep</a></code> | Profile to run under. Omit for the job's default. |
| `refresh` | <code>bool</code> | Start as a refresh: the job's progress goes back to its interval start and the run receives `refresh=True` in its run context; what to reload is up to the job's code. Downstream jobs that depend on its freshness are reset too. Ignored when the job's `refresh_propagation` is `"block"`. |
| `skip_freshness` | <code>bool</code> | Start even when the upstream jobs this job's freshness constraints name are not fresh. |

**Returns:** <code><a href="job-runs#jobrun">JobRun</a></code> — The run that started; awaitable in async mode.

**Raises**

| Exception | When |
| --- | --- |
| [`BadRequest`](errors.md#badrequest) | The request was rejected as invalid; `message`, `code` and `fields` say why. |
| [`Conflict`](errors.md#conflict) | The platform declined to start a run, e.g. an upstream is not fresh, the job is outside its interval, or a concurrency limit is reached; `code` says which. Also raised when the workspace is archived. |
| [`NotAuthorized`](errors.md#notauthorized) | The caller lacks the access this call needs; `message` says why. |
| [`NotFound`](errors.md#notfound) | No such job in this workspace. |
| [`ScopeMissing`](errors.md#scopemissing) | Reached without a workspace in scope. |

## `DeployReport` {#deployreport}

What a manifest deploy did to a workspace's jobs.

**Attributes**

| Name | Type | Description |
| --- | --- | --- |
| `added` | <code>tuple[<a href="#job">Job</a>, ...]</code> | Jobs the manifest introduced. |
| `updated` | <code>tuple[<a href="#job">Job</a>, ...]</code> | Jobs it changed, at their new version. |
| `unchanged` | <code>tuple[<a href="#job">Job</a>, ...]</code> | Jobs it left alone. |
| `archived` | <code>tuple[<a href="#job">Job</a>, ...]</code> | Jobs it removed, which happens only for a module deploy. |
| `warnings` | <code>tuple[str, ...]</code> | Always empty at present. |
| `deployment_module` | <code>str &#124; None</code> | The deployment module the deploy was scoped to. |

## `TriggerResult` {#triggerresult}

The outcome of asking a job to run through [`Jobs.trigger()`](#jobs-trigger).

Asking does not guarantee a run: the platform skips a trigger it cannot
honour and says why, so check `started` before expecting `job_run_id`.
A dry run creates no run, so `job_run_id` is `None` there too.

**Attributes**

| Name | Type | Description |
| --- | --- | --- |
| `job_ref` | <code>str</code> | The job reference of the matched job. |
| `job_id` | <code>str</code> | The ID of the matched job. |
| `status` | <code><a href="#triggerstatus">TriggerStatus</a></code> | Whether a run started, or why it did not. |
| `reasons` | <code>tuple[str, ...]</code> | Human-readable reasons for a skip, e.g. which upstream jobs are not fresh; `None` when the job was triggered or no reasons were recorded. |
| `trigger` | <code>str</code> | The trigger the job fires with. |
| `job_run_id` | <code>str &#124; None</code> | The ID of the created run; `None` in dry-run mode or when skipped. |
| `job_run_number` | <code>int &#124; None</code> | Number of the run within its job, counting up from 1; numbers repeat across jobs. |
| `matched_triggers` | <code>tuple[str, ...]</code> | Every trigger that selected this job: its default trigger when named in `job_ids`, plus any selector matches. |
| `started` | <code>bool</code> | Whether the platform started a run, or would in a dry run. |

## `DeployManifest` {#deploymanifest}

The part of a dlt deployment manifest a deploy reads.

Read-only and a subset, so dlt's own manifest type satisfies it structurally
and the SDK needs no dependency on dlt to say what it accepts.

**Attributes**

| Name | Type | Description |
| --- | --- | --- |
| `engine_version` | <code>int</code> | Manifest engine version the definitions were written for. |
| `jobs` | <code>Sequence[Mapping[str, Any]]</code> | Job definitions, exactly as the manifest carries them. |

## `JobType` {#jobtype}

How a job executes.

**Members**

| Member | Value | Description |
| --- | --- | --- |
| `BATCH` | `'batch'` | Runs to completion and exits. |
| `INTERACTIVE` | `'interactive'` | Serves an interactive session. |
| `STREAM` | `'stream'` | Runs continuously. |
| `UNKNOWN` | `'unknown'` | A type this SDK version does not know. |

## `TriggerStatus` {#triggerstatus}

What became of a request to run a job.

**Members**

| Member | Value | Description |
| --- | --- | --- |
| `TRIGGERED` | `'triggered'` | A run was created. |
| `SKIPPED_PAUSED` | `'skipped_paused'` | The job's schedule is suspended. |
| `SKIPPED_FRESH` | `'skipped_fresh'` | A job its freshness constraints depend on is not fresh yet; `reasons` names which. |
| `SKIPPED_ALREADY_COVERED` | `'skipped_already_covered'` | A completed run already covers this period. |
| `SKIPPED_OUT_OF_INTERVAL` | `'skipped_out_of_interval'` | Outside the job's configured window. |
| `SKIPPED_UPSTREAM_PENDING` | `'skipped_upstream_pending'` | A job it depends on is triggered in the same request; this job runs once that one completes. |
| `SKIPPED_CONCURRENCY_LIMIT` | `'skipped_concurrency_limit'` | The job already has as many active runs as its concurrency limit allows. |
| `SKIPPED_ORG_CONCURRENCY_LIMIT` | `'skipped_org_concurrency_limit'` | The organization is at its run limit. |
| `SKIPPED_MINUTES_LIMIT` | `'skipped_minutes_limit'` | The plan's compute minutes are exhausted. |
| `SKIPPED_TRIAL_EXPIRED` | `'skipped_trial_expired'` | The trial has ended. |
| `SKIPPED_WORKSPACE_ARCHIVED` | `'skipped_workspace_archived'` | The workspace is archived. |
| `UNKNOWN` | `'unknown'` | A status this SDK version does not know. |
