Aaron
11/25/2025, 6:34 PMMarvin
11/25/2025, 6:34 PMMarvin
11/25/2025, 6:38 PMfrom prefect import flow, task
from prefect.logging import get_run_logger
# Tasks
@task
def extract_task(source: str) -> list[str]:
logger = get_run_logger()
<http://logger.info|logger.info>(f"Extracting from {source}")
return [f"row-{i}" for i in range(3)]
@task
def transform_task(rows: list[str]) -> list[str]:
logger = get_run_logger()
<http://logger.info|logger.info>("Transforming rows")
return [r.upper() for r in rows]
@task
def load_task(rows: list[str]) -> int:
logger = get_run_logger()
<http://logger.info|logger.info>(f"Loading {len(rows)} rows")
# pretend to load
return len(rows)
# Subflows (each will appear as a separate flow run in the UI)
@flow(name="extract-stage", retries=1)
def extract_flow(source: str = "database") -> list[str]:
return extract_task(source)
@flow(name="transform-stage")
def transform_flow(rows: list[str]) -> list[str]:
return transform_task(rows)
@flow(name="load-stage")
def load_flow(rows: list[str]) -> int:
return load_task(rows)
# Orchestrator (parent) flow
@flow(name="etl-orchestrator")
def etl_main(source: str = "database") -> int:
# Each of these calls creates a nested subflow run with its own logs and state
rows = extract_flow(source=source)
processed = transform_flow(rows)
loaded_count = load_flow(processed)
logger = get_run_logger()
<http://logger.info|logger.info>(f"ETL complete: loaded {loaded_count} rows")
return loaded_count
if __name__ == "__main__":
etl_main("postgres")
What you’ll see in the UI:
- One run for etl-orchestrator (the parent).
- Three subflow runs: extract-stage, transform-stage, load-stage.
- In the parent’s graph, the subflow calls appear as special nodes linking to the child runs.
- Each subflow has its own logs, state, and retries.
Note: The above shows direct subflow calls (flow-from-flow). If you want to trigger a deployed flow from a flow (with options like timeouts or decoupling from the parent), use `run_deployment()`:
from prefect import flow
from prefect.deployments import run_deployment
@flow
async def orchestrator_calling_deployment():
# "child-flow/deployment-name"
child_run = await run_deployment(
"child-flow/my-deployment",
parameters={"param": "value"},
timeout=None, # wait until finished
as_subflow=True # link as subflow (default)
)
return child_run
If you have a specific use case (e.g., retries, cancellations, or different task runners across subflows), share a bit more detail and I can tailor the pattern and code for you.Aaron
11/25/2025, 6:42 PMMarvin
11/25/2025, 6:43 PMMarvin
11/25/2025, 6:44 PMMarvin
11/25/2025, 6:44 PMrun_deployment(...) from the orchestrator instead of calling it directly. That gives you timeouts and optional decoupling.Marvin
11/25/2025, 6:44 PMAaron
11/25/2025, 6:53 PMMarvin
11/25/2025, 6:53 PMMarvin
11/25/2025, 6:54 PMMarvin
11/25/2025, 6:54 PMThreadPoolTaskRunner(max_workers=...). If you need stricter service-wide limits, we can add Prefect concurrency limits as well.
If you share how your per-table configs are stored and whether these will run as a deployment on workers, I can tailor the data bus contents (e.g., add storage URIs, auth handles) and the parallelism pattern accordingly.Aaron
11/25/2025, 7:31 PMMarvin
11/25/2025, 7:31 PMMarvin
11/25/2025, 7:35 PMMarvin
11/25/2025, 7:35 PM