Nicholas Pini
06/17/2026, 10:18 PMMarvin
06/17/2026, 10:18 PMMarvin
06/17/2026, 10:20 PM.result() on that failed subflow.
A few important details:
- In Prefect 3.x, failure propagation and cancellation propagation are different things.
- A parent flow becoming FAILED does not automatically cascade cancellation to sibling subflows.
- Automatic cascade behavior is tied to cancellation, not ordinary failure.
So with:
- parent flow in one pod
- subflow A in another pod
- subflow B in a third pod
if subflow A fails and the parent does something like:
python
a_state = a_future.result()
or otherwise awaits/reads that failed result and raises, then:
- parent flow run -> FAILED
- subflow A -> FAILED
- subflow B -> keeps running unless something explicitly cancels it
Because your subflows are on Kubernetes as separate flow runs / pods, they are independently orchestrated. The parent failing does not by itself tear down the other pod.
If you want sibling subflows to stop when one fails, you need to do that explicitly — for example by catching the exception in the parent and issuing cancellation for the other flow run(s).
Useful reference docs:
- Flows
- States
- Run work concurrently
If you want, I can show you a Prefect 3.6-compatible pattern for:
1. launching multiple subflows concurrently, and
2. cancelling the remaining ones if any one fails.Nicholas Pini
06/17/2026, 10:21 PMMarvin
06/17/2026, 10:21 PMMarvin
06/17/2026, 10:25 PMpython
import asyncio
from prefect import flow, get_run_logger
from prefect.client.orchestration import get_client
from prefect.client.schemas.objects import State, StateType
@flow
async def subflow_a():
await asyncio.sleep(5)
return "a-ok"
@flow
async def subflow_b():
await asyncio.sleep(30)
return "b-ok"
async def request_cancellation(flow_run_ids):
async with get_client() as client:
for run_id in flow_run_ids:
if run_id is None:
continue
try:
await client.set_flow_run_state(
flow_run_id=run_id,
state=State(type=StateType.CANCELLING),
force=True,
)
except Exception:
# Best-effort cleanup
pass
@flow
async def parent_flow():
logger = get_run_logger()
# Start subflows concurrently and ask Prefect to return State objects
task_a = asyncio.create_task(subflow_a(return_state=True))
task_b = asyncio.create_task(subflow_b(return_state=True))
tasks = {task_a: "subflow_a", task_b: "subflow_b"}
run_ids = {}
try:
for finished in asyncio.as_completed(tasks):
state = await finished
name = tasks[finished]
child_run_id = state.state_details.child_flow_run_id
run_ids[name] = child_run_id
if state.is_failed() or state.is_crashed() or state.is_cancelled():
logger.error(f"{name} ended in state {state.type}. Cancelling siblings.")
sibling_run_ids = [
rid for subflow_name, rid in run_ids.items() if subflow_name != name
]
# Also cancel any tasks we haven't seen finish yet, if their run ids
# become available later via stored futures/state handling
await request_cancellation(sibling_run_ids)
raise RuntimeError(f"{name} failed with state {state.type}")
<http://logger.info|logger.info>(f"{name} completed successfully")
return "all subflows succeeded"
except Exception:
# Best-effort: cancel any child runs we already know about
await request_cancellation(run_ids.values())
raise
A few important notes:
1. Use return_state=True on subflow calls
That gives you a State object instead of immediately raising on failure. Then you can inspect the state and decide what to do.
2. Cancel with StateType.CANCELLING
That tells Prefect to begin cancellation of that child flow run. This is the right orchestration-level request.
3. Cancellation is best-effort
For Kubernetes-backed runs, cancellation means Prefect asks the infrastructure/worker side to stop the run. It is not the same as instantly killing the pod at the exact line you call it.
4. Your subflows should handle cancellation cooperatively
If a subflow is doing long blocking work, it may not stop immediately. Cancellation is usually picked up at orchestration/checkpoint boundaries.
If you want a slightly more production-ready version, I’d actually recommend this structure instead:
```python
import asyncio
from prefect import flow, get_run_logger
from prefect.client.orchestration import get_client
from prefect.client.schemas.objects import State, StateType
@flow
async def worker(name: str, delay: int, fail: bool = False):
await asyncio.sleep(delay)
if fail:
raise ValueError(f"{name} failed")
return f"{name} succeeded"
async def cancel_flow_runs(run_ids):
async with get_client() as client:
for run_id in run_ids:
if not run_id:Marvin
06/17/2026, 10:25 PMflow_run_id
If you want, I can show you the even better version that uses subflow futures + flow_run_id directly, which is usually the cleanest approach for your Kubernetes use case.Nicholas Pini
06/17/2026, 10:42 PMMarvin
06/17/2026, 10:42 PMMarvin
06/17/2026, 10:44 PMsubflow.submit(...) so you get a PrefectFlowRunFuture immediately, including its flow_run_id, then cancel siblings on first failure.
This is nicer because you do not need to wait for a returned State just to learn the child run ID.
python
from prefect import flow, get_run_logger
from prefect.client.orchestration import get_client
from prefect.futures import as_completed
from prefect.states import Cancelled
@flow
def subflow_ok(name: str, delay_seconds: int = 30):
import time
time.sleep(delay_seconds)
return f"{name} completed"
@flow
def subflow_fail(name: str, delay_seconds: int = 10):
import time
time.sleep(delay_seconds)
raise RuntimeError(f"{name} failed")
def cancel_remaining(futures, exclude_flow_run_id=None):
with get_client(sync_client=True) as client:
for future in futures:
if future.flow_run_id == exclude_flow_run_id:
continue
try:
state = future.state
if state is not None and state.is_final():
continue
except Exception:
# If we can't read state, still try cancellation
pass
try:
client.set_flow_run_state(
flow_run_id=future.flow_run_id,
state=Cancelled(),
force=True,
)
except Exception:
# Best-effort cleanup
pass
@flow
def parent_flow():
logger = get_run_logger()
futures = [
subflow_ok.submit("subflow-a", delay_seconds=60),
subflow_fail.submit("subflow-b", delay_seconds=15),
subflow_ok.submit("subflow-c", delay_seconds=120),
]
for future in futures:
<http://logger.info|logger.info>(f"Submitted child flow run: {future.flow_run_id}")
results = []
try:
for completed_future in as_completed(futures):
try:
result = completed_future.result(raise_on_failure=True)
results.append(result)
<http://logger.info|logger.info>(
f"Child flow run {completed_future.flow_run_id} completed successfully"
)
except Exception as exc:
logger.error(
f"Child flow run {completed_future.flow_run_id} failed: {exc}. "
"Cancelling sibling subflows."
)
cancel_remaining(
futures,
exclude_flow_run_id=completed_future.flow_run_id,
)
raise
return results
except Exception:
# Optional extra cleanup pass
cancel_remaining(futures)
raise
Why this is the better pattern:
- subflow.submit(...) returns a PrefectFlowRunFuture
- future.flow_run_id is available right away
- as_completed(futures) lets you react as soon as the first child finishes/fails
- on failure, you can immediately call set_flow_run_state(...) on all siblings
A few practical notes:
1. Use Cancelled() / cancellation request as best-effort orchestration cleanup
This tells Prefect to cancel the child flow runs. For Kubernetes-backed runs, that means the worker/infrastructure will be asked to stop them. It may not be instantaneous.
2. Don’t expect the pod to vanish the instant one sibling fails
There can be a short delay between:
- setting the child flow run to cancelled
- the worker observing that state
- the infrastructure/pod actually stopping
3. force=True
I included force=True because in failure cleanup paths it’s often what you want. If you prefer respecting orchestration rules more strictly, you can try force=False.
If your parent flow is async, the same idea still applies, but you’d typically use the async result methods. Example:
```python
from prefect import flow, get_run_loggerMarvin
06/17/2026, 10:44 PM