> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.ampdb.io/api/mcp` to find what you need.

# Jobs

A **job** is a unit of work Amp schedules on a [worker](/managing-amp/workers): extracting a chain's blocks into a
raw dataset, running a derived dataset's SQL, deleting files no query needs anymore, or exporting tables to an
external database. Most jobs are created for you when you [deploy a dataset](/managing-amp/datasets#deploy-a-dataset).
This page covers what jobs there are, and how to create, list, inspect, stop, and remove them.

You manage jobs with [`ampctl job`](/clis/ampctl/job) or the [Admin API](/managing-amp/admin-api/jobs). This page
shows both. Every `ampctl` command also takes `--json`, which prints the result as JSON, for scripts. Where a job is in
its life is its status: see [Job Lifecycle](/managing-amp/jobs/lifecycle).

## Prerequisites

* Amp running, with its Admin API reachable. [Solo mode](/running-amp/solo-mode) serves it at
  `http://localhost:1610`, where `ampctl` looks by default. For another Amp, set its URL, and a token if it needs one:

  ```bash
  export AMP_ADMIN_URL=https://amp.example.com:1610
  export AMP_AUTH_TOKEN=your-token
  ```

* `ampctl` installed (see [Installation](/running-amp/installation)).

* For the examples, which follow the [Quickstart](/get-started/quickstart): `acme/eth_mainnet@1.0.0` registered and
  deployed (see [Managing Datasets](/managing-amp/datasets)), and its manifest hash in `MANIFEST_HASH`:

  ```bash
  export MANIFEST_HASH=$(ampctl dataset inspect acme/eth_mainnet@1.0.0 --json | jq -r .manifest_hash)
  ```

* For the export examples, an ADBC provider named `warehouse` (see
  [Destinations](/managing-amp/pipelines/destinations)).

## Kinds of jobs

Each job has a kind, which says what it does:

| Kind                  | What it does                                                                                                 | Created by                                                   |
| --------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ |
| `materialize-raw`     | Extracts a [raw dataset](/datasets/raw-datasets)'s blocks from its provider into its tables.                 | Deploying a raw dataset                                      |
| `materialize-derived` | Runs a [derived dataset](/datasets/derived-datasets)'s SQL and writes the results.                           | Deploying a derived dataset                                  |
| `materialize-phaser`  | Writes a dataset that a [Phaser](/providers/phaser) bridge serves.                                           | Deploying a Phaser dataset                                   |
| `gc`                  | Deletes a dataset's files that no query reads anymore, once their grace period has passed.                   | Deploying a dataset (it runs every hour), or `job create gc` |
| `adbc`                | Exports a dataset's tables to a PostgreSQL or Snowflake database.                                            | `job create adbc`                                            |
| `composite`           | Groups a deployment's jobs. The controller runs it rather than a worker, and its status is the deployment's. | Deploying a dataset                                          |

Garbage collection is explained in [Garbage Collection](/running-amp/storage/garbage-collection), and exports in
[Exports](/managing-amp/pipelines).

### Deployment jobs

Deploying a dataset revision schedules three jobs at once: the **materialization** (`materialize-raw`,
`materialize-derived`, or `materialize-phaser`), **garbage collection** (`gc`, every hour), and a **parent**
(`composite`) that groups them. The parent's ID is the one `ampctl dataset deploy` prints, and its worker is
`controller`, since it runs on the controller.

The parent's status follows its children's:

* `RUNNING` while any child is still active.
* `FATAL` once every child has finished and at least one failed permanently.
* `COMPLETED` once every child has finished and none failed permanently.

