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 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 |
A single explicit event ID is the only replay that executes without --yes. Everything else is bulk: multiple IDs, any --query, or any --dlq selection. 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 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.
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 isFAILED). Inspectfailed, thenforwards chainthe events that did not land.7(timeout):--max-wait(default5m) elapsed before the job finalized. The job keeps running; pollreplay statusor wait again.
Job lifecycle#
A replay job moves through these states. wait returns the moment finalized becomes true: at COMPLETED, FAILED, or CANCELLED.
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.
repost replay status rpl_01JZ9F2K7M --jsonstatus, 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.
Replay decision procedure
- Require
writescope (auth status→scopescontainswrite), else stop withforbidden_scope. - Build a selector from concrete IDs / bounded query / DLQ rows. Never an open query.
replay … --dry-run --json→ readdata.would_replay. If0, stop. If unexpectedly large, refine.- Re-run without
--dry-run; add--yesunless the selector is exactly one event ID. Always pass--idempotency-key. - On transport error, re-run the identical command;
data.deduplicated == truemeans it is already running. replay wait <job_id> --json→ branch on exit:0done ·8inspectdata.failedthenforwards chain·7not finalized, wait again.