Pav Staz
02/24/2026, 3:14 PMMarvin
02/24/2026, 3:14 PMNate
02/24/2026, 3:16 PMMarvin
02/24/2026, 3:18 PMclient.read_flow_runs(...) and client.delete_flow_run(...) (no leading/trailing underscores).
- Use filter field names like before_ and any_ (single trailing underscore), not `before__`/`any__`.
- Include a short sleep between batches to be kind to your DB/API.
- Stick to terminal states (COMPLETED/FAILED/CANCELLED/CRASHED) to avoid deleting active runs.
- Note: logs are deleted asynchronously after the run is deleted, so space won’t free up exactly at the same moment.
- Optional: if you want absolute stability, you can add simple retry handling around deletes, but it’s usually not necessary.
Here’s a corrected, drop-in version:
from datetime import datetime, timedelta, timezone
import asyncio
from prefect import task, get_run_logger
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
FlowRunFilter,
FlowRunFilterStartTime,
FlowRunFilterState,
FlowRunFilterStateType,
)
from prefect.client.schemas.objects import StateType
@task
async def prune_old_prefect_flows(days: int = 56, batch_size: int = 200):
"""
Delete terminal flow runs older than `days`.
Prefect 3 will remove associated flow-run logs shortly after via a background task.
"""
log = get_run_logger()
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
deleted = 0
run_filter = FlowRunFilter(
start_time=FlowRunFilterStartTime(before_=cutoff),
state=FlowRunFilterState(
type=FlowRunFilterStateType(
any_=[
StateType.COMPLETED,
StateType.FAILED,
StateType.CANCELLED,
StateType.CRASHED,
]
)
),
)
async with get_client() as client:
while True:
runs = await client.read_flow_runs(
flow_run_filter=run_filter,
limit=batch_size,
)
if not runs:
break
for run in runs:
await client.delete_flow_run(flow_run_id=run.id)
deleted += 1
<http://log.info|log.info>(f"Deleted flow run: {run.name} ({run.id})")
<http://log.info|log.info>(f"Deleted {deleted} flow runs so far (cutoff: {cutoff.isoformat()})")
# Small pause between batches to reduce DB/API pressure
await asyncio.sleep(1.0)
<http://log.info|log.info>(f"Done. Deleted {deleted} flow runs older than {cutoff.isoformat()}")
3) How should you call an async task from your flow?
- Don’t call asyncio.run() inside a flow — Prefect manages the event loop and this can raise “asyncio.run() cannot be called from a running event loop”.
- Instead:Marvin
02/24/2026, 3:18 PMfrom prefect import flow
@flow
def weekly_maintenance(retention_days: int = 14):
# ... other maintenance tasks ...
prune_old_prefect_flows(days=retention_days) # runs inline
# or, to run concurrently with other tasks:
# prune_old_prefect_flows.submit(days=retention_days)
- If your flow is async, await the task:
from prefect import flow
@flow
async def weekly_maintenance(retention_days: int = 14):
# ... other maintenance tasks ...
await prune_old_prefect_flows(days=retention_days)
A few extra tips
- You can schedule this maintenance flow/deployment to run nightly or weekly to keep things tidy.
- Consider starting with a conservative days and batch_size, then increase once you see performance is acceptable.
- Remember that runs without a start_time will not match `start_time.before_`; if you have many never-started runs to clean, you may need an additional strategy for those.
If you’d like, I can help wire this into your existing weekly_maintenance flow cleanly and set it up for scheduling.Pav Staz
02/24/2026, 3:20 PMNate
02/24/2026, 3:23 PM