MarcoM
06/30/2026, 2:48 PMfrom datetime import date
from prefect import flow, pause_flow_run
from core.logger import get_logger
@flow(flow_run_name="breakdown-production-{customer}-{breakdown_type}")
def breakdown_production(
customer: str,
breakdown_type: str,
reference_date: date | None = None,
dispatch: bool = False,
):
logger = get_logger()
<http://logger.info|logger.info>("Executing data quality")
try:
# simulate data quality failure
raise Exception("Data quality failed")
except:
logger.exception("Data quality check failed.")
action = pause_flow_run(wait_for_input=str)
if action == "go":
<http://logger.info|logger.info>("User chose to continue")
else:
raise ValueError("User chose not to continue")
The flow runs correctly until the pause point.
Before that, all tasks complete successfully (SQL execution, exports, S3 uploads, etc.).
When the flow reaches:
pause_flow_run(wait_for_input=str)
Prefect logs:
Pausing flow, execution will continue when this flow run is resumed.
However, after that I get this error:
Concurrency lease renewal failed - slots are no longer reserved.
Terminating execution to prevent over-allocation.
Lease not found - it may have expired or been revoked
HTTP 410 Gone
Full error:
prefect.exceptions.PrefectHTTPStatusError:
Client error '410 Gone' for url:
.../concurrency_limits/leases/<lease_id>/renew
Response:
{
'detail': 'Lease not found - it may have expired or been revoked'
}
Then the worker crashes:
Crash detected! Execution was cancelled by the runtime environment.
Finished in state Crashed()
The stack trace points back to:
prefect/flow_runs.py
pause_flow_run()
time.sleep(poll_interval)
prefect._internal.concurrency.cancellation.CancelledError
My understanding is that while the flow is paused, the ECS worker process stays alive and keeps trying to renew the concurrency lease, but the lease expires/revoked before the user resumes the flow.
Questions:
1. Is pause_flow_run(wait_for_input=...) expected to work with ECS workers, or does it require a long-running worker process?
2. Is there a recommended configuration for Human in the Loop on ECS?
3. Should the concurrency lease timeout be increased, or should paused flows release the ECS worker/concurrency slot?
4. Is there a different pattern recommended for production HITL flows (for example using deployments/work pools instead of keeping the task alive)?
Thanks!Marvin
06/30/2026, 2:48 PMMarvin
06/30/2026, 2:51 PMpause_flow_run(wait_for_input=...) is meant to work on ECS-backed flow runs.
2. But a paused flow keeps the run process alive, so the ECS task stays up while waiting.
3. The 410 Gone means the run lost its concurrency lease while paused, and Prefect cancels execution to avoid oversubscribing concurrency.
4. For production HITL on ECS, the safest pattern is usually to avoid holding expensive infrastructure open for long human waits.
A few details and recommendations below.
---
What is happening
When your flow hits:
pause_flow_run(wait_for_input=str)
Prefect:
- transitions the flow run into a Paused state
- keeps the Python process alive
- polls waiting for resume/input
- keeps trying to renew the deployment concurrency lease in the background
Your error:
Concurrency lease renewal failed - slots are no longer reserved.
...
Lease not found - it may have expired or been revoked
HTTP 410 Gone
means that background lease renewal failed because the lease no longer existed. Once that happens, Prefect cancels the run on purpose:
- to prevent the run from continuing without a reserved concurrency slot
- which shows up as the worker/ECS task crashing or being cancelled
So your diagnosis is basically right: the paused ECS task is still alive, and the lease it depends on was lost while waiting.
---
Answers to your questions
1. Is pause_flow_run(wait_for_input=...) expected to work with ECS workers, or does it require a long-running worker process?
It is expected to work with ECS-launched flow runs.
But there’s an important nuance:
- it does not require the worker service itself to be the thing that stays alive
- it does require the flow-run process (the ECS task/container running your code) to stay alive while paused
So on ECS, a paused flow means:
- the ECS task remains running
- memory/CPU remain allocated
- the run keeps its concurrency reservation unless/until that lease is lost
This makes HITL technically possible on ECS, but often not ideal for long waits.
---
2. Is there a recommended configuration for Human in the Loop on ECS?
Yes: use ECS HITL only for short pauses.
Good fit:
- brief approvals
- operator input expected within minutes
- cases where keeping the container alive is acceptable
Not a great fit:
- approvals that may sit for hours
- overnight/manual review workflows
- flows with tight deployment concurrency limits
- cost-sensitive ECS environments
If you stay with pause_flow_run(...) on ECS, I’d recommend:
- keeping the pause duration short
- making sure you’re on a recent Prefect 3 release
- reviewing any deployment-level concurrency settings
Also, if your deployment has a concurrency limit, paused runs will generally continue to occupy that slot while paused.
---
3. Should the concurrency lease timeout be increased, or should paused flows release the ECS worker/concurrency slot?
Today, paused flows do not behave like “checkpoint and release infra.” They keep the run alive.
So in current behavior:
- ECS worker/infrastructure is not released automatically
- deployment concurrency is not automatically freed just because the flow is paused
That means you have two practical options:
Option A: Increase lease/grace settings
- This can help if your HITL pause is only moderately longer than the default lease behavior.
- It reduces the chance of renewal expiry during pause.
- But it does not solve the underlying cost/concurrency problem, because the ECS task still sits there waiting.
Option B: Change the workflow pattern
- Better for production if humans may take a while.
- Avoids tying up ECS compute and deployment concurrency during approval waits.
In most production cases, Option B is better.
---
4. Is there a different pattern recommended for production HITL flows?Marvin
06/30/2026, 2:51 PMpause_flow_run(...) only for short interactive gates
- best if approval is expected quickly
- keep timeout bounded
- acceptable for “someone is actively watching this run right now”
---
Why this is especially painful on ECS
On ECS, a paused flow means:
- container is still running
- ECS resources are still billed
- the run may hold deployment concurrency
- long pauses create more surface area for lease renewal failure
So even if you increase lease duration, that only makes the waiting container survive longer — it doesn’t make the pattern more efficient.
---
What I would recommend for your case
Given your example, I’d suggest:
If the human responds quickly
- keep pause_flow_run(wait_for_input=...)
- make the pause explicitly short
- ensure you’re on a recent Prefect 3 version
Example:
from datetime import date
from prefect import flow, pause_flow_run
from core.logger import get_logger
@flow(flow_run_name="breakdown-production-{customer}-{breakdown_type}")
def breakdown_production(
customer: str,
breakdown_type: str,
reference_date: date | None = None,
dispatch: bool = False,
):
logger = get_logger()
<http://logger.info|logger.info>("Executing data quality")
try:
raise Exception("Data quality failed")
except Exception:
logger.exception("Data quality check failed.")
action = pause_flow_run(
wait_for_input=str,
timeout=300,
)
if action == "go":
<http://logger.info|logger.info>("User chose to continue")
else:
raise ValueError("User chose not to continue")
If approvals may take a while
- do not pause inside the ECS task
- split the workflow into “request approval” and “continue after approval”
---
One more thing to check
The 410 Gone can also happen if the deployment concurrency lease is revoked/expired for reasons beyond just “human waited too long,” including concurrency configuration changes or lease handling edge cases.
So I’d verify:
- whether the deployment has a concurrency limit configured
- whether this only happens on long pauses
- your exact Prefect version
If you want, I can help you narrow this down further if you share:
- your Prefect version
- whether the deployment has a concurrency limit
- how long the run stays paused before the 410 Gone
- whether this is Prefect 2.x or 3.x on the runtime actually executing this flow
---
Relevant links
- HITL / pause APIs in Prefect docs: https://docs.prefect.io/
- Prefect deployment/work pool concepts: https://docs.prefect.io/
- Prefect AWS integration: https://prefecthq.github.io/prefect-aws/
If you want, I can also help you design the split-deployment production HITL pattern for ECS and sketch the flow/deployment structure.MarcoM
06/30/2026, 2:56 PM410 Gone: --> just 4 minutes
- whether this is Prefect 2.x or 3.x on the runtime actually executing this flow: 3.x --> please try to modifiy the flow to see how we can solve the problemMarvin
06/30/2026, 2:56 PMMarvin
06/30/2026, 2:56 PMpause_flow_run(...). The flow can be made safer, but the core issue is that a paused ECS-backed run is still holding infrastructure and a deployment concurrency lease.
That said, if your goal is to modify the flow so it works reliably in production, the best fix is to change the HITL pattern.
---
Important API note
In Prefect 3.7.6, pause_flow_run has this signature:
pause_flow_run(
wait_for_input=None,
timeout=3600,
poll_interval=10,
key=None,
)
So you can set a shorter timeout, but that alone probably won’t solve a lease-renewal failure at ~4 minutes.
---
My recommendation: do not keep the ECS run paused for human input
Instead of this:
action = pause_flow_run(wait_for_input=str)
use a two-stage workflow:
1. First flow does the heavy work and detects the data-quality failure
2. It records a “pending approval” decision somewhere durable
3. It ends cleanly
4. A second flow is triggered later with the human decision
This avoids:
- idle ECS containers
- lease renewal problems
- deployment concurrency slots being tied up during human wait time
---
Recommended modified design
## Option 1 — Best production pattern on ECS: stop and relaunch
Instead of pausing inside the same flow run, fail or complete the first flow after creating an approval request.
Example:
from datetime import date
from uuid import uuid4
from prefect import flow
from core.logger import get_logger
def create_approval_request(customer: str, breakdown_type: str, reference_date: date | None) -> str:
approval_id = str(uuid4())
# Store this in your DB / S3 / app backend / ticketing system
# status = "pending"
# include enough context for the approver and for the continuation step
return approval_id
@flow(flow_run_name="breakdown-production-{customer}-{breakdown_type}")
def breakdown_production(
customer: str,
breakdown_type: str,
reference_date: date | None = None,
dispatch: bool = False,
):
logger = get_logger()
<http://logger.info|logger.info>("Executing data quality")
try:
# simulate data quality failure
raise Exception("Data quality failed")
except Exception:
logger.exception("Data quality check failed.")
approval_id = create_approval_request(
customer=customer,
breakdown_type=breakdown_type,
reference_date=reference_date,
)
<http://logger.info|logger.info>(f"Approval required. approval_id={approval_id}")
# End this run cleanly or fail intentionally depending on your business logic
return {
"status": "waiting_for_approval",
"approval_id": approval_id,
}
Then create a second flow:
from datetime import date
from prefect import flow
from core.logger import get_logger
def load_approval_decision(approval_id: str) -> str:
# Read from your DB/backend
# return "go" or "stop"
return "go"
@flow(flow_run_name="resume-breakdown-{customer}-{breakdown_type}")
def resume_breakdown_production(
approval_id: str,
customer: str,
breakdown_type: str,
reference_date: date | None = None,
dispatch: bool = False,
):
logger = get_logger()
action = load_approval_decision(approval_id)
if action == "go":
<http://logger.info|logger.info>("User chose to continue")
# continue the rest of the pipeline here
else:
raise ValueError("User chose not to continue")
This is the pattern I’d use for production ECS.
---Marvin
06/30/2026, 2:56 PMfrom datetime import date
from prefect import flow, pause_flow_run
from core.logger import get_logger
@flow(flow_run_name="breakdown-production-{customer}-{breakdown_type}")
def breakdown_production(
customer: str,
breakdown_type: str,
reference_date: date | None = None,
dispatch: bool = False,
):
logger = get_logger()
<http://logger.info|logger.info>("Executing data quality")
try:
raise Exception("Data quality failed")
except Exception:
logger.exception("Data quality check failed.")
action = pause_flow_run(
wait_for_input=str,
timeout=180,
poll_interval=5,
)
if action == "go":
<http://logger.info|logger.info>("User chose to continue")
else:
raise ValueError("User chose not to continue")
Changes:
- timeout=180
- poll_interval=5
- except Exception: instead of bare except:
Why this helps:
- If the approval doesn’t happen quickly, the flow exits before the lease-renewal edge case around ~4 minutes
- It reduces how long ECS is held open
- It’s safer operationally
But this is only appropriate if a user is expected to respond very quickly.
---
## Option 3 — Use suspend_flow_run instead of pause_flow_run if your use case allows it
Prefect 3 also exposes suspend APIs in `prefect.flow_runs`:
- suspend_flow_run
- asuspend_flow_run
This is worth testing for your case because suspend is generally the more appropriate concept when you want orchestration to stop waiting actively.
I have not verified enough implementation detail here to promise it will solve your ECS lease problem outright, so I want to be careful: I can’t guarantee suspend_flow_run will fully avoid the same infrastructure/concurrency coupling in your exact setup without testing. But it is the next Prefect-native thing I would evaluate.
If you want, I can help you rewrite your example using suspend_flow_run(...) and outline how to test it safely.
---
Best concrete change to your flow right now
If you want the most reliable immediate fix, I would rewrite the flow so it does not wait for human input inside the ECS task.
Here is a practical version that keeps your logic but stops before the human step:
```
from datetime import date
from uuid import uuid4
from prefect import flow
from core.logger import get_logger
def create_manual_decision(customer: str, breakdown_type: str, reference_date: date | None) -> str:
decision_id = str(uuid4())
# Persist this somewhere:
# - database table
# - S3 json file
# - internal API
#
# Example payload:
# {
# "decision_id": decision_id,
# "customer": customer,
# "breakdown_type": breakdown_type,
# "reference_date": str(reference_date) if reference_date else None,
# "status": "pending"
# }
return decision_id
@flow(flow_run_name="breakdown-production-{customer}-{breakdown_type}")
def breakdown_production(
customer: str,
breakdown_type: str,
reference_date: date | None = None,
dispatch: bool = False,
):
logger = get_logger()
logger.info("Executing data quality")
try:
raise Exception("Data quality failed")
except Exception:
logger.exception("Data quality check failed.")
decision_id = create_manual_decision(
customer=customer,
breakdown_type=breakdown_type,
reference_date=reference_date,
)
logger.info(
f"Manual approval required. decision_id={decision_id}. "
"This flow run will now exit and wait for an external continuation trigger."
)
return {
"status": "manual_approval_required",
"decision_id": decision_id,
"customer": customer,Marvin
06/30/2026, 2:56 PMThen trigger a second deployment when the person approves.
---
*If you insist on keeping the pause in one flow*
Then I would at least use this safer version:
from datetime import date
from prefect import flow, pause_flow_run
from core.logger import get_logger
@flow(flow_run_name="breakdown-production-{customer}-{breakdown_type}")
def breakdown_production(
customer: str,
breakdown_type: str,
reference_date: date | None = None,
dispatch: bool = False,
):
logger = get_logger()
logger.info("Executing data quality")
try:
raise Exception("Data quality failed")
except Exception:
logger.exception("Data quality check failed.")
try:
action = pause_flow_run(
wait_for_input=str,
timeout=180,
poll_interval=5,
)
except Exception:
logger.exception("Pause/resume failed")
raise
if action == "go":
logger.info("User chose to continue")
else:
raise ValueError("User chose not to continue")
```
But again: this is a mitigation, not a robust production fix for ECS.
---
Why 4 minutes matters
The fact that your crash happens around 4 minutes strongly suggests the failure is tied to lease renewal cadence, not to your flow logic.
So:
- changing wait_for_input=str to some other type won’t help
- adding try/except around pause_flow_run() won’t really fix it
- shortening the timeout may avoid the symptom
- redesigning the workflow avoids the class of problem
---
My final recommendation
For Prefect 3.7.6 + ECS worker + deployment concurrency limit, use:
- short pause only if humans respond immediately
- otherwise split the flow into request/approve/continue stages
If you want, next I can give you one of these two things:
1. a complete Prefect 3.x example of the two-deployment approval pattern, or
2. a test version using suspend_flow_run() to compare against pause_flow_run() on ECS.MarcoM
06/30/2026, 3:00 PMMarvin
06/30/2026, 3:00 PMMarvin
06/30/2026, 3:02 PMsuspend_flow_run is the right thing to try on ECS.
For your use case, it is usually better than pause_flow_run because:
- the flow run is suspended
- the ECS task can exit
- Prefect can resume later on new infrastructure
- you avoid keeping the container alive while waiting for a human
That is exactly the behavior you want for production HITL on ECS.
---
Important differences from pause_flow_run
pause_flow_run(...)
- keeps the current process alive
- keeps polling
- tends to keep infra/concurrency tied up while waiting
suspend_flow_run(...)
- suspends the run for later resumption
- current process exits
- resume happens by re-entering orchestration later
So this is the best next experiment.
---
Important requirement
Use persist_result=True on the flow.
Without persisted results, resumed/suspended workflows are much more fragile because Prefect needs stored state/results to continue correctly.
---
Suggested rewrite of your flow
Here is the cleanest version of your example to test with suspend:
from datetime import date
from prefect import flow
from prefect.flow_runs import suspend_flow_run
from core.logger import get_logger
@flow(
flow_run_name="breakdown-production-{customer}-{breakdown_type}",
persist_result=True,
)
def breakdown_production(
customer: str,
breakdown_type: str,
reference_date: date | None = None,
dispatch: bool = False,
):
logger = get_logger()
<http://logger.info|logger.info>("Executing data quality")
try:
# simulate data quality failure
raise Exception("Data quality failed")
except Exception:
logger.exception("Data quality check failed.")
action = suspend_flow_run(
wait_for_input=str,
timeout=3600,
key="dq-approval",
)
if action == "go":
<http://logger.info|logger.info>("User chose to continue")
else:
raise ValueError("User chose not to continue")
---
Why I changed these parts
- persist_result=True
- important for suspend/resume behavior
- except Exception:
- better than bare except:
- key="dq-approval"
- helpful to make the suspension step stable/idempotent
- suspend_flow_run(...)
- this is the key change you want to test
---
What to expect operationally
When the flow reaches:
action = suspend_flow_run(
wait_for_input=str,
timeout=3600,
key="dq-approval",
)
expected behavior is:
1. flow run enters a suspended/paused waiting state in Prefect
2. ECS task exits instead of sitting alive
3. deployment concurrency/infrastructure pressure should be much lower than with pause_flow_run
4. when you resume with input, Prefect will schedule continuation
This is why suspend_flow_run is much more appropriate than pause_flow_run for ECS HITL.
---
Very important caveat
You should test whether the code before the suspend point is re-orchestrated on resume in a way that matters for your flow.
In practice, with suspend/resume patterns, you should assume:
- resume may involve re-entering the flow logic
- already-completed work should be safe/idempotent
- persisted results help Prefect avoid recomputing unnecessarily, but you still want your workflow design to be robust
Since your real flow does:
- SQL execution
- exports
- S3 uploads
- etc.
you should not assume that blindly suspending in the middle is safe unless those steps are idempotent or checkpointed appropriately.
---
Safer production structure around suspend
A better structure is:
1. do your expensive/data-mutating work
2. save any outputs/decision context durably
3. call suspend_flow_run(...)
4. after resume, only run the continuation logic
For example:
```
from datetime import date
from prefect import flow
from prefect.flow_runs import suspend_flow_run
from core.logger import get_logger
@flow(
flow_run_name="breakdown-production-{customer}-{breakdown_type}",
persist_result=True,
)Marvin
06/30/2026, 3:02 PM---
*How to test it*
I’d test in this order:
*Test 1 — minimal suspend*
- deploy the simplified flow
- trigger it
- verify that once it reaches `suspend_flow_run`, the ECS task stops cleanly
- verify the flow run remains resumable in UI
*Test 2 — manual resume with input*
- resume it with `go`
- verify continuation works
*Test 3 — rejection path*
- resume it with any value other than `go`
- verify it fails with your intended business error
*Test 4 — long wait*
- leave it suspended longer than the 4-minute window that was breaking `pause_flow_run`
- confirm there is no lease-renewal crash while waiting
That last test is the important proof.
---
*What I expect relative to your current issue*
With `pause_flow_run`, you saw:
- lease renewal attempts while waiting
- HTTP 410
- runtime cancellation/crash
With `suspend_flow_run`, I would expect that class of failure to go away *while the run is waiting*, because the ECS task should not be sitting there renewing the lease the same way.
---
*One warning about user input type*
You are using:
wait_for_input=str
That is fine for a quick test.
For production, I’d recommend a structured input model instead, so the resume payload is explicit and less error-prone.
Example:
from pydantic import BaseModel
class ApprovalInput(BaseModel):
action: str
comment: str | None = None
and then:
action = suspend_flow_run(
wait_for_input=ApprovalInput,
timeout=3600,
key="dq-approval",
)
if action.action == "go":
logger.info("User chose to continue")
else:
raise ValueError("User chose not to continue")
That tends to be cleaner in real HITL flows.
---
*Recommended next test version*
If you want the smallest diff from your current code, use this:
from datetime import date
from prefect import flow
from prefect.flow_runs import suspend_flow_run
from core.logger import get_logger
@flow(
flow_run_name="breakdown-production-{customer}-{breakdown_type}",
persist_result=True,
)
def breakdown_production(
customer: str,
breakdown_type: str,
reference_date: date | None = None,
dispatch: bool = False,
):
logger = get_logger()
logger.info("Executing data quality")
try:
raise Exception("Data quality failed")
except Exception:
logger.exception("Data quality check failed.")
action = suspend_flow_run(
wait_for_input=str,
timeout=3600,
key="dq-approval",
)
if action == "go":
logger.info("User chose to continue")
else:
raise ValueError("User chose not to continue")
```
---
If you want, I can next give you:
- a version with a Pydantic approval model, or
- a full test checklist for resuming a suspended run from the UI/API.MarcoM
06/30/2026, 3:09 PMMarvin
06/30/2026, 3:10 PMMarvin
06/30/2026, 3:10 PMsuspend_flow_run is the better fit for your ECS setup.
What you’re seeing now is the normal suspend/resume behavior:
- first run executes until suspend_flow_run(...)
- run goes to Suspended(type=PAUSED)
- ECS task exits
- when resumed, Prefect starts a new ECS task
- the flow is re-entered
- previously completed work can be reused if it was persisted/cached
- anything not protected by caching/persistence may run again
So your conclusion is correct:
yes, you need to cache or otherwise persist everything you do before the data quality approval step
---
What your logs show clearly
You have this sequence:
- initial run:
- flow starts
- upstream work runs
- suspend happens
- resumed run:
- flow starts again
- some prior step shows Finished in state Cached(type=COMPLETED)
- then execution continues after approval
That means:
- Prefect is re-orchestrating the flow on resume
- cached/persisted steps are being reused
- this is exactly the pattern you should lean into
---
Recommended design
For a suspend-based HITL flow on ECS, structure the flow into three parts:
1. pre-approval tasks
- expensive SQL
- exports
- S3 uploads
- any deterministic prep work
2. approval gate
- suspend_flow_run(...)
3. post-approval tasks
- dispatch / publish / irreversible side effects
The key is:
- everything before the suspend point should be in tasks
- those tasks should use result persistence and/or caching
- post-approval work should be separated so it only runs after approval
---
What not to do
Avoid putting important pre-approval work inline in the flow body like this:
@flow(persist_result=True)
def my_flow():
run_big_sql()
upload_files()
action = suspend_flow_run(wait_for_input=str)
publish_results()
Why this is risky:
- plain Python in the flow body is not as easy for Prefect to cache/reuse as tasks
- on resume, the flow body is re-entered
- you may accidentally re-run side effects
Instead, move those operations into tasks.
---
Recommended rewrite pattern
Here’s a practical structure for your case.
```
from datetime import date
from pydantic import BaseModel
from prefect import flow, task
from prefect.flow_runs import suspend_flow_run
from prefect.cache_policies import INPUTS
from core.logger import get_logger
class ApprovalInput(BaseModel):
action: str
comment: str | None = None
@task(
persist_result=True,
cache_policy=INPUTS,
)
def run_data_preparation(
customer: str,
breakdown_type: str,
reference_date: date | None,
) -> dict:
logger = get_logger()
logger.info("Running SQL / exports / S3 uploads before approval")
# your real pre-approval work here
# must be safe to reuse from persisted result
return {
"customer": customer,
"breakdown_type": breakdown_type,
"reference_date": str(reference_date) if reference_date else None,
"manifest_path": "s3://bucket/path/manifest.json",
"dq_passed": False,
}
@task(
persist_result=True,
cache_policy=INPUTS,
)
def evaluate_data_quality(prepared_data: dict) -> bool:
logger = get_logger()
logger.info("Executing data quality")
# replace with real logic
return prepared_data["dq_passed"]
@task
def dispatch_manifest(prepared_data: dict) -> None:
logger = get_logger()
logger.info("Dispatch is True, saving manifest file.")
# irreversible side effect here
# e.g. save/publish/send downstream
@flow(
flow_run_name="breakdown-production-{customer}-{breakdown_type}",
persist_result=True,
)
def breakdown_production(
customer: str,
breakdown_type: str,
reference_date: date | None = None,
dispatch: bool = False,
):
logger = get_logger()
logger.info("=== START BREAKDOWN PRODUCTION ===")
logger.info(f"Customer: {customer}")
logger.info(f"Breakdown Type: {breakdown_type}")Marvin
06/30/2026, 3:10 PM---
*Why this structure helps*
`run_data_preparation(...)`
- encapsulates all expensive pre-approval work
- persisted/cached, so resume can reuse it
`evaluate_data_quality(...)`
- clearly separates decision logic
`suspend_flow_run(...)`
- stops infra while waiting for human input
`dispatch_manifest(...)`
- only runs after approval
- avoids re-triggering irreversible side effects before approval
---
*Important operational rule*
Anything before suspend should be one of these:
- *idempotent*, or
- *cached/persisted*, or
- *both*
Anything after suspend should be:
- safe to run exactly once after approval
- ideally isolated into its own task(s)
---
*About caching specifically*
You said:
> the theme now is that i have to cache what i've done till the dataquality checks
Yes, exactly.
But more precisely, you want a mix of:
- *task result persistence*
- *cache policies / cache keys*
- *idempotent external writes*
These are related, but not identical.
---
*Practical guidance for your pre-approval steps*
## If a step reads data / computes data
Use a task with persisted results and caching.
Example:
@task(
persist_result=True,
cache_policy=INPUTS,
)
def compute_something(customer: str, reference_date: str) -> dict:
...
## If a step writes to S3
Prefer one of these:
- deterministic output path, so rerun overwrites same object safely
- check-if-exists logic
- persist the produced path as the task result
Example:
@task(persist_result=True, cache_policy=INPUTS)
def export_to_s3(customer: str, reference_date: str) -> str:
path = f"s3://my-bucket/exports/{customer}/{reference_date}/file.csv"
# write once or overwrite safely
return path
## If a step writes to a database
Make it idempotent:
- upsert instead of insert
- use a run/business key
- avoid “append duplicate row every retry/resume”
---
*One subtle but important point*
Caching helps when Prefect can recognize the task as already done for the same inputs.
But for *external side effects*, do not rely on caching alone.
For example:
- if a task uploaded a file but its state/result was not available for some reason
- Prefect may rerun the task
- so the operation itself should still be safe
That’s why *idempotency* matters in addition to caching.
---
*Suggested minimal adaptation of your current code*
If you don’t want a big rewrite yet, do this first:
1. move all pre-DQ work into one or more tasks
2. add `persist_result=True` to flow and tasks
3. suspend only after those tasks
4. move post-approval side effects into separate tasks
Minimal pattern:
from datetime import date
from pydantic import BaseModel
from prefect import flow, task
from prefect.flow_runs import suspend_flow_run
class ApprovalInput(BaseModel):
action: str
@task(persist_result=True)
def prepare_breakdown(customer: str, breakdown_type: str, reference_date: date | None) -> dict:
# sql, exports, s3 uploads
return {
"customer": customer,
"breakdown_type": breakdown_type,
}
@task
def finalize_breakdown(prepared: dict, dispatch: bool) -> None:Marvin
06/30/2026, 3:10 PMpause_flow_run was the wrong fit here
- suspend_flow_run works
- the remaining task is to checkpoint the pre-approval work into persisted/cached tasks
That’s the right production direction.
If you want, I can next help you with one of these:
1. refactor your real flow into pre-approval/post-approval tasks, or
2. show a good cache strategy for SQL + S3 export tasks so they don’t rerun on resume.Bring your towel and join one of the fastest growing data communities. Welcome to our second-generation open source orchestration platform, a completely rethought approach to dataflow automation.
Powered by