Run Replay
Re-execute any workflow run exactly as it happened — or as a counterfactual with new code or parameters.
"Why was Thursday's forecast bad?" is normally an archaeology project: dig out which code version ran, what parameters it got, what the input tables looked like before Friday's data landed on top of them. Replay collapses that into one command:
rebase run replay <run-id>The platform starts a new run of the same workflow, pinned to the same code version and parameters, and — for time-aware reads — bounded to the data that was knowable when the original run executed. The replay is a shadow run: it never signals datasets and never fires downstream triggers, so you can debug production without re-triggering production.
Exact Replay
A replay preserves everything that defined the original execution:
- Code: the replay runs the exact workflow version the original run was pinned to, even if the workflow has been redeployed since.
- Parameters: the original run's parameters are reused (overridable, see below).
- Trigger context:
ctxcarries the original firing context (reason, watermarks), plusctx.is_replay = Trueand actx.replaydict withof_run_id,knowledge_time, andcode. - Knowledge time: the run executes with
REBASE_REPLAY_KNOWLEDGE_TIMEset to the original run's moment. Bitemporal reads through the SDK are automatically bounded to it —read_bitemporalfilters out rows that became knowable later, andread_series-style point-in-time reads defaultas_ofto the bound. Late-arriving corrections, restated actuals, and newer forecast issues are invisible, exactly as they were then.
rebase run replay 4f2a... # exact re-execution, waits and compares
rebase run replay 4f2a... --no-wait # fire and forgetWith the default --wait, the CLI polls the replay to completion and prints a comparison against the original run: status, whether the results are identical (and the first differing keys when not), and durations. Identical result means the failure was deterministic — it is in the code or the inputs, not in timing. Different result means the world changed underneath the run: late data, a restated table, an unpinned dependency.
Counterfactuals
Replay one axis differently and keep everything else fixed:
# Would the fixed code have produced a good forecast on Thursday's data?
rebase run replay 4f2a... --code latest
# A specific version instead of the current one
rebase run replay 4f2a... --code 0b8c...
# Same code, different parameter
rebase run replay 4f2a... --param horizon=72--code latest runs the workflow's current version against the original knowledge-time bound; any other value is a version ID (rebase workflow versions <name>). --param/-p overrides merge over the original parameters. The new run records what it ran: replay_of, the pinned target_version_id, and trigger_source: "replay".
Batch Replay
Backtest a fix across a period by replaying every run of a workflow:
rebase run replay --workflow energy/da-forecast --since 7d --code latest
rebase run replay --workflow da-forecast --project energy \
--since 2026-07-01T00:00:00Z --until 2026-07-08T00:00:00Z \
--status succeeded --max-parallel 8 --yes--since takes a duration (7d, 24h, 90m) or an ISO datetime. The CLI lists the candidate runs, asks for confirmation (skip with --yes), replays them concurrently, and prints a comparison table:
+--------------------------------------------------------------------------------+
| Original | Replay | Status | Result | Created |
|----------+------------+------------------------+------------------+------------|
| 4f2a... | 9c1d... | succeeded -> succeeded | identical | 2026-07-04 |
| 5a3b... | 0e2f... | succeeded -> succeeded | differs: p50 | 2026-07-05 |
| 6b4c... | 1f3a... | failed -> succeeded | - | 2026-07-06 |
+--------------------------------------------------------------------------------+Runs that are themselves replays are excluded from the candidates (no replaying replays), unless you ask for them explicitly with --trigger-source replay. --no-compare submits without waiting; --json emits the comparison rows as JSON for scripting.
Shadow Mode
A replay must not re-run your pipeline, only your code. During a replay:
dataset.mark_updated(...)and the signal step ofsrc.write(..., dataset=...)are suppressed — no downstream trigger ever fires, no watermark moves. The suppressed signal is recorded on theWriteResult(signal.error = "suppressed: replay").- Warehouse writes still execute. The SDK cannot know whether a write is a harmless staging table or a production forecast, so it writes and logs a warning. Guard destination writes yourself:
@project.workflow()
def da_forecast(ctx: rb.TriggerContext = None):
forecast = model.predict(features)
if ctx is not None and ctx.is_replay:
src.write(forecast, "scratch.replay_forecasts") # inspect, don't publish
else:
src.write(forecast, "prod.forecasts", dataset="forecasts/dayahead")Exactness, Honestly
Replay bounds what the SDK can bound. Whether a read reproduces the original run depends on how it reads:
| Read path | During replay |
|---|---|
read_bitemporal(...) | Bounded exactly — rows with knowledge_time after the original run are filtered out. |
read_series(...) / series_values_select(...) without as_of | Bounded exactly — as_of defaults to the replay's knowledge time. |
Sources with no knowledge column (BitemporalSpec ingestion-time fallback) | Rows are stamped at the replay's knowledge time — kept, but their true availability history is unknown. |
Plain read("SELECT ...") | Not bounded — you get today's table. Exact replay of raw reads requires the bitemporal discipline: append-only tables with a knowledge_time column, read through read_bitemporal or as_of. |
This is the same leakage-safe discipline that makes backtesting honest: if data is stored so that "what did we know when" is answerable, replay answers it; if it is overwritten in place, no tool can recover what the run actually saw.

