Skip to main content
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.
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.

Before you replay

Replay follows the universal safe-mutation pattern: confirm write scope, resolve stable IDs (arrive here from 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.
repost replay \
  --forwarder prod-api \
  --bucket stripe-prod \
  --query 'response_status:500 AND path:/stripe/*' \
  --since 30m \
  --dry-run \
  --json
{
  "schema": "repost.replay.create/v1",
  "data": {
    "status": "DRY_RUN",
    "dry_run": true,
    "would_replay": 42
  }
}
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.

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 shapesource_type--yes for execution
Specific eventsreplay evt_1 evt_2 …ID_LISTOnly when more than one ID
A search result setreplay --query '…' --since 30mSEARCH_QUERYRequired
A DLQ ULID rangereplay --dlq --dlq-id-from … --dlq-id-to …DLQ_RANGERequired
Specific DLQ rowsreplay --dlq --dlq-id … --dlq-id …DLQ_ID_LISTRequired
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.

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.
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
{
  "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.
--rate is deliveries per second (default 50, accepted range 5500); 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.

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:
{
  "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.
repost replay wait rpl_01JZ9F2K7M --max-wait 5m --json
{
  "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"
  }
}
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.

Job lifecycle

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

Manage a running job

While a job runs, inspect or steer it by job_id.
repost replay status rpl_01JZ9F2K7M --json
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 shows the full pause-deploy-resume loop.

Continue

Guard a deployment

Pause forwarding around a deploy, then resume and verify recovery.

Diagnose webhooks

Produce the event IDs and DLQ rows a replay should target.

Replay concepts

Windows, sourcing, and delivery semantics behind the command.