A child you stop doesn't stop the parent, and stopping the parent stops its children. To stop a deployment, use
[`ampctl dataset stop`](/managing-amp/datasets#stop-a-deployment) rather than the job commands, and follow it with
[`ampctl dataset status`](/managing-amp/dataset-status).

## IDs and idempotency keys

Each job has an **ID**, an integer that grows with each new job, and an **idempotency key**, 64 hexadecimal characters
derived from what the job works on: its kind and dataset revision and, for an `adbc` job, the tables it exports and
the provider it writes through.

The key keeps Amp from running the same work twice. When you create a job whose key matches a job that hasn't
finished (one that isn't `COMPLETED`, `STOPPED`, or `FATAL`):

* With the same options, you get that job's ID back, and nothing new is scheduled.
* With different options, creation fails with `409 Conflict` and `ACTIVE_JOB_CONFLICT`. Stop the job first.

When the matching job has finished, creating it again schedules that same job again: it keeps its ID and its history,
and runs with the options you pass.

## Create a job

`ampctl job create` creates garbage collection (`gc`) and export (`adbc`) jobs. The Admin API's
[`POST /jobs`](/managing-amp/admin-api/jobs#create-a-job) also accepts the materialization and `composite` kinds, but
deploying a dataset is how you create those.

The scheduler places a new job on a worker that has sent a heartbeat in the last 5 seconds: the one you name with
`--worker-id`, a random one among those matching it, or else the controller's
[`controller.default_worker_id`](/running-amp/configuration-reference#controllerdefault_worker_id), or a random
active worker. The job then stays on that worker, retries included. Creation fails with `NO_WORKERS_AVAILABLE` when no
worker is active, and `WORKER_NOT_AVAILABLE` when the worker you name isn't.

### Collect garbage

A `gc` job deletes the files of a dataset revision that no query reads anymore, such as those that compaction replaced,
once their grace period has passed.

:::terminal
```bash
ampctl job create gc acme eth_mainnet "$MANIFEST_HASH"
```

```
✓ Job 9 created
```
:::

The Admin API answers `202 Accepted` with the job's ID:

```bash
curl -X POST http://localhost:1610/jobs \
  -H 'Content-Type: application/json' \
  -d "$(jq -n --arg hash "$MANIFEST_HASH" \
    '{kind: "gc", dataset_namespace: "acme", dataset_name: "eth_mainnet", manifest_hash: $hash}')"
```

```json
{ "job_id": 3 }
```

A deployment already runs a garbage collection job every hour, and `job create gc` for the same revision has the same
idempotency key, so it doesn't add a job: it runs that one again now, or fails with `409` while it's scheduled or
running. That's why the example prints job 3, the deployment's.

:::warning
This reschedules the deployment's job with the new options. With the default `--trigger one_shot`, the deployment loses
its hourly garbage collection, and the job leaves the deployment. `--trigger interval --trigger-interval 3600` brings
back the hourly runs, but the job stays out of the deployment, so stopping the deployment won't stop it.
:::

| Argument        | Description                                                                             |
| --------------- | --------------------------------------------------------------------------------------- |
| `NAMESPACE`     | The dataset's namespace, like `acme`.                                                   |
| `NAME`          | The dataset's name, like `eth_mainnet`.                                                 |
| `MANIFEST_HASH` | The revision's manifest hash: 64 hexadecimal characters, from `ampctl dataset inspect`. |

| Option                              | Type                                | Default                                               | Description                                                                                                                       |
| ----------------------------------- | ----------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `--worker-id <WORKER_ID>`           | worker ID or glob pattern           | the controller's default worker, or any active worker | The worker to run the job on, like `--worker-id worker-01`, or one of those matching a pattern, like `--worker-id 'worker-*'`.    |
| `--trigger <TRIGGER>`               | `one_shot`, `interval`, or `cron`   | `one_shot`                                            | When the job runs: once, or again and again. See [Triggers](/managing-amp/jobs/triggers), which covers the `--trigger-*` options. |
| `--retry-strategy <RETRY_STRATEGY>` | `none`, `bounded`, `unless_stopped` | `unless_stopped`                                      | How the job is retried when it fails. See [Retries](/managing-amp/jobs/retries), which covers the `--retry-*` options.            |

### Export to a database (Unstable)

An `adbc` job exports a dataset revision's tables to a PostgreSQL or Snowflake database, through an ADBC provider you
registered. It keeps exporting new blocks as the dataset grows, unless you give it an end block.

:::terminal
```bash
ampctl job create adbc acme eth_mainnet "$MANIFEST_HASH" --provider warehouse --table blocks --table logs
```

```
✓ Job 10 created
```
:::

With the Admin API, `end_block` is a string, like `"5000000"`, or `null` to run continuously:

```bash
curl -X POST http://localhost:1610/jobs \
  -H 'Content-Type: application/json' \
  -d "$(jq -n --arg hash "$MANIFEST_HASH" '{
    kind: "adbc",
    dataset_namespace: "acme",
    dataset_name: "eth_mainnet",
    manifest_hash: $hash,
    table_names: ["blocks", "logs"],
    end_block: null,
    adbc_target: {provider_name: "warehouse", reorg_policy: "append"}
  }')"
```

| Option                                          | Type                                  | Default                                               | Description                                                                                                                                                                                                                                                                                     |
| ----------------------------------------------- | ------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--provider <PROVIDER>`                         | string                                | required                                              | The ADBC provider to export through, like `--provider warehouse`. Its kind (`adbc-postgres` or `adbc-snowflake`) selects the database.                                                                                                                                                          |
| `--provider-version <PROVIDER_VERSION>`         | version requirement                   | any version                                           | A requirement the provider's version must meet, like `--provider-version '^1'`. A provider's version goes up each time it's registered with a changed configuration. Creation fails with `ADBC_VERSION_REQ_UNSATISFIED` when it doesn't meet it.                                                |
| `--table <TABLE>`                               | string, repeatable                    | every table                                           | A table to export, like `--table logs`. Repeat it for several.                                                                                                                                                                                                                                  |
| `--start-block <START_BLOCK>`                   | integer                               | the source table's first block with rows              | The block a table's export starts at, like `--start-block 18000000`. It only applies to tables the destination has no export progress for: the others resume where they stopped.                                                                                                                |
| `--end-block <END_BLOCK>`                       | integer                               | none: the export runs continuously                    | The block the export stops at, like `--end-block 20000000`. The job completes once every table reaches it. Creation fails with `ADBC_START_AFTER_END_BLOCK` when it's below `--start-block`.                                                                                                    |
| `--reorg-policy <REORG_POLICY>`                 | `append`, `replace`, or `soft-delete` | `append`                                              | What happens in the destination to rows a [reorg](/get-started/concepts/reorgs) replaced, like `--reorg-policy replace`. See below.                                                                                                                                                             |
| `--state-heartbeat-secs <STATE_HEARTBEAT_SECS>` | integer                               | the worker's progress interval, 10 seconds by default | How long the export may go without recording its progress in the destination while it finds no rows to export, like `--state-heartbeat-secs 60`. `0` records it after every batch. See [`worker.events.progress_interval`](/running-amp/configuration-reference#workereventsprogress_interval). |
| `--worker-id <WORKER_ID>`                       | worker ID or glob pattern             | the controller's default worker, or any active worker | The worker to run the job on, as for `gc` jobs.                                                                                                                                                                                                                                                 |
| `--retry-strategy <RETRY_STRATEGY>`             | `none`, `bounded`, `unless_stopped`   | `unless_stopped`                                      | How the job is retried when it fails. See [Retries](/managing-amp/jobs/retries).                                                                                                                                                                                                                |

An `adbc` job has no trigger: it runs until it reaches its end block, or until you stop it.

The reorg policy decides what happens to exported rows when a reorg replaces the blocks they came from:

* `append`: they stay, and the new rows are added beside them.
* `replace`: they're deleted.
* `soft-delete`: they stay, marked with the reorg that replaced them.

`replace` and `soft-delete` only work on PostgreSQL, and only on tables whose data comes from a single network. See
[Reorg policies](/managing-amp/pipelines#reorg-policies).

## List jobs

`ampctl job list` shows the jobs that haven't finished, newest first.

:::code-group
```bash [ampctl]
ampctl job list
```

```bash [Admin API]
curl http://localhost:1610/jobs
```
:::

`ampctl` shows each job's ID, status, and worker, its idempotency key and, for materialization jobs, what it works on,
with the manifest hash shortened to 7 characters:

```
Jobs:
  8 - RUNNING (worker)
    Idempotency Key: 9d172701920e8de1e6d26d9fa7edc6d87742d624cd5770f792f34bc25c1d901a
    Descriptor: materialize-raw acme/eth_mainnet@9ce400b
  7 - RUNNING (controller)
    Idempotency Key: 7c1979eb622264f65e9380f31356352660a8a2b1334e6a9bd8ff71c7c0028e3d

Next cursor: 7
```

| Option                    | Type                                   | Default  | Description                                                                                                                                                                               |
| ------------------------- | -------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-s`, `--status <STATUS>` | `active`, `all`, or a list of statuses | `active` | Which jobs to list: those that haven't finished, all of them, or those with the given statuses, comma-separated, like `--status error,fatal`. A status that doesn't exist matches no job. |
| `-l`, `--limit <LIMIT>`   | integer, 1 to 1000                     | `50`     | How many jobs a page holds, like `--limit 100`.                                                                                                                                           |
| `-a`, `--after <AFTER>`   | integer                                | none     | Start after this job, like `--after 7`: lists the jobs older than it.                                                                                                                     |

Any page with jobs ends with a cursor, the last job's ID. Pass it to `--after` for the next page. The list is over when
a page comes back empty. With the Admin API, the parameters are `status`, `limit`, and `last_job_id`, and the response
is `{"jobs": [...], "next_cursor": 7}`.

## Inspect a job

:::code-group
```bash [ampctl]
ampctl job inspect 1
```

```bash [Admin API]
curl http://localhost:1610/jobs/1
```
:::

`ampctl` shows the job's ID, idempotency key, status, worker, when it was created and last changed status, and its
**descriptor**, the options it was scheduled with:

```
Job ID: 1
Idempotency Key: 1f88bf7bcbcf5172888c5833dec8d43bb6768c5fed1985c1e79fda5c9cc91410
Status: COMPLETED
Worker: controller
Created: 2026-10-30T14:02:11.418223+00:00
Updated: 2026-10-30T14:02:11.536084+00:00

Descriptor:
{
  "kind": "composite",
  "name": "acme/eth_mainnet@b9e2ebd92c0752ba8a61a233f9cc5d3f4f8b601b1b789a882a5dd8ece9bca56b",
  "trigger": {
    "type": "one_shot"
  },
  "retry_strategy": {
    "backoff": {
      "kind": "exponential",
      "multiplier": 2.0,
      "max_delay_secs": 3600,
      "base_delay_secs": 1
    },
    "strategy": "unless_stopped"
  }
}
```

`ampctl job get` does the same. To find a deployment's children, list its jobs with
[`ampctl dataset events`](/managing-amp/datasets#see-a-deployments-events).

## See a job's events

A job's events record each status it went through, in order. To see what a job has written so far, and the error that
stopped it, if any, see [Job Status](/managing-amp/jobs/status).

:::code-group
```bash [ampctl]
ampctl job events 2
```

```bash [Admin API]
curl http://localhost:1610/jobs/2/events
```
:::

```
Job ID: 2
Idempotency Key: 9d172701920e8de1e6d26d9fa7edc6d87742d624cd5770f792f34bc25c1d901a

Events:
  [42] 2026-10-30T14:02:11.418223+00:00 - SCHEDULED (node: worker)
  [44] 2026-10-30T14:02:12.006517+00:00 - RUNNING (node: worker)
```

Each line shows the event's ID, its time, the status the job entered, and the node that recorded it. A `SCHEDULED`
event also holds the options the job was scheduled with, and an `ERROR` or `FATAL` event the error. To see them, pass
the event's ID:

:::code-group
```bash [ampctl]
ampctl job events 2 --event-id 42
```

```bash [Admin API]
curl http://localhost:1610/jobs/2/events/42
```
:::

| Option                  | Type    | Default | Description                                                    |
| ----------------------- | ------- | ------- | -------------------------------------------------------------- |
| `--event-id <EVENT_ID>` | integer | none    | Show this event alone, with its details, like `--event-id 42`. |

## Stop a job

Stopping asks a job's worker to stop it. The command returns as soon as the request is recorded; the job then goes
through `STOP_REQUESTED` and `STOPPING` to `STOPPED`, once its worker has stopped it.

:::code-group
```bash [ampctl]
ampctl job stop 10
```

```bash [Admin API]
curl -X PUT http://localhost:1610/jobs/10/stop
```
:::

You can stop a job that's scheduled, running, or waiting to be retried, and a job that runs on a
[trigger](/managing-amp/jobs/triggers), between runs: that ends its schedule. Stopping a job that has already finished
succeeds without changing anything. The Admin API answers `200 OK`, or `404` with `JOB_NOT_FOUND`.

The job's worker does the stopping, so a job whose worker is down stays in `STOP_REQUESTED` until that worker starts
again. A stopped job is never restarted; to run it again, create it again.

## Remove jobs

Removing a job deletes it and its events. The data it wrote stays. Only finished jobs can be removed: those that are
`COMPLETED`, `STOPPED`, or `FATAL`.

:::code-group
```bash [ampctl]
ampctl job rm 10
```

```bash [Admin API]
curl -X DELETE http://localhost:1610/jobs/10
```
:::

`ampctl job remove` does the same. Removing a job that doesn't exist succeeds. Removing one that hasn't finished fails
with `409 Conflict` and `JOB_CONFLICT`: stop it first.

To remove jobs in bulk by status, **prune** them:

:::code-group
```bash [ampctl]
ampctl job prune --status completed
```

```bash [Admin API]
curl -X DELETE 'http://localhost:1610/jobs?status=completed'
```
:::

| Option                    | Type                                           | Default  | Description                                               |
| ------------------------- | ---------------------------------------------- | -------- | --------------------------------------------------------- |
| `-s`, `--status <STATUS>` | `terminal`, `completed`, `stopped`, or `error` | required | Which jobs to remove, like `--status stopped`. See below. |

At v0.0.42, always pass `--status`. `ampctl job prune --help` says `terminal` is the default, but without the flag the
command fails with `INVALID_QUERY_PARAM`, because the Admin API requires `status`.

* `terminal` removes the jobs that are `COMPLETED`, `STOPPED`, or `FATAL`.
* `completed` and `stopped` remove the jobs with that status.
* `error` removes the jobs that are `FATAL`, and those that are `ERROR`, which would otherwise be
  [retried](/managing-amp/jobs/retries).

:::warning
A job that runs on a trigger is `COMPLETED` or `FATAL` between runs, so pruning removes it, and its schedule with it.
That includes the hourly garbage collection job of every deployment. Prefer `--status stopped`, or remove jobs one by
one.
:::

The Admin API's `status` parameter is required. It answers `204 No Content` whether or not any job matched.
