> **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.

# ampctl job

`ampctl job` manages [jobs](/managing-amp/jobs): the work Amp's [workers](/managing-amp/workers) do, like extracting a
chain into a raw dataset, materializing a derived dataset, deleting files Amp no longer needs, or exporting a dataset
into another database. [`ampctl dataset deploy`](/clis/ampctl/dataset) creates most jobs. `ampctl job` lists them,
shows their progress and history, stops them, deletes the finished ones, and creates the two kinds you start yourself:
garbage collection and ADBC exports.

How a job moves from one status to the next is explained in [Job Lifecycle](/managing-amp/jobs/lifecycle), and how
failed jobs are retried and periodic jobs rerun in [Retries](/managing-amp/jobs/retries) and
[Triggers](/managing-amp/jobs/triggers). This page is about the commands.

## Prerequisites

* A running Amp, like the one the [Quickstart](/get-started/quickstart) starts with
  [`ampd solo`](/running-amp/solo-mode). The examples use its `acme/eth_mainnet@1.0.0` dataset, and
  [`acme/eth_transfers@1.0.0`](/datasets/derived-datasets#the-erc-20-transfers-dataset), a derived dataset you
  create on top of it.
* `ampctl`, [installed](/clis/ampctl#install) and [connected to it](/clis/ampctl#connect-to-amp).
* [`jq`](https://jqlang.org), for the examples that read JSON output.

Every command also takes the [global flags](/clis/ampctl#global-flags): `--admin-url`, `--auth-token`, and `--json`,
which prints the result as [JSON](/clis/ampctl#json-output). `ampctl jobs` is the same command as `ampctl job`.

Commands that fail exit with code 1, or 2 for arguments `ampctl` can't parse ([exit codes](/clis/ampctl#exit-codes)).
When the Admin API refuses a request, `ampctl` also [logs](/clis/ampctl#logging) a line before its error, like
`2026-11-02T17:48:19.679690Z ERROR run{…}: ampctl::cmd::job::stop: Failed to stop job …`, which the examples leave out.

## Jobs, IDs, and statuses

Each job has a numeric ID, a positive integer, which `inspect`, `status`, `events`, `stop`, and `rm` take. A job's
status is one of `SCHEDULED`, `RUNNING`, `COMPLETED`, `ERROR`, `CANCELLED`, `FATAL`, `STOP_REQUESTED`, `STOPPING`, and
`STOPPED`, which [Job Status](/managing-amp/jobs/status) describes. Three of them are *terminal*: `COMPLETED`,
`STOPPED`, and `FATAL`. A terminal job does nothing more, unless its trigger runs it again. `ERROR` isn't terminal: the
job waits, then retries.

Deploying a dataset creates three jobs. The deployment itself is a *composite* job, run by the controller rather than a
worker, whose two children do the work: one extracts or materializes the dataset, and the other collects its garbage
every hour.

Each job also has an idempotency key, which says what the job is for, like garbage collection of one dataset version.
Amp keeps one job per key, so creating a job that already exists reuses it (see
[`ampctl job create`](/clis/ampctl/job#ampctl-job-create)).

## `ampctl job list`

Lists jobs, newest first, a page at a time.

```
ampctl job list [OPTIONS]
```

| Flag                      | Type               | Default  | Description                                                                                                                                            |
| ------------------------- | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `-s`, `--status <STATUS>` | string             | `active` | Which jobs to list: `active`, the ones that aren't terminal; `all`; or statuses separated by commas, like `--status error,fatal`. Case doesn't matter. |
| `-l`, `--limit <LIMIT>`   | integer, 1 to 1000 | `50`     | How many jobs a page holds, like `--limit 100`.                                                                                                        |
| `-a`, `--after <AFTER>`   | job ID             | none     | Lists the jobs older than this one, like `--after 4`. Pass the previous page's `Next cursor`.                                                          |

`ampctl job ls` is the same command.

Once these datasets' jobs are done, none of them is active, so a plain `list` finds none:

:::terminal
```bash
ampctl job list
```

```
No jobs found
```
:::

With `--status all`, it lists finished jobs too:

:::terminal
```bash
ampctl job list --status all --limit 3
```

```
Jobs:
  6 - COMPLETED (worker)
    Idempotency Key: 0169ee16d11f6f7889f3517a87285cde466810c1cb5cf0528fe24b10198e1218
  5 - COMPLETED (worker)
    Idempotency Key: dfdb43dc4f5ab3a20163a1e8bc0365a1b782453ab2d638597ead59eea4d2286a
    Descriptor: materialize-derived acme/eth_transfers@fceb18f
  4 - COMPLETED (controller)
    Idempotency Key: 8d050abac8f7719b50a88479e8a05aa25d8a02bd9f6c644be0a4215cdeccad5d

Next cursor: 4
```
:::

Each job shows its ID, its status, and the node that runs it: a worker's node ID, or `controller` for a deployment's
composite job. The `Descriptor` line names the job's kind and its dataset, at the first 7 characters of its manifest
hash. Composite jobs, garbage collection jobs, and ADBC exports have no `Descriptor` line;
[`ampctl job inspect`](/clis/ampctl/job#ampctl-job-inspect) shows theirs.

`Next cursor` is the ID of the last job on the page. Pass it to `--after` for the next page. It's printed whenever the
page has jobs, even when no more are left, so you've read them all when a page prints `No jobs found`:

:::terminal
```bash
ampctl job list --status all --after 4
```

```
Jobs:
  3 - COMPLETED (worker)
    Idempotency Key: 2417400af0fd460657deccf08b59ec3184be13d24facd97d3786c64de3f991e7
  2 - COMPLETED (worker)
    Idempotency Key: 23f6d8595cb00fd128311d930159118ec3f8c3a1e1f075754ddcee4ae8776889
    Descriptor: materialize-raw acme/eth_mainnet@b9e2ebd
  1 - COMPLETED (controller)
    Idempotency Key: 1f88bf7bcbcf5172888c5833dec8d43bb6768c5fed1985c1e79fda5c9cc91410

Next cursor: 1
```
:::

A [periodic job](/managing-amp/jobs/triggers), one with an `interval` or `cron` trigger, is `COMPLETED` between runs, so
a plain `list` leaves it out. That includes each deployment's garbage collection job, which can look missing. While
`acme/eth_mainnet@1.0.0` is deployed without an end block, `list` shows two of its three jobs, and not job 3:

:::terminal
```bash
ampctl job list
```

```
Jobs:
  2 - RUNNING (worker)
    Idempotency Key: 23f6d8595cb00fd128311d930159118ec3f8c3a1e1f075754ddcee4ae8776889
    Descriptor: materialize-raw acme/eth_mainnet@b9e2ebd
  1 - RUNNING (controller)
    Idempotency Key: 1f88bf7bcbcf5172888c5833dec8d43bb6768c5fed1985c1e79fda5c9cc91410

Next cursor: 1
```
:::

To find periodic jobs, go through every page as JSON, and keep the jobs whose trigger isn't `one_shot`. This loop passes
each page's `next_cursor` to `--after`, and stops at the first page with no jobs, which has no `next_cursor`:

:::terminal
```bash
cursor=
while
  page=$(ampctl job list --status all ${cursor:+--after=$cursor} --json)
  cursor=$(jq '.next_cursor // empty' <<< "$page")
  [ -n "$cursor" ]
do
  jq -r '.jobs[] | select(.descriptor.trigger.type != "one_shot") | "\(.id) \(.status) \(.descriptor.kind) \(.descriptor.dataset_namespace)/\(.descriptor.dataset_name)"' <<< "$page"
done
```

```
6 COMPLETED gc acme/eth_transfers
3 COMPLETED gc acme/eth_mainnet
```
:::

To find one deployment's garbage collection job, you don't need the list. Its
[status](/managing-amp/dataset-status) has the job's ID:

:::terminal
```bash
ampctl dataset status acme/eth_mainnet@1.0.0 --json | jq .status.garbage_collection.job_id
```

```
3
```
:::

With `--json`, a page is `{"jobs": […], "next_cursor": 1}`, or `{"jobs": []}` once none are left. Each job has its `id`,
`idempotency_key`, `created_at`, `updated_at`, `node_id`, `status`, and `descriptor`, the options it was created with,
as [`inspect`](/clis/ampctl/job#ampctl-job-inspect) shows them:

:::terminal
```bash
ampctl job list --status all --limit 2 --json | jq -r '.jobs[] | "\(.id) \(.status) \(.descriptor.kind) \(.descriptor.dataset_namespace)/\(.descriptor.dataset_name)"'
```

```
6 COMPLETED gc acme/eth_transfers
5 COMPLETED materialize-derived acme/eth_transfers
```
:::

### Errors

* `LIMIT_INVALID: limit must be greater than 0` or `LIMIT_TOO_LARGE: limit 2000 exceeds maximum allowed limit of 1000`:
  `--limit` is outside 1 to 1000.
* `error: invalid value 'abc' for '--after <AFTER>': Invalid number format`: `--after` takes a job ID.

A status that doesn't exist, like `--status done`, isn't an error: no job has it, so the command prints
`No jobs found`.

## `ampctl job inspect`

Shows a job: its status, the node that runs it, when it was created and last changed, and its descriptor.

```
ampctl job inspect [OPTIONS] ID
```

| Argument | Type   | Default  | Description                |
| -------- | ------ | -------- | -------------------------- |
| `ID`     | job ID | required | The job to show, like `4`. |

`ampctl job get` is the same command.

:::terminal
```bash
ampctl job inspect 4
```

```
Job ID: 4
Idempotency Key: 8d050abac8f7719b50a88479e8a05aa25d8a02bd9f6c644be0a4215cdeccad5d
Status: COMPLETED
Worker: controller
Created: 2026-11-02T17:46:41.412936+00:00
Updated: 2026-11-02T17:46:42.454644+00:00

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

| Field             | JSON field        | Description                                                                                                                       |
| ----------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `Idempotency Key` | `idempotency_key` | What the job is for, as a hash. Amp keeps one job per key.                                                                        |
| `Status`          | `status`          | The job's [status](/managing-amp/jobs/status).                                                                                    |
| `Worker`          | `node_id`         | The worker the job is assigned to, by node ID, or `controller` for a composite job.                                               |
| `Created`         | `created_at`      | When the job was first created.                                                                                                   |
| `Updated`         | `updated_at`      | When its status last changed.                                                                                                     |
| `Descriptor`      | `descriptor`      | The options the job was last scheduled with: its `kind`, its dataset, its `trigger` and `retry_strategy`, and its kind's options. |

A composite job doesn't list its children. A deployment's children are the jobs created right after it, which
[`ampctl dataset events`](/clis/ampctl/dataset#ampctl-dataset-events) lists with their IDs and kinds.

### Errors

* `✗ job not found: 999`: no job has that ID, or it was removed. The command exits with code 1.
* `error: invalid value 'abc' for '<ID>': Invalid number format` or
  `error: invalid value '0' for '<ID>': JobId must be positive, got: 0`: the ID isn't a positive integer.

## `ampctl job status`

Shows how far a job has gotten: for each table it writes, the blocks it holds and how fresh they are; and when the job
has failed, why.

```
ampctl job status [OPTIONS] ID
```

| Argument | Type   | Default  | Description                |
| -------- | ------ | -------- | -------------------------- |
| `ID`     | job ID | required | The job to show, like `5`. |

`ampctl job progress` is the same command.

:::terminal
```bash
ampctl job status 5
```

```
Job ID: 5
Idempotency Key: dfdb43dc4f5ab3a20163a1e8bc0365a1b782453ab2d638597ead59eea4d2286a
Status: COMPLETED

Tables:
  transfers:
    Block range: {mainnet: [24000000-24000099]}
    Latest segment delay: 28056058s (46 weeks)
    Blocks extracted: 100
    Files: 1
    Size: 518.91 KiB
```
:::

| Field                  | JSON field                     | Description                                                                                                                                                       |
| ---------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Block range`          | `synced_ranges`                | The blocks the table holds without gaps, per network, from the start of its data. `(no data)` before the first file is written.                                   |
| `Latest segment delay` | `latest_segment_delay_seconds` | How old the table's newest block is. The Quickstart's blocks are from December 2025, so it's large here; a table that keeps up with a chain stays within seconds. |
| `Blocks extracted`     | `extracted_block_count`        | How many blocks the table holds, including those after a gap.                                                                                                     |
| `Files`                | `files_count`                  | How many files the table's data is in.                                                                                                                            |
| `Size`                 | `total_size_bytes`             | Their total size.                                                                                                                                                 |

A garbage collection job writes no tables, so `status` shows only its ID, idempotency key, and status. Its backlog, the
files still waiting to be deleted, is in its deployment's status: see
[`ampctl dataset status`](/clis/ampctl/dataset#ampctl-dataset-status). A composite job writes no tables itself either,
and an ADBC export writes into its destination database rather than into Amp, so both print `No tables written yet.`
Check a composite job's children, and an export's destination. A job that did write tables prints it too once
[another dataset with the same manifest](/managing-amp/datasets#register-a-manifest-under-a-second-name) is deployed.

When the job is in `ERROR` or `FATAL`, `status` also shows the error of its last attempt. Here, the provider doesn't
serve receipts for these blocks, so the extraction fails and is retried:

:::terminal
```bash
ampctl job status 2
```

```
Job ID: 2
Idempotency Key: 23f6d8595cb00fd128311d930159118ec3f8c3a1e1f075754ddcee4ae8776889
Status: ERROR

✖ Materialization failed

  Error Code        : MATERIALIZATION
  Dataset           : acme/eth_mainnet
  Attempt Index     : 2
  Manifest Hash     : b9e2ebd92c0752ba8a61a233f9cc5d3f4f8b601b1b789a882a5dd8ece9bca56b
  Block Range End   : 24000099
  Block Range Start : 24000000

  Stack Trace:
    • Failed to read stream
    • failed to fetch receipts
    • no receipts returned for block 24000000

Tables:
  blocks:
    Block range: (no data)
    Latest segment delay: (unknown)
    Blocks extracted: 0
    Files: 0
    Size: 0 B
…
```
:::

With `--json`, the tables are an object keyed by name, and each block range has its first and last block, its network,
and the hashes of its last block and of the block before its first:

:::terminal
```bash
ampctl job progress 5 --json
```

```json
{
  "job_id": 5,
  "idempotency_key": "dfdb43dc4f5ab3a20163a1e8bc0365a1b782453ab2d638597ead59eea4d2286a",
  "job_status": "COMPLETED",
  "tables": {
    "transfers": {
      "synced_ranges": [
        {
          "numbers": { "start": 24000000, "end": 24000099 },
          "network": "mainnet",
          "hash": "0x6ed4b3511195052eea47925fe5035f5e3fc528440eed62649ba82058a1b70e40",
          "prev_hash": "0xe8f20171e085c248ac4d60b65ddeb9c1bd679149cc1be9925ad58799dc380b25",
          "timestamp": 1765585559
        }
      ],
      "extracted_block_count": 100,
      "latest_segment_delay_seconds": 28056058,
      "files_count": 1,
      "total_size_bytes": 531363
    }
  }
}
```
:::

`tables` is `null` for a garbage collection job, and empty for a composite job or an export. `detail`, the error,
appears only when the job has failed. `ampctl job status` fails like [`inspect`](/clis/ampctl/job#ampctl-job-inspect) for
an ID that isn't a job's.

## `ampctl job events`

Lists a job's events: each time it entered a status, across all its attempts. With `--event-id`, it shows one event in
full.

```
ampctl job events [OPTIONS] JOB_ID
```

| Argument / flag         | Type     | Default  | Description                                                    |
| ----------------------- | -------- | -------- | -------------------------------------------------------------- |
| `JOB_ID`                | job ID   | required | The job, like `5`.                                             |
| `--event-id <EVENT_ID>` | event ID | none     | Shows only this event, with its details, like `--event-id 11`. |

:::terminal
```bash
ampctl job events 5
```

```
Job ID: 5
Idempotency Key: dfdb43dc4f5ab3a20163a1e8bc0365a1b782453ab2d638597ead59eea4d2286a

Events:
  [11] 2026-11-02T17:46:41.412936+00:00 - SCHEDULED (node: worker)
  [14] 2026-11-02T17:46:41.430753+00:00 - RUNNING (node: worker)
  [17] 2026-11-02T17:46:41.594404+00:00 - COMPLETED (node: worker)
```
:::

Each event has its ID, in brackets, when it happened, the status the job entered, and the node it ran on. Events are
numbered across all jobs, so a job's IDs have gaps. A job that was retried, stopped, or created again lists every
attempt, oldest first.

A `SCHEDULED` event records the descriptor the job was scheduled with, so you can tell what changed between attempts:

:::terminal
```bash
ampctl job events 5 --event-id 11
```

```
Event ID:        11
Job ID:          5
Idempotency Key: dfdb43dc4f5ab3a20163a1e8bc0365a1b782453ab2d638597ead59eea4d2286a
Created At:      2026-11-02T17:46:41.412936+00:00
Node ID:         worker
Event Type:      SCHEDULED

Descriptor
──────────
Kind              : materialize-derived
Trigger           : {"type":"one_shot"}
End Block         : 24000099
Composite Id      : 4
Dataset Name      : eth_transfers
Manifest Hash     : fceb18fb7f3d2be31ea324a0d8760ff3e3440c212a7bea0a0b65016ff613af26
Retry Strategy    : {"backoff":{"kind":"exponential","multiplier":2.0,"max_delay_secs":3600,"base_delay_secs":1},"strategy":"unless_stopped"}
Dataset Namespace : acme
```
:::

An `ERROR` or `FATAL` event records that attempt's error, like `status` shows it. Other events have no details.

With `--json`, the list is `{"job_id", "idempotency_key", "events": […]}`, each event with its `id`, `created_at`,
`node_id`, and `event_type`. A single event also has its `detail`:

:::terminal
```bash
ampctl job events 2 --event-id 8 --json
```

```json
{
  "id": 8,
  "job_id": 2,
  "idempotency_key": "23f6d8595cb00fd128311d930159118ec3f8c3a1e1f075754ddcee4ae8776889",
  "created_at": "2026-11-02T17:44:04.283513+00:00",
  "node_id": "worker",
  "event_type": "ERROR",
  "detail": {
    "error_code": "MATERIALIZATION",
    "error_context": {
      "dataset": "acme/eth_mainnet",
      "stack_trace": ["Failed to read stream", "failed to fetch receipts", "no receipts returned for block 24000000"],
      "attempt_index": 1,
      "manifest_hash": "b9e2ebd92c0752ba8a61a233f9cc5d3f4f8b601b1b789a882a5dd8ece9bca56b",
      "block_range_end": 24000099,
      "block_range_start": 24000000
    },
    "error_message": "Materialization failed"
  }
}
```
:::

### Errors

* `✗ job not found: 999`: no job has that ID.
* `✗ event 9999 not found for job 5`: the job has no event with that ID. Events belong to one job each.

Both exit with code 1.

## `ampctl job create`

Creates a job of one of the two kinds you start yourself:

* [`gc`](/clis/ampctl/job#ampctl-job-create-gc) deletes a dataset version's files that are waiting to be deleted.
* [`adbc`](/clis/ampctl/job#ampctl-job-create-adbc) exports a dataset version into a PostgreSQL or Snowflake database.

Amp creates the other kinds when you [deploy a dataset](/clis/ampctl/dataset#ampctl-dataset-deploy): the extraction and
materialization jobs.

Both take the dataset version as three arguments: its namespace, its name, and its manifest hash, which
[`ampctl dataset inspect`](/clis/ampctl/dataset) prints. The examples read it into `HASH`:

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

Creating a job whose [idempotency key](/clis/ampctl/job#jobs-ids-and-statuses) belongs to a job that exists depends on that job:

* **It's active, with the same options**: nothing changes, and `create` prints that job's ID.
* **It's active, with other options**: `create` fails with `ACTIVE_JOB_CONFLICT`. Stop it first.
* **It's terminal**: `create` schedules it again, with the same ID and your options. Its events keep the earlier
  attempts.

When it succeeds, `create` prints `✓ Job N created`. With `--json`, it prints the job's ID and what you asked for:
`job_id`, `kind`, the dataset, and the options you set.

### Errors

These apply to both kinds:

* `NO_WORKERS_AVAILABLE`: no worker is running. Start one, or check [`ampctl worker list`](/clis/ampctl/worker).
* `WORKER_NOT_AVAILABLE: failed to schedule job: specified worker 'worker-01' not found or inactive`: the worker
  `--worker-id` names hasn't sent a heartbeat recently.
* `SCHEDULER_ERROR: failed to schedule job: no active workers match pattern 'nobody-*'`: no active worker's node ID
  starts with the pattern's prefix.
* `ACTIVE_JOB_CONFLICT: failed to schedule job: an active job (id=9) already exists for this dataset with different options; stop it first`:
  see above. Stop job 9 with [`ampctl job stop`](/clis/ampctl/job#ampctl-job-stop), or create the job with the options it has.
* `error: invalid value 'deadbeef' for '<MANIFEST_HASH>': invalid hash length: expected 64, got 8`: the manifest
  hash is 64 hexadecimal characters.

## `ampctl job create gc`

Creates a [garbage collection](/running-amp/storage/garbage-collection) job for a dataset version. It deletes the files
that compaction and [`ampctl table prune`](/clis/ampctl/table) scheduled for deletion once their grace period is over.

```
ampctl job create gc [OPTIONS] DATASET_NAMESPACE DATASET_NAME MANIFEST_HASH
```

| Argument / flag                 | Type               | Default                 | Description                                                                                                                                                                                                                                                                      |
| ------------------------------- | ------------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DATASET_NAMESPACE`             | string             | required                | The dataset's namespace, like `acme`.                                                                                                                                                                                                                                            |
| `DATASET_NAME`                  | string             | required                | The dataset's name, like `eth_mainnet`.                                                                                                                                                                                                                                          |
| `MANIFEST_HASH`                 | 64 hex characters  | required                | The version's manifest hash, like `$HASH`.                                                                                                                                                                                                                                       |
| `--worker-id <WORKER_ID>`       | node ID or pattern | the controller's choice | The worker to run the job: a node ID, like `--worker-id worker-01`, or a prefix ending in `*`, like `--worker-id 'worker-*'`, for any active worker it matches. Without it, Amp uses the controller's default worker, if one is configured, or picks an active worker at random. |
| [Retry flags](/clis/ampctl/job#retry-flags)     |                    | Amp's default strategy  | How the job is retried when it fails.                                                                                                                                                                                                                                            |
| [Trigger flags](/clis/ampctl/job#trigger-flags) |                    | `--trigger one_shot`    | When the job runs again.                                                                                                                                                                                                                                                         |

Each deployed dataset version already has a garbage collection job, the deployment's `gc` child, which runs every hour.
Since a version has one garbage collection job, `create` reuses it, and replaces its trigger and retry strategy with
the ones you pass, or their defaults. Without trigger flags, that's `one_shot`, so pass
`--trigger interval --trigger-interval 3600` to collect now and keep collecting every hour:

:::terminal
```bash
ampctl job create gc acme eth_mainnet $HASH --trigger interval --trigger-interval 3600
```

```
✓ Job 3 created
```
:::

Job 3 is `acme/eth_mainnet`'s `gc` child, which runs again at once. It no longer belongs to the deployment then:
[`ampctl dataset stop`](/clis/ampctl/dataset#ampctl-dataset-stop) doesn't stop it anymore, so stop it with
[`ampctl job stop`](/clis/ampctl/job#ampctl-job-stop). For a version that was never deployed, `create` makes a new job.

:::terminal
```bash
ampctl job create gc acme eth_mainnet $HASH --trigger interval --trigger-interval 3600 --json
```

```json
{
  "job_id": 3,
  "kind": "gc",
  "dataset_namespace": "acme",
  "dataset_name": "eth_mainnet",
  "manifest_hash": "b9e2ebd92c0752ba8a61a233f9cc5d3f4f8b601b1b789a882a5dd8ece9bca56b",
  "trigger": { "type": "interval", "every_secs": 3600 }
}
```
:::

Amp doesn't check that the dataset exists. A garbage collection job for a version Amp doesn't have finds nothing to
delete, and completes.

## `ampctl job create adbc` (Unstable)

Creates an export job: it copies a dataset version's tables into a PostgreSQL or Snowflake database, through an
[ADBC provider](/managing-amp/pipelines/destinations) you've registered, and keeps copying new blocks until it reaches
`--end-block`, if you set one.

```
ampctl job create adbc [OPTIONS] --provider PROVIDER DATASET_NAMESPACE DATASET_NAME MANIFEST_HASH
```

| Argument / flag                                 | Type                                     | Default                        | Description                                                                                                                                                                                                                                 |
| ----------------------------------------------- | ---------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DATASET_NAMESPACE`                             | string                                   | required                       | The dataset's namespace, like `acme`.                                                                                                                                                                                                       |
| `DATASET_NAME`                                  | string                                   | required                       | The dataset's name, like `eth_transfers`.                                                                                                                                                                                                   |
| `MANIFEST_HASH`                                 | 64 hex characters                        | required                       | The version's manifest hash, like `$HASH`.                                                                                                                                                                                                  |
| `--provider <PROVIDER>`                         | string                                   | required                       | The ADBC provider to export through, by name, like `--provider warehouse`. Its kind, `adbc-postgres` or `adbc-snowflake`, picks the driver.                                                                                                 |
| `--provider-version <PROVIDER_VERSION>`         | [semver](https://semver.org) requirement | any                            | A version the provider must have, like `--provider-version '^1'`. A provider starts at `1.0.0`, and registering it again with a changed config gives it a new version.                                                                      |
| `--table <TABLE>`                               | string, repeatable                       | every table                    | A table to export, like `--table transfers`. Repeat it for several.                                                                                                                                                                         |
| `--start-block <START_BLOCK>`                   | integer                                  | the first block with data      | The block a table's export starts at, like `--start-block 24000050`. It applies only to tables the destination hasn't exported to yet: an export to a table that has data [resumes](/managing-amp/pipelines/resuming) after its last block. |
| `--end-block <END_BLOCK>`                       | integer                                  | none: the export doesn't stop  | The block the export stops at, like `--end-block 24000099`. The job is `COMPLETED` once every table reaches it.                                                                                                                             |
| `--reorg-policy <REORG_POLICY>`                 | `append`, `replace`, or `soft-delete`    | `append`                       | What happens to exported rows a [reorg](/get-started/concepts/reorgs) invalidates: `append` leaves them, `replace` deletes them, and `soft-delete` keeps them and marks them with the reorg that replaced them.                             |
| `--state-heartbeat-secs <STATE_HEARTBEAT_SECS>` | integer                                  | the worker's progress interval | How long the destination's record of the last exported block may lag behind, in seconds, when there's nothing new to export, like `--state-heartbeat-secs 60`. `0` updates it after every batch.                                            |
| `--worker-id <WORKER_ID>`                       | node ID or pattern                       | the controller's choice        | The worker to run the job, like [`gc`'s](/clis/ampctl/job#ampctl-job-create-gc).                                                                                                                                                                            |
| [Retry flags](/clis/ampctl/job#retry-flags)                     |                                          | Amp's default strategy         | How the job is retried when it fails. Exports take no trigger flags.                                                                                                                                                                        |

Before you create one, the export needs:

* The ADBC driver, which `ampd` loads: [`ampup adbc install postgresql`](/clis/ampup#ampup-adbc-install) installs the
  PostgreSQL driver beside it. [Install ADBC drivers](/running-amp/installation#install-adbc-drivers) covers
  Snowflake's.
* A registered ADBC provider, like `warehouse`, and the environment variables holding its connection details, set
  where `ampd` runs. [Destinations](/managing-amp/pipelines/destinations) explains both.
* In PostgreSQL, the schema the tables go into, `public` unless the provider sets `data_schema`, and the one where
  Amp records each table's last exported block, `amp` unless it sets `state_schema`. Amp creates the tables, not the
  schemas.

Amp keeps one export job per dataset version, provider, and tables, so an export with other options, like another end
block or reorg policy, reuses the job.

:::terminal
```bash
HASH=$(ampctl dataset inspect acme/eth_transfers@1.0.0 --json | jq -r .manifest_hash)
ampctl job create adbc acme eth_transfers $HASH --provider warehouse --end-block 24000099 --json
```

```json
{
  "job_id": 9,
  "kind": "adbc",
  "end_block": "24000099",
  "dataset_namespace": "acme",
  "dataset_name": "eth_transfers",
  "manifest_hash": "fceb18fb7f3d2be31ea324a0d8760ff3e3440c212a7bea0a0b65016ff613af26",
  "adbc_target": { "provider_name": "warehouse", "reorg_policy": "append" }
}
```
:::

With the `warehouse` provider from [Destinations](/managing-amp/pipelines/destinations), whose `data_schema` is `eth`,
the job writes the dataset's `transfers` table into `eth.transfers`, with the same columns, and completes once it has
exported block 24,000,099. Its [`status`](/clis/ampctl/job#ampctl-job-status) shows `No tables written yet.`, since it writes no
tables in Amp: query the destination to follow it.

### Errors

Creation fails when the provider can't be used:

* `ADBC_PROVIDER_NOT_FOUND: … ADBC target provider "warehouse" is not registered`: no ADBC provider has that name. A
  provider of another kind, like `mainnet_rpc`, fails the same way.
* `ADBC_VERSION_REQ_UNSATISFIED: … ADBC target provider "warehouse" is at version 1.0.0, which does not satisfy "^2"`:
  the provider's version doesn't meet `--provider-version`.
* `INVALID_BODY: … unexpected character 'a' while parsing major version number`: `--provider-version` isn't a semver
  requirement.
* `ADBC_START_AFTER_END_BLOCK: … start block (24000200) is greater than end block (24000099)`.

Other problems show once the job runs, as its error in [`ampctl job status`](/clis/ampctl/job#ampctl-job-status):

* `ADBC driver "amp-adbc-driver-postgresql.dylib" not found (searched: ["/Users/you/.amp/versions/v0.0.42", "/usr/local/lib/amp"])`:
  the driver isn't installed. `ampd` looks for it in the directory `AMP_ADBC_DRIVERS_DIR` names, then in its own
  directory, then in `/usr/local/lib/amp` on macOS or `/usr/lib/amp` on Linux. Snowflake's driver is
  `amp-adbc-driver-snowflake.dylib`, or `.so` on Linux. The job ends `FATAL`.
* `ADBC secret-field validation failed: field "password": environment variable AMP_ADBC_WAREHOUSE_PASSWORD is required by the provider config but is not set`:
  the environment `ampd` runs in doesn't have one of the provider's variables. The job ends `FATAL`; set it, restart
  `ampd`, and create the export again.
* `IO: [libpq] Failed to connect: connection to server at "localhost" (127.0.0.1), port 5433 failed: Connection refused`:
  the database can't be reached. The job is in `ERROR` and retries; check the provider's connection details.
* `IO: [Snowflake] 261004 (08004): failed to auth for unknown reason. HTTP: 404, URL: https://myorg-myaccount.snowflakecomputing.com:443/session/v1/login-request?…`:
  Snowflake has no account with that identifier. The job is in `ERROR` and retries; check the provider's
  `AMP_ADBC_<ALIAS>_ACCOUNT`.
* `ERROR:  schema "amp" does not exist` (or the data schema): create the schema in the database. The job ends `FATAL`;
  create it again once the schema exists.
* `IO: [Snowflake] 390201 (08004): The requested schema does not exist or not authorized.`, or for the state schema
  `IO: [Snowflake] 002003 (02000): SQL compilation error: Schema 'ANALYTICS.AMP' does not exist or not authorized.`:
  create the schema in the database, or give the provider's role access to it. The job is in `ERROR` and retries.
* `IO: [Snowflake] 002028 (42601): SQL compilation error: ambiguous column name 'NETWORK'`: every Snowflake export fails
  this way at Amp v0.0.42 (see [Destinations](/managing-amp/pipelines/destinations#register-a-snowflake-destination)).
  The job is in `ERROR` and retries.
* `reorg policy Replace requires an Append-shaped export state mode, got "upsert"`: `replace` and `soft-delete` need a
  provider whose `export_state_mode` is `append` or `upsert_retry`. The job ends `FATAL`.
* `✖ Table not found: approvals` (`TABLE_NOT_FOUND`): a `--table` isn't one of the dataset's tables. The job ends
  `FATAL`.

## Retry flags

`ampctl job create gc` and `ampctl job create adbc` take these flags, which set how a job that fails is retried.
[Retries](/managing-amp/jobs/retries) explains how Amp counts failures and waits between attempts.

Without `--retry-strategy`, a job gets Amp's default: `unless_stopped`, waiting 1 second after the first failure, then
twice as long after each one, up to an hour. The other flags only apply with `--retry-strategy`.

:::terminal
```bash
ampctl job create gc acme eth_mainnet $HASH --retry-strategy bounded --retry-max-attempts 5 --retry-window-secs 86400 --retry-backoff exponential_with_jitter --json
```

```json
{
  "job_id": 3,
  "kind": "gc",
  "dataset_namespace": "acme",
  "dataset_name": "eth_mainnet",
  "manifest_hash": "b9e2ebd92c0752ba8a61a233f9cc5d3f4f8b601b1b789a882a5dd8ece9bca56b",
  "retry_strategy": {
    "strategy": "bounded",
    "max_attempts": 5,
    "window_secs": 86400,
    "backoff": { "kind": "exponential_with_jitter", "base_delay_secs": 1, "multiplier": 2.0 }
  }
}
```
:::

This job fails for good, `FATAL`, once it has failed 5 times within a day, and waits 1, 2, 4, 8… seconds, plus a
random extra, between attempts.

### `--retry-strategy <RETRY_STRATEGY>`

Whether a failed job is retried.

* **Type**: `none`, `bounded`, or `unless_stopped`
* **Default**: Amp's default, `unless_stopped` with backoff capped at an hour

`none` never retries: the first failure is `FATAL`. `bounded` retries until `--retry-max-attempts` failures fall
within `--retry-window-secs`. `unless_stopped` retries until you stop the job.

### `--retry-max-attempts <RETRY_MAX_ATTEMPTS>`

How many failures within the window make a `bounded` job fail for good.

* **Type**: integer, at least 1
* **Default**: none; required with `--retry-strategy bounded`, and refused with the others

### `--retry-window-secs <RETRY_WINDOW_SECS>`

How far back `bounded` counts failures, in seconds. Older failures stop counting, so a job that fails rarely keeps
being retried.

* **Type**: integer, 1 to 604800 (7 days)
* **Default**: `3600`; refused with strategies other than `bounded`

### `--retry-backoff <RETRY_BACKOFF>`

How the wait between attempts grows.

* **Type**: `fixed`, `exponential`, or `exponential_with_jitter`
* **Default**: `exponential`

`fixed` waits `--retry-base-delay-secs` every time. `exponential` multiplies the wait by `--retry-multiplier` after each
failure. `exponential_with_jitter` adds a random extra, up to a quarter of the wait, so that jobs failing together don't
retry together.

### `--retry-base-delay-secs <RETRY_BASE_DELAY_SECS>`

The wait after the first failure, in seconds, or every wait with `fixed`.

* **Type**: integer, at least 1
* **Default**: `1`

### `--retry-multiplier <RETRY_MULTIPLIER>`

What the wait is multiplied by after each failure. `fixed` ignores it.

* **Type**: positive number
* **Default**: `2.0`

### `--retry-max-delay-secs <RETRY_MAX_DELAY_SECS>`

The longest wait, in seconds. `fixed` ignores it.

* **Type**: integer, at least `--retry-base-delay-secs`
* **Default**: none: with `--retry-strategy`, the wait has no limit unless you set one. Amp's default strategy stops
  at 3600 seconds.

### Errors

`ampctl` checks the flags before sending anything, and prints `✗ invalid retry strategy arguments` followed by the
reason:

* `--retry-max-attempts is required when --retry-strategy=bounded`
* `--retry-max-attempts is only valid when --retry-strategy=bounded`
* `--retry-window-secs is only valid when --retry-strategy=bounded`
* `backoff flags (--retry-backoff, --retry-base-delay-secs, --retry-multiplier, --retry-max-delay-secs) are only valid when --retry-strategy is bounded or unless_stopped`:
  a backoff flag came with `--retry-strategy none`, or a flag came without `--retry-strategy`. `--retry-max-attempts`
  without it reports this too.
* `max_attempts must be at least 1, got 0`, `window_secs must be between 1 and 604800, got 700000`,
  `base_delay_secs must be at least 1, got 0`, `multiplier must be a finite positive number, got 0`, or
  `max_delay_secs (10) must be >= base_delay_secs (60)`: a value is out of range.

## Trigger flags

`ampctl job create gc` takes these flags, which make a job run again on a schedule.
[Triggers](/managing-amp/jobs/triggers) explains how scheduled runs work. A scheduled run never overlaps the previous
one: if that one is still `SCHEDULED` or `RUNNING`, Amp skips the run.

:::terminal
```bash
ampctl job create gc acme eth_mainnet $HASH --trigger cron --trigger-cron '0 3 * * *' --trigger-time-zone=-04:00 --json
```

```json
{
  "job_id": 3,
  "kind": "gc",
  "dataset_namespace": "acme",
  "dataset_name": "eth_mainnet",
  "manifest_hash": "b9e2ebd92c0752ba8a61a233f9cc5d3f4f8b601b1b789a882a5dd8ece9bca56b",
  "trigger": { "type": "cron", "schedule": "0 3 * * *", "time_zone": "-04:00" }
}
```
:::

This job runs now, then every day at 3:00 at UTC−4.

### `--trigger <TRIGGER>`

When the job runs.

* **Type**: `one_shot`, `interval`, or `cron`
* **Default**: `one_shot`

`one_shot` runs once. `interval` runs every `--trigger-interval` seconds, and `cron` on the schedule `--trigger-cron`
sets.

### `--trigger-interval <TRIGGER_INTERVAL>`

How often an `interval` job runs, in seconds. Runs start on that cadence, however long each one takes.

* **Type**: integer, at least 1
* **Default**: none; required with `--trigger interval`, and refused with the others

### `--trigger-jitter <TRIGGER_JITTER>`

The longest random delay added to each `interval` run, so that jobs created together don't all run at once.

* **Type**: integer, 0 to 30
* **Default**: a tenth of the interval, up to 30 seconds. `0` adds none.

It never exceeds half the interval: with `--trigger-interval 40`, `--trigger-jitter 25` delays runs by up to 20
seconds.

### `--trigger-cron <TRIGGER_CRON>`

The schedule of a `cron` job, as a [cron expression](https://crontab.guru): minute, hour, day of the month, month, and
day of the week, like `'0 */6 * * *'` for every 6 hours. Quote it, so the shell doesn't expand `*`. An expression of 6
fields starts with seconds, and one of 7 also ends with a year. Shortcuts like `@daily` aren't accepted.

* **Type**: string
* **Default**: none; required with `--trigger cron`, and refused with the others

### `--trigger-time-zone <TRIGGER_TIME_ZONE>`

The time zone the cron schedule is read in, as an offset from UTC.

* **Type**: `+HH:MM` or `-HH:MM`, like `+05:30`. Time zone names, like `America/Toronto`, aren't accepted, so the
  schedule doesn't follow daylight saving time.
* **Default**: `+00:00`; refused with triggers other than `cron`

Write a negative offset with `=`, as in `--trigger-time-zone=-04:00`. With a space, `ampctl` reads `-04:00` as a flag:
`error: unexpected argument '-0' found`.

### Errors

`ampctl` prints `✗ invalid trigger arguments` followed by the reason:

* `--trigger-interval is required when --trigger=interval`, or `--trigger-cron is required when --trigger=cron`
* `--trigger-interval is only valid when --trigger=interval`, and likewise for `--trigger-jitter`, `--trigger-cron`,
  and `--trigger-time-zone`: a flag doesn't apply to the trigger, or came without `--trigger`.
* `every_secs must be at least 1, got 0`, or `jitter_secs must be at most 30, got 31`
* `invalid cron schedule: '0 3 * *'. Expected 5-field cron expression (e.g., '0 */6 * * *')`
* `invalid time_zone: 'America/Toronto'. Expected UTC offset format '+HH:MM' or '-HH:MM' (e.g., '+00:00', '+05:30')`

## `ampctl job stop`

Asks a job to stop. The job moves to `STOP_REQUESTED`, then `STOPPING` once its worker starts winding it down, and
`STOPPED` when it's done.

```
ampctl job stop [OPTIONS] ID
```

| Argument | Type   | Default  | Description                |
| -------- | ------ | -------- | -------------------------- |
| `ID`     | job ID | required | The job to stop, like `9`. |

It stops a job that's running, or in `ERROR` waiting to be retried, and a job with a trigger stops running again. To
stop a deployed dataset, use [`ampctl dataset stop`](/clis/ampctl/dataset#ampctl-dataset-stop).

Here, job 9 is the export [created above](/clis/ampctl/job#ampctl-job-create-adbc), created again without `--end-block`, so it keeps
running until you stop it:

```bash
ampctl job stop 9
```

The command returns once Amp has the request, not once the job has stopped. Check with
[`ampctl job events`](/clis/ampctl/job#ampctl-job-events):

:::terminal
```bash
ampctl job events 9
```

```
Job ID: 9
Idempotency Key: f6b6f5d6cd7feb497d9666c993aa9e1cf04b9f3f3084c258b6468c18cfd58598

Events:
  [25] 2026-11-02T17:47:08.971242+00:00 - SCHEDULED (node: worker)
  [26] 2026-11-02T17:47:08.979240+00:00 - RUNNING (node: worker)
  [27] 2026-11-02T17:47:14.509785+00:00 - COMPLETED (node: worker)
  [34] 2026-11-02T17:48:05.170853+00:00 - SCHEDULED (node: worker)
  [35] 2026-11-02T17:48:05.188913+00:00 - RUNNING (node: worker)
  [36] 2026-11-02T17:48:16.474982+00:00 - STOP_REQUESTED (node: worker)
  [37] 2026-11-02T17:48:16.493406+00:00 - STOPPING (node: worker)
  [38] 2026-11-02T17:48:16.506333+00:00 - STOPPED (node: worker)
```
:::

Stopping a job that's already terminal succeeds and changes nothing, except for a job with a trigger: it's `COMPLETED`
between runs, and stopping it moves it to `STOPPED`. With `--json`, `stop` prints `{"job_id":9}`.

A job that doesn't exist fails, with exit code 1.

## `ampctl job rm`

Deletes a terminal job: one that's `COMPLETED`, `STOPPED`, or `FATAL`.

```
ampctl job rm [OPTIONS] ID
```

| Argument | Type   | Default  | Description                  |
| -------- | ------ | -------- | ---------------------------- |
| `ID`     | job ID | required | The job to delete, like `9`. |

`ampctl job remove` is the same command. It doesn't ask first, and a deleted job can't be recovered.

```bash
ampctl job rm 9
```

With `--json`, it prints `{"job_id":9}`. Deleting a job that doesn't exist succeeds too, so you can run it again
safely.

A job that isn't terminal, including one in `ERROR`, can't be deleted. Stop it first:

```
✗ failed to remove job
  → job cannot be deleted from current state
    → JOB_CONFLICT: job '9' cannot be deleted from current state: RUNNING
```

:::warning
Deleting a deployment's composite job removes the deployment from
[`ampctl dataset status`](/clis/ampctl/dataset), which then fails with
`materialized dataset not found: acme/eth_transfers@1.0.0`. The dataset's data stays, and queries still read it.
:::

## `ampctl job prune`

Deletes every job with a status, in one go.

```
ampctl job prune [OPTIONS]
```

| Flag                      | Type                                           | Default                   | Description                                                                                                                                                            |
| ------------------------- | ---------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-s`, `--status <STATUS>` | `terminal`, `completed`, `stopped`, or `error` | `terminal`, but see below | Which jobs to delete: `terminal`, the `COMPLETED`, `STOPPED`, and `FATAL` ones; `completed`; `stopped`; or `error`, the `ERROR` and `FATAL` ones. Case doesn't matter. |

Amp refuses a `prune` without `--status`, though `--help` says it defaults to `terminal`:

```
✗ failed to prune jobs
  → invalid query parameter
    → INVALID_QUERY_PARAM: invalid query parameters: Failed to deserialize query string: missing field `status`: missing field `status`: missing field `status`
```

Pass `--status terminal` instead:

```bash
ampctl job prune --status terminal
```

With `--json`, it prints the filter, like `{"status_filter":"terminal"}`. It doesn't say how many jobs it deleted.

:::danger
`prune` doesn't ask first, and deletes every job with the status, whatever created it:

* Every finished deployment's jobs, so [`ampctl dataset status`](/clis/ampctl/dataset) no longer finds those
  deployments, and their datasets' garbage collection jobs, which stop collecting.
* With `--status error`, jobs in `ERROR` too, which were waiting to be retried.

To delete specific jobs, use [`ampctl job rm`](/clis/ampctl/job#ampctl-job-rm).
:::

Any other status fails before anything is sent.
