> ## Documentation Index
> Fetch the complete documentation index at: https://docs.repost.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Replay deliveries safely

> Re-deliver events through a forwarder behind a mandatory dry-run, with idempotency, rate control, and exit-code-driven waiting.

`replay` is the primary mutating command in the agentic surface. It re-delivers real events to a real target — a **deliberate** re-send, distinct from the forwarder's automatic retries. Every replayed delivery has the same downstream effect as the original webhook.

<Warning>
  A replay is **not** a simulation. Replayed deliveries hit the live target and can charge cards, send mail, or mutate state again. Always run `--dry-run` first, and never replay a `--query` or `--dlq` selection without reading the `would_replay` count.
</Warning>

## Before you replay

Replay follows the universal [safe-mutation pattern](/agents/safe-mutations): confirm `write` scope, resolve stable IDs (arrive here from [Diagnose webhooks](/agents/diagnose-webhooks)), preview, make it idempotent, gate bulk behind `--yes`, then wait. This page covers the replay-specific parts — choosing a selector, reading the dry-run, and branching on the wait exit code. `--forwarder` is always required; a replay targets exactly one forwarder.

## Always dry-run first

`--dry-run` resolves the selector against the index and returns a count without creating a job. It is the only safe way to learn a replay's blast radius.

```bash theme={null}
repost replay \
  --forwarder prod-api \
  --bucket stripe-prod \
  --query 'response_status:500 AND path:/stripe/*' \
  --since 30m \
  --dry-run \
  --json
```

```json theme={null}
{
  "schema": "repost.replay.create/v1",
  "data": {
    "status": "DRY_RUN",
    "dry_run": true,
    "would_replay": 42
  }
}
```

<Check>
  In a dry-run, `job_id` is absent and `would_replay` is authoritative. Gate on it: if the count is `0`, there is nothing to replay; if it is higher than the diagnosis predicted, the selector is too broad — refine it rather than continuing.
</Check>

## Choose a selector

A replay sources its events one of four ways. The selector you pass determines the `source_type` on the resulting job.

| To replay…          | Command shape                                | `source_type`  | `--yes` for execution      |
| ------------------- | -------------------------------------------- | -------------- | -------------------------- |
| Specific events     | `replay evt_1 evt_2 …`                       | `ID_LIST`      | Only when more than one ID |
| A search result set | `replay --query '…' --since 30m`             | `SEARCH_QUERY` | Required                   |
| A DLQ ULID range    | `replay --dlq --dlq-id-from … --dlq-id-to …` | `DLQ_RANGE`    | Required                   |
| Specific DLQ rows   | `replay --dlq --dlq-id … --dlq-id …`         | `DLQ_ID_LIST`  | Required                   |

<Note>
  A **single explicit event ID** is the only replay that executes without `--yes`. Everything else — multiple IDs, any `--query`, or any `--dlq` selection — is bulk: the CLI refuses it locally (`--yes is required for non-dry-run bulk replay`) and replays nothing unless you pass `--yes` or `--dry-run`. This makes "replay one known event" a one-liner while forcing a deliberate acknowledgement for fan-out.
</Note>

## Execute the replay

Once the dry-run count is right, drop `--dry-run` and confirm. Keep `--idempotency-key` and set `--rate` if the target is sensitive to burst.

```bash theme={null}
repost replay \
  --forwarder prod-api \
  --bucket stripe-prod \
  --query 'response_status:500 AND path:/stripe/*' \
  --since 30m \
  --rate 25 \
  --idempotency-key "incident-4821-stripe-500s" \
  --yes \
  --json
```

```json theme={null}
{
  "schema": "repost.replay.create/v1",
  "data": {
    "job_id": "rpl_01JZ9F2K7M",
    "status": "PENDING",
    "dry_run": false
  }
}
```

The job is accepted asynchronously and starts in `PENDING`. Capture `job_id` — every follow-up (`status`, `wait`, `pause`, `cancel`) takes it.

<Info>
  **`--rate`** is deliveries per second (default `50`, accepted range `5`–`500`); values outside it are rejected before any job is created. Lower it when the target rate-limits or when you are replaying into production during business hours. This is the per-job replay speed — distinct from a forwarder's persistent [`--rate-limit-per-second`](/agents/provision-targets#create-a-forwarder).
</Info>

### Idempotent retries

If a `replay` call times out or its connection drops, re-run the **identical** command. With the same `--idempotency-key`, the API re-attaches to the original job instead of creating a second one and sets `deduplicated`:

```json theme={null}
{
  "schema": "repost.replay.create/v1",
  "data": { "job_id": "rpl_01JZ9F2K7M", "status": "IN_PROGRESS", "deduplicated": true }
}
```

Treat `deduplicated: true` as success — the work is already running under `job_id`. Without a key, a blind retry double-delivers, so always set one for bulk replays.

## Wait for the result

`replay wait` polls until the job is **finalized**, then exits with a code that encodes the outcome. Branch on the exit code and the boolean `finalized` field — never on the status string.

```bash theme={null}
repost replay wait rpl_01JZ9F2K7M --max-wait 5m --json
```

```json theme={null}
{
  "schema": "repost.replay.status/v1",
  "data": {
    "job_id": "rpl_01JZ9F2K7M",
    "status": "COMPLETED",
    "finalized": true,
    "total": 42,
    "pending": 0,
    "succeeded": 41,
    "failed": 1,
    "created_at": "2026-06-19T09:02:11Z",
    "completed_at": "2026-06-19T09:03:44Z"
  }
}
```

<Check>
  `replay wait` exit codes:

  * **`0`** — finalized with zero failed deliveries. Done.
  * **`8`** (`partial_failure`) — finalized but at least one delivery failed (or the job itself is `FAILED`). Inspect `failed`, then `forwards chain` the events that did not land.
  * **`7`** (`timeout`) — `--max-wait` (default `5m`) elapsed before the job finalized. The job keeps running; poll `replay status` or wait again.
</Check>

## Job lifecycle

A replay job moves through these states. `wait` returns the moment `finalized` becomes `true` — at `COMPLETED`, `FAILED`, or `CANCELLED`.

```mermaid theme={null}
stateDiagram-v2
    [*] --> PENDING: replay accepted
    PENDING --> STARTING
    STARTING --> IN_PROGRESS
    IN_PROGRESS --> PAUSED: pause
    PAUSED --> IN_PROGRESS: resume
    IN_PROGRESS --> COMPLETED: all delivered
    IN_PROGRESS --> FAILED: errors exceed threshold
    IN_PROGRESS --> CANCELLED: cancel
    PAUSED --> CANCELLED: cancel
    COMPLETED --> [*]
    FAILED --> [*]
    CANCELLED --> [*]
```

## Manage a running job

While a job runs, inspect or steer it by `job_id`.

<CodeGroup>
  ```bash Check one job theme={null}
  repost replay status rpl_01JZ9F2K7M --json
  ```

  ```bash List recent jobs theme={null}
  repost replay list --bucket stripe-prod --forwarder prod-api --limit 20 --json
  ```

  ```bash Pause / resume / cancel theme={null}
  repost replay pause  rpl_01JZ9F2K7M --json
  repost replay resume rpl_01JZ9F2K7M --json
  repost replay cancel rpl_01JZ9F2K7M --json
  ```
</CodeGroup>

`status`, `list`, and the actions return `repost.replay.status/v1`, `repost.replay.list/v1`, and `repost.replay.action/v1` respectively. `list` is `created_at`-descending and paginates with `has_more` — raise `--limit` to see older jobs. Use `pause`/`resume` to throttle a replay mid-incident; [Guard a deployment](/agents/guard-deployments) shows the full pause-deploy-resume loop.

**Replay decision procedure**

1. Require `write` scope (`auth status` → `scopes` contains `write`), else stop with `forbidden_scope`.
2. Build a selector from concrete IDs / bounded query / DLQ rows. Never an open query.
3. `replay … --dry-run --json` → read `data.would_replay`. If `0`, stop. If unexpectedly large, refine.
4. Re-run without `--dry-run`; add `--yes` unless the selector is exactly one event ID. Always pass `--idempotency-key`.
5. On transport error, re-run the identical command; `data.deduplicated == true` means it is already running.
6. `replay wait <job_id> --json` → branch on exit: `0` done · `8` inspect `data.failed` then `forwards chain` · `7` not finalized, wait again.

## Continue

<Columns cols={3} className="gap-y-4">
  <Card title="Guard a deployment" icon="shield-check" href="/agents/guard-deployments" cta="Protect" arrow="true">
    Pause forwarding around a deploy, then resume and verify recovery.
  </Card>

  <Card title="Diagnose webhooks" icon="search-check" href="/agents/diagnose-webhooks" cta="Find the IDs" arrow="true">
    Produce the event IDs and DLQ rows a replay should target.
  </Card>

  <Card title="Replay concepts" icon="rotate-ccw" href="/history/replay" cta="How replay works" arrow="true">
    Windows, sourcing, and delivery semantics behind the command.
  </Card>
</Columns>
