Rematch a Battle and Run a Series
This guide covers the operational moves introduced in Phase V: spawning one rematch on demand, and standing up a recurring series so the dispatcher chains rematches automatically.
For the conceptual model — what gets copied, what doesn't, how lineage works — see Rematches, Replays, and Series.
Prerequisites
- The parent battle must be in a terminal status:
closed,published, orarchived. Drafts and in-flight battles cannot be rematched. - The caller must be the owner of the parent battle (
battles.battles.creator_lenser_id = auth lenser). The CLI and the web button both call the owner-checked RPCpublic.fn_battles_create_rematch. - Cloud battles require operator checklist completion and controlled rollout. See Local vs. Cloud Battles.
Single rematch via CLI
The CLI command resolves a slug, calls the owner-checked RPC, and prints the new slug.
lf battle rematch csv-parser-2026Example output:
i Resolving battle: csv-parser-2026
✔ Created rematch: csv-parser-2026-r3a7f2cJSON mode for scripting:
lf battle rematch csv-parser-2026 --json
# { "rematch_id": "9b2e4f1a-...", "slug": "csv-parser-2026-r3a7f2c" }The new battle starts in draft. You still own the lifecycle: re-open it (lf battle open), let it execute or accept submissions, then start-voting / finalize / publish as usual. See lf battle for the full flag table.
Single rematch from the web UI
On the BattleResultPage for any finalized battle you own, a "Create rematch" action is rendered next to the result summary. The button is gated to the battle owner — non-owners do not see it.
The button calls the same fn_battles_create_rematch RPC the CLI uses, so the constraints are identical: the parent must be in a terminal status, and the caller must be the owner. After success the page redirects to the new draft battle's edit view.
Recurring series via SQL
Series management is currently SQL-only. The web UI surface for series is tracked separately.
Create a series
SELECT public.fn_battles_series_create(
'weekly-arena', -- name (also used to derive the slug)
'a3c7e1f2-9b2e-4f1a-8e8e-1234567890ab', -- seed battle id (you must own it)
'0 12 * * MON' -- cron: noon every Monday
);The function:
- validates the cron expression (must be 5 whitespace-separated fields);
- requires the caller to own the seed battle;
- generates a slug like
weekly-arena-a3c7e1; - inserts the seed at
series_battles.position = 1; - primes
next_dispatch_atto the next top-of-hour boundary.
It returns the new battles.series.id.
How next_dispatch_at advances
The dispatcher (battles.fn_dispatch_series_rematches) runs once an hour at 0 * * * *. On every tick, for every active series whose pointer has been reached, the pointer is moved forward to the next top-of-hour — regardless of whether the cron matched. So:
- A matching tick spawns a rematch and bumps the pointer.
- A non-matching tick is "checked but skipped" — the pointer still bumps, so the series isn't re-evaluated until the next hour.
- A failing tick is logged as
WARNING, the pointer still bumps, and the dispatcher moves on. Per-series failures cannot block the rest of the queue.
Inspect series state
SELECT id, name, slug, cron_expr, next_dispatch_at, is_active
FROM battles.series
WHERE creator_lenser_id = lensers.get_auth_lenser_id();
SELECT position, battle_id, created_at
FROM battles.series_battles
WHERE series_id = '<series-id>'
ORDER BY position ASC;Pause a series
UPDATE battles.series
SET is_active = false
WHERE id = '<series-id>'
AND creator_lenser_id = lensers.get_auth_lenser_id();The dispatcher's main loop filters on is_active = true, so paused series are skipped without losing their position pointer. Re-activate by flipping the flag back to true; the next eligible tick picks up where the chain left off.
Troubleshooting
parent_battle_not_terminal: status=open You called fn_battles_create_rematch against a battle that hasn't reached closed/published/archived yet. Finalize and close the parent first.
parent_battle_not_found The id was wrong, the battle was hard-deleted, or it was soft-deleted (deleted_at IS NOT NULL). The CLI command resolves slugs via PostgREST and will print this if the slug doesn't match a row.
not_battle_owner The signed-in lenser is not the parent's creator_lenser_id. Co-owners and moderators are not currently authorized to spawn rematches; only the original creator is.
Slug collision on the rematch The clone helper appends -r<6-hex> to the parent slug, which makes a collision astronomically unlikely. If the parent slug is already at the length cap, the helper truncates the parent prefix to 200 characters before appending.
Series isn't producing battles Check, in order:
is_active = true.next_dispatch_at <= now().- The cron expression actually matches the current hour. The dispatcher only fires hourly — sub-hourly cron expressions are accepted but observed at hour boundaries (see Known Limitations).
- The Postgres logs for
WARNING: series_dispatch_failed: series_id=...lines. Per-series exceptions are swallowed and logged.
Related
- Rematches, Replays, and Series — what's preserved and what isn't
lf battle rematch— CLI flags and error cases- Battle share-card API —
og:imagefor any battle, including rematches - Known Limitations → Battles