WarioRD1
12/04/2025, 6:58 PMMarvin
12/04/2025, 6:58 PMNate
12/04/2025, 7:02 PMMarvin
12/04/2025, 7:03 PMjob_variables per deployment for env/config/resources; leave image at the pool default.
- Verified CLI
prefect work-pool create my-docker-pool --type docker
prefect work-queue create my-docker-pool/critical --priority 1 --limit 2
prefect work-queue create my-docker-pool/batch --priority 10 --limit 10
- Example
from prefect import flow
@flow
def etl(): ...
# Shared image via pool; per-deployment config via job_variables and queues
etl.deploy(
name="etl-critical",
work_pool_name="my-docker-pool",
work_queue_name="critical",
tags=["etl","prod"],
concurrency_limit=2,
job_variables={
"env": {"PROFILE": "prod"},
"limits": {"cpu": "1500m", "memory": "1Gi"},
},
)
etl.deploy(
name="etl-batch",
work_pool_name="my-docker-pool",
work_queue_name="batch",
tags=["etl","batch"],
job_variables={
"env": {"PROFILE": "batch"},
"limits": {"cpu": "500m", "memory": "512Mi"},
},
)
- Per-deployment isolated images
- Keep a common base Dockerfile if helpful, but each deployment sets its own image.
- You can stay on the same work pool; just override the image per deployment. Create separate pools only if the infrastructure differs (e.g., GPU nodes vs CPU).
- Example
from prefect import flow
from prefect.infrastructure.docker import DockerImage
@flow
def train_model(): ...
@flow
def report_metrics(): ...
train_model.deploy(
name="train-model",
work_pool_name="ml-pool",
image="registry/my-ml-image:cuda-12.1",
tags=["ml","gpu"],
job_variables={
"limits": {"cpu": "2000m", "memory": "8Gi"},
"env": {"WANDB_MODE": "online"},
},
)
report_metrics.deploy(
name="report-metrics",
work_pool_name="ml-pool",
image=DockerImage(dockerfile="Dockerfile.metrics", buildargs={"PYPI_MIRROR":"https://..."}),
tags=["reporting"],
job_variables={"limits": {"cpu": "500m", "memory": "512Mi"}},
)
Other knobs that help regardless of approach
- Concurrency limits: set per deployment (concurrency_limit) and/or via queue limits to avoid overwhelming shared systems.
- Schedules and tags: organize by environment, domain, cost center, etc.
- From-source deployments: flow.from_source(...).deploy(...) to pull code at runtime; handy when you want fewer images but still isolate runtime via job variables.
- CI/CD: matrix builds for per-deployment images; single build for shared-image model.Marvin
12/04/2025, 7:03 PMjob_variables.env at run time.
3.x specifics to avoid surprises
- Use flow.deploy(...) or prefect deploy in 3.x; Deployment.build_from_flow() and prefect deployment build were removed.
- Agents are replaced by workers; use work pools/queues for routing and scaling.
If you share a bit about:
- How many deployments and how different their dependencies are
- Your runtime (Docker vs. Kubernetes) and scaling needs
- Any security/isolation requirements
- Your current CI/CD setup
…I can recommend a concrete layout (pools/queues/images) and give you a starter prefect.yaml and Dockerfile(s) tailored to your project.WarioRD1
12/04/2025, 7:07 PMMarvin
12/04/2025, 7:07 PMMarvin
12/04/2025, 7:14 PMfrom prefect import flow, tags
from prefect.artifacts import create_markdown_artifact
@flow(flow_run_name="task-{task_id}")
def process_task(task_id: str, customer_id: str, region: str):
# Tags can carry multiple identifiers/classifications
with tags(f"task_id:{task_id}", f"customer:{customer_id}", f"region:{region}"):
# Optional: write an artifact for rich audit details
create_markdown_artifact(
key="business-summary",
markdown=f"# Task {task_id}\nCustomer: {customer_id}\nRegion: {region}"
)
...
2) Launch a run with name, tags, and idempotency key (API or a parent flow)
from prefect.deployments import run_deployment
# This creates the run with the desired name/tags and enables deduplication
run = run_deployment(
name="my-project/process-task",
parameters={"task_id": "T-123", "customer_id": "C-456", "region": "us-east"},
flow_run_name="task-T-123",
tags=["task_id:T-123", "customer:C-456", "region:us-east"],
idempotency_key="T-123", # same incoming task_id if it must be unique
)
3) Query runs by name, tags, or idempotency key
import asyncio
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
FlowRunFilter, FlowRunFilterName, FlowRunFilterTags, FlowRunFilterIdempotencyKey
)
async def find_runs():
async with get_client() as client:
# By exact name
by_name = await client.read_flow_runs(
flow_run_filter=FlowRunFilter(name=FlowRunFilterName(any_=["task-T-123"]))
)
# By partial name
like_name = await client.read_flow_runs(
flow_run_filter=FlowRunFilter(name=FlowRunFilterName(like_="task-T-"))
)
# By tag
by_tag = await client.read_flow_runs(
flow_run_filter=FlowRunFilter(tags=FlowRunFilterTags(any_=["task_id:T-123"]))
)
# By idempotency key (best for exact “business ID → run” lookup)
by_idem = await client.read_flow_runs(
flow_run_filter=FlowRunFilter(
idempotency_key=FlowRunFilterIdempotencyKey(any_=["T-123"])
)
)
return by_name, like_name, by_tag, by_idem
asyncio.run(find_runs())
Notes and gotchas
- Parameters: Great for internal logic, but not filterable. Don’t rely on them for lookup.
- Run names: Highly visible and searchable. Use for your primary business ID.
- Tags: Perfect for multiple attributes. If you update tags later via the client, be sure to read and merge existing tags first—updates replace the tag list.
- Idempotency key: If you programmatically create runs (API/parent flow), setting this to your task_id gives you de-duplication and exact lookup via FlowRunFilterIdempotencyKey. If your business ID can repeat, don’t use idempotency_key.
- Privacy: Tags and names are visible in the UI; avoid putting sensitive data directly. Consider hashing if needed.
If you share how your runs are triggered (scheduled vs. API/parent) and whether the business task_id is unique, I can tailor this to your setup (e.g., naming conventions, tag schemas, and the exact client filters you’ll use).WarioRD1
12/04/2025, 10:08 PMMarvin
12/04/2025, 10:08 PMMarvin
12/04/2025, 10:09 PMfrom prefect import flow, tags
@flow
def child(task_id: str):
...
@flow
def parent(task_id: str):
# Apply tags once; both tasks and subflows inside get these tags
with tags(f"task_id:{task_id}", "region:us-east"):
child(task_id) # subflow run will include these tags
2) Add child-specific tags on top of the inherited ones
from prefect import flow, tags
@flow
def child(task_id: str):
with tags("child-phase:transform"):
...
3) If you start a “child” run via run_deployment (not a subflow), pass tags explicitly
from prefect.deployments import run_deployment
from prefect import flow
@flow
def parent(task_id: str):
run_deployment(
name="my-project/child",
parameters={"task_id": task_id},
tags=[f"task_id:{task_id}", "region:us-east"] # must pass explicitly
)
Note: runs started with run_deployment do not automatically inherit the parent’s context tags—you need to pass them.
4) Modifying tags after a run starts (if needed)
If you must add tags after creation, read and merge; updates replace the list.
from prefect import flow, get_client
from prefect.context import get_run_context
@flow
async def child():
rc = get_run_context()
async with get_client() as client:
existing = rc.flow_run.tags or []
new_tags = list(set(existing + ["extra:tag"]))
await client.update_flow_run(flow_run_id=rc.flow_run.id, tags=new_tags)
Best practices
- Use tags context in the parent around the subflow call for automatic inheritance.
- Standardize tag keys (e.g., task_id:<id>, region:<region>) for easy filtering.
- For runs triggered via run_deployment, pass tags explicitly.
- Avoid putting sensitive data in tag values (visible in UI).WarioRD1
12/04/2025, 10:16 PMMarvin
12/04/2025, 10:16 PMMarvin
12/04/2025, 10:17 PMfrom prefect import flow
@flow
def child(task_id: str):
...
@flow
def parent(task_id: str):
child(task_id=task_id) # explicit propagation
2) Child triggered via run_deployment (best when using deployments)
- Pass parameters explicitly when you create the child run. By default, run_deployment links the child as a subflow (as_subflow=True), but parameters do not auto-propagate—you must pass them.
from prefect.deployments import run_deployment
from prefect import flow
@flow
def parent(task_id: str):
run_deployment(
name="project/child",
parameters={"task_id": task_id},
# optional extras:
flow_run_name=f"task-{task_id}",
tags=[f"task_id:{task_id}"],
)
3) Defensive fallback inside the child (inherit from parent if not provided)
- If you want the child to “pick up” the parent’s task_id when the caller forgets to pass it, read the parent run’s parameters via runtime context. This works for both native subflows and runs created via run_deployment (as_subflow=True), because the child has parent_flow_run_id set.
from typing import Optional
from prefect import flow, get_client
from prefect.runtime import flow_run as rt_flow_run
@flow
async def child(task_id: Optional[str] = None):
if not task_id:
parent_id = rt_flow_run.parent_flow_run_id
if parent_id:
async with get_client() as client:
parent = await client.read_flow_run(parent_id)
task_id = parent.parameters.get("task_id")
# use task_id from here on
4) Tags as a last-resort transport
- If the parent already adds a tag like task_id:T-123, the child (as a subflow) will inherit tags. You could parse the ID from tags in the child—but this is brittle. Prefer parameters for data, tags for classification.
from prefect import flow
from prefect.runtime import flow_run as rt_flow_run
@flow
def child():
task_id = None
for t in rt_flow_run.tags or []:
if t.startswith("task_id:"):
task_id = t.split(":", 1)[1]
break
What not to use for per-run IDs
- Variables/Blocks: good for global/shared config, not for run-scoped identifiers like task_id.
Summary
- Native subflow: pass task_id as an argument.
- run_deployment: pass task_id via parameters in the API call.
- Optional fallback: child reads parent’s parameters via prefect.runtime.flow_run.parent_flow_run_id + client.
- Avoid relying on tags for passing data; use them as a complement for search and filtering.Nate
12/04/2025, 10:18 PMWarioRD1
12/05/2025, 8:15 AMget_run_logger() in every flow and task, or is there a more efficient or recommended way to structure logging?Marvin
12/05/2025, 8:15 AMMarvin
12/05/2025, 8:16 AMfrom prefect import flow, task, get_run_logger
@task
def compute(x: int) -> int:
logger = get_run_logger()
<http://logger.info|logger.info>("Computing x=%s", x)
return x * 2
@flow
def my_flow():
logger = get_run_logger()
<http://logger.info|logger.info>("Starting flow")
y = compute(21)
<http://logger.info|logger.info>("Result=%s", y)
- Let third‑party logs be auto-captured
- Prefect’s logging config attaches handlers so standard Python logging (e.g., logging.getLogger(name).info(...)) emitted during a flow/task run is captured and sent to Prefect automatically.
- Use get_run_logger when you want Prefect’s run context in the message (flow/task run IDs, nice formatting in UI).
- Don’t use module-level get_run_logger
- Avoid logger = get_run_logger() at import time; there’s no run context then. Always call it inside the flow/task body.
- Don’t pass loggers across task boundaries
- Tasks may run in separate processes/containers. Just call get_run_logger in each task; it’s lightweight.
- If you trigger child runs or subflows
- Each subflow/task gets its own run-scoped logger. Call get_run_logger in that subflow/task as needed.
Nice-to-haves
- Redirect prints to logs if you have legacy code:
from prefect.logging import patch_print
@flow
def my_flow():
patch_print() # sends print() to the run logger
print("This will appear as a Prefect log")
- Add business context without polluting tags:
logger = get_run_logger()
<http://logger.info|logger.info>("Processing", extra={"task_id": task_id})
Note: extra fields are great for structured sinks; the UI primarily displays the message text.
- Tune globally via Prefect settings/profile
- Configure log level/format/handlers via your Prefect profile or settings file. Keep code simple and let configuration control verbosity.
Rule of thumb
- Call get_run_logger at the top of each flow/task and reuse it.
- Use standard logging in libraries/utilities; Prefect will capture it during runs.
- Avoid global run loggers and passing logger objects around.WarioRD1
12/22/2025, 7:20 AMMarvin
12/22/2025, 7:20 AMMarvin
12/22/2025, 7:26 AMallow_failure(...) to let downstream tasks run even when an upstream failed.
Recommended patterns
1) Keep failures visible, but don’t block downstream: use allow_failure
- Let your artifact task raise if it can’t create the artifact (so you can see a Failed task in the UI).
- For any downstream that shouldn’t be blocked by the artifact, wrap the upstream future/state with allow_failure(...). The downstream will receive either the successful result or the Exception.
from prefect import flow, task, allow_failure
from prefect.artifacts import create_markdown_artifact
from prefect import get_run_logger
@task
def publish_artifact(text: str) -> str:
# Will raise if external storage is unavailable
return create_markdown_artifact(markdown=text, key="daily-report")
@task
def continue_work(artifact_outcome):
logger = get_run_logger()
if isinstance(artifact_outcome, Exception):
logger.warning(f"Artifact creation failed (non-blocking): {artifact_outcome}")
# proceed without the artifact
return "continued without artifact"
else:
<http://logger.info|logger.info>(f"Artifact created: {artifact_outcome}")
return "continued with artifact"
@flow
def main():
art = publish_artifact.submit("# Hello from Prefect")
# This downstream will run even if the artifact task failed
continue_work.submit(allow_failure(art))
# Other independent tasks will also continue running as usual
Notes:
- allow_failure(future) passes the Exception object to the downstream if the upstream failed; otherwise it passes the successful result.
- This keeps the artifact task marked as Failed if it truly failed, but your flow and other tasks keep going.
2) Inspect a task’s State explicitly: return_state=True
- If you want to branch on the actual state (and log messages, etc.):
from prefect import flow, task, allow_failure, get_run_logger
from prefect.artifacts import create_markdown_artifact
@task
def publish_artifact(text: str) -> str:
return create_markdown_artifact(markdown=text, key="daily-report")
@task
def handle_artifact_state(state):
logger = get_run_logger()
if state.is_failed():
exc = state.result(raise_on_failure=False)
logger.warning(f"Artifact failed (non-blocking): {exc}")
return None
return state.result()
@flow
def main():
art_state = publish_artifact(return_state=True) # returns a State
handle_artifact_state(allow_failure(art_state))
3) Make the task “safe” by swallowing exceptions
- If you never want the artifact task to fail (so it doesn’t show as Failed in the UI), catch exceptions inside the task and return a sentinel value:
from prefect import flow, task, get_run_logger
from prefect.artifacts import create_markdown_artifact
@task(retries=3, retry_delay_seconds=10)
def try_publish_artifact(text: str):
logger = get_run_logger()
try:
return {"artifact_id": create_markdown_artifact(markdown=text, key="daily-report")}
except Exception as e:
logger.warning(f"Artifact creation skipped: {e}")
return {"artifact_id": None, "error": str(e)} # Task is Completed, not Failed
@flow
def main():
info = try_publish_artifact("# Hello")
# continue regardless; info["artifact_id"] may be None
When to choose which
- Use allow_failure (Pattern 1/2) if you want the artifact step to visibly fail (for observability) but not block downstream work.
- Use try/except inside the task (Pattern 3) only if you truly want to avoid a Failed state for that task.
ExtrasMarvin
12/22/2025, 7:26 AMallow_failure also works with collections and mapped tasks, so you can pass lists/dicts of futures and proceed even if some elements failed.
If you share how you’d like these failures to appear in the UI (visible as Failed vs. silently handled), I can tailor the pattern and snippet to your exact use case.