# Manage runners with the CLI

> List, create, update, pause, resume, duplicate and delete runners, wait for them, and read their metrics from the terminal.

Source: https://managerunners.com/docs/cli/runners/

The examples use the active profile and organization. Add `--profile` or `--org` to choose others, and `--output json` for scripts. Commands that change runners need the scopes from [Authentication](https://managerunners.com/docs/cli/authentication/#scopes). `runner update` and `--wait` also need `runners:read`, which the CLI's login token always has.

## List and inspect

```sh
manage-runners runner list
manage-runners runner get --id 812999368881481087
manage-runners runner get --name build-01
```

Runner IDs are long numbers; treat them as strings. `runner get --name` needs an exact, unique name. The `state` field uses the API names: `CREATING`, `CONFIGURING`, `READY` (shown as **Active** in the dashboard), `PAUSED`, `UNKNOWN` and `INVALID_GITLAB_TOKEN`. See [Runner states](https://managerunners.com/docs/manual/concepts/#runner-states).

## Find a server type and location

```sh
manage-runners product list
manage-runners product get --id hz-cpx31
manage-runners product locations --id hz-cpx31
```

`product locations` shows where Hetzner offers the server type and Hetzner's monthly net price in each location. Use the location code, for example `fsn1`, with `runner create`.

To see the SSH keys of your Hetzner project, pass the Hetzner token on standard input or through an environment variable:

```sh
manage-runners provider hetzner ssh-keys --token-env HCLOUD_TOKEN
```

For an existing runner, `runner ssh-keys --id <id>` lists the keys available with the runner's stored Hetzner token.

## Create a runner

The CLI reads the GitLab runner token and the Hetzner token from environment variables you name, so they never appear on the command line:

```sh
export GLRT_TOKEN HCLOUD_TOKEN   # set them from your secret manager

manage-runners runner create \
  --name build-01 \
  --product-id hz-cpx31 \
  --location fsn1 \
  --executor docker \
  --concurrency 8 \
  --gitlab-host https://gitlab.com/ \
  --runner-token-env GLRT_TOKEN \
  --hetzner-token-env HCLOUD_TOKEN \
  --ssh-key-id 1234567 \
  --label role=ci-runner \
  --monitoring on \
  --wait
```

| Flag | Required | Description |
| --- | --- | --- |
| `--name` | yes | Letters, numbers, spaces and dashes |
| `--product-id` | yes | From `product list` |
| `--location` | yes | From `product locations` |
| `--executor` | yes | `docker`, `dind` or `shell` |
| `--concurrency` | yes | At least 1 |
| `--gitlab-host` | yes | Base URL of your GitLab instance |
| `--runner-token-env` | yes | Name of the variable holding the `glrt-` token |
| `--hetzner-token-env` | yes | Name of the variable holding the Hetzner token |
| `--ssh-key-id` | no | Hetzner SSH key ID, repeatable |
| `--label` | no | Hetzner label, repeatable |
| `--monitoring on\|off` | no | Needs an active plan to turn on |
| `--wait` | no | Wait until the runner reaches a final state |
| `--yes` | no | Confirm creating paid resources without a prompt |

`runner create` creates a paid Hetzner server, so it asks `create runner build-01 and potentially create paid resources? [y/N]`. Pass `--yes` in scripts. Without a terminal and without `--yes`, it fails.

Each create request carries an idempotency key. The CLI generates a new one per invocation; pass your own with `--idempotency-key` (16 to 128 letters, digits, `.`, `_` or `-`) to make a script's retry safe. For the same user, organization, route, key and request, the API returns the first result until 24 hours after the first attempt; after that, the same request creates another runner. A different request with the same key, or a retry while the first attempt has not completed, is rejected as a conflict. Check `runner list` before retrying an old create. See [Idempotency and unknown outcomes](https://managerunners.com/docs/cli/agents/#idempotency-and-unknown-outcomes).

## Wait for a runner

`--wait` on `create`, `update`, `pause`, `resume` and `duplicate` waits until the runner reaches `READY`, `PAUSED`, `UNKNOWN` or `INVALID_GITLAB_TOKEN`, for at most `--timeout` (default 10 minutes). Check the returned state: reaching `UNKNOWN` or `INVALID_GITLAB_TOKEN` ends the wait too.

To wait later or for a specific state:

```sh
manage-runners runner wait --id 812999368881481087 --state READY --timeout 15m
manage-runners runner watch --id 812999368881481087
```

If the wait times out, the command exits with code 7 (`TIMEOUT`). If it fails for another reason, for example a token without `runners:read`, it exits with that error. In both cases the change itself was accepted and is not undone; check the runner with `runner get`.

## Update a runner

```sh
manage-runners runner update --id 812999368881481087 --name build-02 --label role=ci-runner
manage-runners runner update --id 812999368881481087 --concurrency 4 --yes --wait
```

Flags you leave out keep their current values. `--label` and `--ssh-key-id` replace the whole list.

> `--product-id`, `--gitlab-host`, `--runner-token-env`, `--concurrency`, `--executor`, `--ssh-key-id` and `--monitoring` recreate an active runner's server and interrupt its jobs. The CLI asks for confirmation, or needs `--yes`. `--name`, `--label` and `--hetzner-token-env` alone never recreate the server. See [Changes that recreate the server](https://managerunners.com/docs/manual/concepts/#changes-that-recreate-the-server).

The location cannot be changed.

## Pause, resume and duplicate

```sh
manage-runners runner pause --id 812999368881481087 --yes
manage-runners runner resume --id 812999368881481087 --yes --wait
manage-runners runner duplicate --id 812999368881481087 --yes
```

`pause` deletes the server and keeps the IP addresses. `resume` and `duplicate` create servers and may cost money. All three ask for confirmation unless you pass `--yes`.

## Delete a runner

Deleting needs the `runners:delete` scope and the runner ID twice, as `--id` and `--confirm`:

```sh
manage-runners runner delete --id 812999368881481087 --confirm 812999368881481087 --yes
```

Interactively, the CLI asks you to confirm and to type the ID. Delete removes the server, the IP addresses and the runner's schedule. It does not remove the runner from GitLab.

If deletion cannot be confirmed with Hetzner, the runner is kept and the command fails. Run it again later.

## Forget an Unknown runner

`runner forget` only works on a runner in the `UNKNOWN` state. It removes the record from Manage Runners and leaves any server and IP addresses in Hetzner untouched. It needs the `runners:forget` scope and the same confirmation as `delete`:

```sh
manage-runners runner forget --id 812999368881481087 --confirm 812999368881481087 --yes
```

Never use `forget` instead of `delete`. Try [recovering the runner](https://managerunners.com/docs/manual/troubleshooting/#a-runner-is-unknown) first, and clean up Hetzner yourself afterwards.

## Read monitoring data

```sh
manage-runners runner metrics
manage-runners runner metrics --name build-01 --range 7d --output json
```

Without `--id` or `--name`, `runner metrics` covers every active runner with monitoring on. `--range` accepts `1h`, `24h` (default), `7d`, `30d` and `90d`. Human output shows the latest CPU, RAM and disk usage and a summary of the history; JSON includes the full history.

- `latest` is empty until the first sample arrives, and `stale` marks samples older than three minutes.
- A runner that is not monitored or not active fails with `NOT_MONITORED` (exit code 5).
- Without an active plan, the command fails with `SUBSCRIPTION_REQUIRED` (exit code 4).
- If the history is shorter than the range, a `RANGE_EXCEEDS_HISTORY` warning lists the available ranges.

## If a change has an unknown outcome

If the connection fails while a change is in progress, the CLI exits with code 10 (`UNKNOWN_OUTCOME`). The change may or may not have happened. Check the runner with `runner list` or `runner get` before you try again; never retry blindly.
