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: ctx carries the original firing context (reason, watermarks), plus ctx.is_replay = True and a ctx.replay dict with of_run_id, knowledge_time, and code.
  • Knowledge time: the run executes with REBASE_REPLAY_KNOWLEDGE_TIME set to the original run's moment. Bitemporal reads through the SDK are automatically bounded to it — read_bitemporal filters out rows that became knowable later, and read_series-style point-in-time reads default as_of to 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 forget

With 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 of src.write(..., dataset=...) are suppressed — no downstream trigger ever fires, no watermark moves. The suppressed signal is recorded on the WriteResult (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 pathDuring replay
read_bitemporal(...)Bounded exactly — rows with knowledge_time after the original run are filtered out.
read_series(...) / series_values_select(...) without as_ofBounded exactlyas_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.

On this page