<@ULVA73B9P> can you provide any additional inform...
# ask-marvin
s
@Marvin can you provide any additional information on upgrading from Prefect 2.20.16 to Prefect 3.x.x. and is there any other documentation you would suggest reading in conjunction with https://docs.prefect.io/v3/how-to-guides/migrate/upgrade-to-prefect-3?
m
thought for 109.1 seconds
Yes — beyond the main migration guide, there are a few high-value things I’d make sure to review when moving from Prefect 2.20.16 to Prefect 3.x. Start here - Upgrade to Prefect 3 Also strongly recommended - Upgrade from agents to workers - Deployments - Work pools - Workers - States - Caching - Deploy via Python - Store flow code / pull code from source - Manage settings - Database maintenance if you are self-hosting Prefect server The biggest practical differences to watch for 1. Deployments changed a lot If your 2.20.16 setup uses: -
Deployment.build_from_flow()
-
prefect deployment build
- infrastructure blocks for execution - agents then this is probably the biggest migration area. In Prefect 3: -
Deployment.build_from_flow()
is removed -
prefect deployment build
is removed - you should use
prefect deploy
or
flow.from_source(...).deploy(...)
- workers + work pools replace the old agent/infrastructure-block execution pattern So if your team has deployment automation around YAML generation or
build/apply
, that deserves focused testing. 2. Agents → workers / work pools If you were already using workers in late Prefect 2, this part is easier. If not, read this one carefully: - Upgrade from agents to workers Conceptually: - agent model: pick up work broadly - worker model: tied to a work pool type - pool/job config replaces much of the old infrastructure-block pattern 3. Flow/task behavior changes are worth testing explicitly A few behavior changes can surprise people during upgrade: - Task caching defaults: Prefect 3 introduces stronger built-in caching behavior. If you have side-effecting tasks, review them against: - Caching - States / failure handling: audit flows that depend on task failures bubbling in a particular way: - States - Async patterns: if you have async flows/tasks, test them directly after upgrade rather than assuming old behavior carries over. 4. Storage/block usage may need updates If you relied on older storage block patterns, especially GitHub/file-system style deployment code retrieval, review: - Store flow code In Prefect 3, a common pattern is:
Copy code
python
my_flow.from_source(
    source="<https://github.com/org/repo>",
    entrypoint="flows.py:my_flow",
).deploy(
    name="prod",
    work_pool_name="my-pool",
)
5. Self-hosted server users should plan the DB upgrade If you run your own Prefect server/database, also read: - Database maintenance And plan for the database migration step during the upgrade window. 6. Settings/config are nicer, but worth reviewing Prefect 3 adds better project-level configuration patterns like
prefect.toml
/
pyproject.toml
support and
.env
loading. If your 2.x setup depends heavily on profiles and env vars, review: - Manage settings Suggested upgrade reading order If I were doing this migration, I’d read in this order:
1. Upgrade to Prefect 3 2. Upgrade from agents to workers 3. Deployments 4. Work pools 5. Deploy via Python 6. Store flow code 7. States 8. Caching 9. Manage settings My practical recommendation Before upgrading production, test these 4 things in staging: - one deployment created with your new deployment method - one worker/work-pool path end-to-end - one flow with retries/failures/state-dependent logic - one flow with side-effecting tasks to confirm caching behavior is what you expect If you want, I can also help you build a 2.20.16 → 3.x migration checklist tailored to your setup, for example if you tell me: - Cloud or self-hosted - agents or workers today - Docker / Kubernetes / ECS / process execution - Python deployment code vs
prefect.yaml
vs CLI-driven deployment
s
@Marvin we are already on workers with Prefect 2.20.16 - what is the recommended upgrade path for the workers?
m
thought for 98.9 seconds
If you're already on workers in Prefect 2.20.16, the worker upgrade path is much simpler than the older agents → workers migration. Short version: - you generally keep using work pools/workers - upgrade the API/server side first - then upgrade and restart your workers - then update/redeploy flows using Prefect 3 deployment patterns as needed Most important compatibility rule Prefect client/server major versions need to match. In practice, that means: - do not upgrade workers to 3.x before the server/API is on 3.x - for self-hosted, upgrade server first - for Cloud, the control plane is already compatible, so you mainly upgrade your worker/runtime environments and deployments Recommended path If you use Prefect Cloud 1. Upgrade Prefect in the worker environment:
Copy code
bash
   pip install -U prefect
2. Upgrade any integration packages your workers use:
Copy code
bash
   pip install -U prefect-aws prefect-gcp prefect-kubernetes prefect-docker
only for the ones you actually use 3. Restart/redeploy the workers 4. Rebuild/redeploy your deployments using Prefect 3-compatible deployment patterns where needed If you are self-hosting Prefect server 1. Pause schedules first This is a recommended precaution during the migration window. 2. Upgrade the server environment to Prefect 3 3. Run the DB migration:
Copy code
bash
   prefect server database upgrade
4. Start the upgraded Prefect 3 server/API 5. Upgrade worker environments to Prefect 3 6. Restart/redeploy all workers 7. Validate that workers reconnect and pick up runs 8. Resume schedules What happens to existing work pools? If you're already using workers, your work pools are not the main problem. In most cases: - existing work pools remain - you do not need a brand-new worker architecture migration - the biggest follow-up work is usually around deployments, not the workers themselves So think of this as: - workers: upgrade package + restart - deployments: verify and modernize where needed What I would specifically test after worker upgrade For each pool type you use, validate one real run end-to-end: - process worker - docker worker - kubernetes worker - ecs / cloud-run / aci worker, etc. Check: - worker starts successfully - worker registers with the expected pool - job template/job variables still behave as expected - environment variables/secrets still resolve correctly - image / pull / namespace / service account settings still apply correctly Operationally, I would recommend this sequence 1. Pick one non-prod work pool 2. Upgrade one worker instance/container/image 3. Run a deployment from that pool 4. Confirm infrastructure behavior 5. Then roll the same worker image/version change out to the rest What usually needs more attention than the workers Even if workers are already in place, these often still need review: - deployment creation method Prefect 3 removes old 2.x deployment build patterns. Use
prefect deploy
or
flow.from_source(...).deploy(...)
. Deployments - code retrieval/storage pattern Store flow code - work pool/job configuration expectations Work pools - worker behavior / concepts Workers Recommended docs for your case specifically Since you are already on workers, I’d prioritize these over the agents migration guide: - Upgrade to Prefect 3 - Workers - Work pools - Deployments - Deploy via Python
- Store flow code - Manage settings If self-hosted: - Database maintenance Practical recommendation If you’re already on 2.20.16 workers, I’d treat the upgrade as: - Phase 1: upgrade API/server - Phase 2: rebuild worker image/environment on Prefect 3 and restart workers - Phase 3: validate deployments and update any old deployment-generation code - Phase 4: resume schedules and monitor first runs If you want, I can give you a worker-by-worker upgrade checklist for your specific pool type — e.g. Kubernetes worker, Docker worker, or process worker.
s
@Marvin here is our current setup that we need to migrate from 2.20.16 → 3.x migration checklist: - Cloud - workers - Kubernetes i.e EKS - Python deployment code
m
thought for 18.7 seconds
Got it — for Prefect Cloud + workers + Kubernetes/EKS + Python deployment code, the worker side is relatively straightforward, and the main migration effort is usually: - upgrading the worker image/environment to Prefect 3 - validating Kubernetes work pool behavior - updating Python deployment code to Prefect 3 deployment patterns Here’s a practical 2.20.16 → 3.x migration checklist for your setup. Recommended docs for your exact setup - Upgrade to Prefect 3 - Workers - Work pools - Deployments - Deploy via Python - Store flow code High-level migration plan For your environment, I’d do this in 4 phases: 1. Inventory current deployment code and worker config 2. Upgrade one non-prod Kubernetes worker to Prefect 3 3. Migrate Python deployment code and test end-to-end in one pool 4. Roll out to remaining workers and deployments Phase 1: Inventory what you have today Before changing anything, identify: - all worker deployments in EKS - all Prefect work pools they connect to - current Prefect version in each worker image - all Python deployment scripts - whether your deployment code uses any of these 2.x patterns: -
Deployment.build_from_flow()
-
Deployment(...)
- infrastructure blocks like
KubernetesJob.load(...)
- storage blocks for Git-based code retrieval - any custom
job_variables
/ namespace / image / serviceAccount / env overrides For your setup, the biggest red flag is old deployment code like:
Copy code
python
from prefect.deployments import Deployment
from prefect.infrastructure.kubernetes import KubernetesJob
If you have that, it needs to be rewritten for Prefect 3. Phase 2: Upgrade a non-prod Kubernetes worker first Because you’re on Prefect Cloud, you do not need to upgrade a self-hosted API/server. Focus on the worker runtime. 2.1 Build a new worker image Upgrade Prefect and any integrations you use. Example:
Copy code
bash
pip install -U prefect prefect-kubernetes
If your worker image also runs deployment scripts or flow code that uses AWS or other integrations, include those too:
Copy code
bash
pip install -U prefect prefect-kubernetes prefect-aws
2.2 Deploy one test worker in EKS Pick one non-prod worker deployment and update it to the new image. Verify: - worker pod starts cleanly - worker connects to the expected Prefect Cloud workspace - worker registers against the expected work pool - no import/runtime errors from missing integrations 2.3 Validate one test flow run Run a simple deployment through that pool and confirm: - job is created in the correct namespace - image is correct - env vars are injected - service account / IAM role assumptions still work - logs stream correctly back to Cloud Phase 3: Migrate Python deployment code This is likely the main code change. Old 2.x patterns to remove Do not keep using: -
Deployment.build_from_flow()
- infrastructure-block-based deployment creation - old GitHub storage block patterns In Prefect 3, prefer: -
flow.from_source(...).deploy(...)
- or
flow.deploy(...)
if code is already available in the image/runtime Typical target pattern for Git-based source If your Kubernetes jobs should pull code from a repo: ```python from prefect import flow @flow def my_flow(): print("hello") if name == "__main__": my_flow.from_source( source="https://github.com/my-org/my-repo.git", entrypoint="flows/my_flow.py:my_flow", ).deploy( name="my-flow", work_pool_name="eks-pool", job_variables={ "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-image:latest",
"namespace": "prefect", "service_account_name": "prefect-worker", }, )
Copy code
*If code is baked into the image*
If your flow code is already in the execution image, you may use:
```python
from prefect import flow

@flow
def my_flow():
    print("hello")

if __name__ == "__main__":
    my_flow.deploy(
        name="my-flow",
        work_pool_name="eks-pool",
        image="<http://123456789012.dkr.ecr.us-east-1.amazonaws.com/my-image:latest|123456789012.dkr.ecr.us-east-1.amazonaws.com/my-image:latest>",
    )
Which pattern is best depends on whether your EKS jobs: - pull code from git/storage at run time, or - run code baked into a container image For most Kubernetes/EKS production setups, I’d strongly recommend being explicit and standardizing one of those two patterns. Phase 4: Validate Kubernetes-specific job settings With EKS, pay special attention to anything previously handled via infrastructure blocks or overrides. Check these carefully in the work pool + deployment job variables: -
namespace
-
image
-
image_pull_policy
-
service_account_name
-
env
-
labels
-
annotations
-
finished_job_ttl
-
cpu
/
memory
requests and limits - node selectors / tolerations if used - IAM role / IRSA assumptions - secrets/config maps mounting behavior Make sure every old
infra_overrides
setting has a home in the new work pool/job variables model. Suggested migration checklist Preparation - [ ] Identify all EKS worker deployments - [ ] Identify all Prefect work pools used by those workers - [ ] List all Python deployment scripts - [ ] Find/remove any use of
Deployment.build_from_flow()
- [ ] Find/remove any infrastructure-block-based deployment code - [ ] List all Kubernetes execution settings currently overridden per deployment - [ ] Identify whether flows use git-pull source or image-baked source Worker upgrade - [ ] Build a new worker container image with Prefect 3 - [ ] Include required integrations:
prefect-kubernetes
,
prefect-aws
, etc. - [ ] Deploy upgraded worker to non-prod EKS - [ ] Confirm registration with Prefect Cloud - [ ] Confirm it polls the expected work pool Deployment code migration - [ ] Rewrite one deployment script using
flow.from_source(...).deploy(...)
or
flow.deploy(...)
- [ ] Recreate one non-prod deployment - [ ] Validate work pool assignment - [ ] Validate Kubernetes
job_variables
- [ ] Trigger a test run - [ ] Verify pod creation, execution, logs, and completion Behavior validation - [ ] Test a successful run - [ ] Test a failing run - [ ] Test retries - [ ] Test env var / secret injection - [ ] Test AWS permissions from inside the job pod - [ ] Test concurrency / scaling expectations if applicable Rollout - [ ] Upgrade remaining worker deployments in EKS - [ ] Migrate remaining deployment scripts - [ ] Recreate/redeploy production deployments - [ ] Monitor first production runs closely What I would watch most closely in your case 1. Old Python deployment builders This is the most likely breaking area. If your current scripts programmatically construct deployments using the old deployment/infrastructure APIs, those should be rewritten first. 2. Kubernetes job customization Anything you previously passed as infra config/overrides needs to be revalidated in the work pool + deployment configuration. 3. Code retrieval model Be clear whether your flow code is: - cloned/pulled at runtime, or - baked into the image A lot of migration confusion comes from mixing both. 4. Integration packages If flow code or worker code touches AWS/Kubernetes features, ensure the relevant packages are explicitly installed in the worker and/or run image. My recommended rollout strategy For your setup, I would not do a big-bang cutover. I’d do: - one non-prod pool - one worker deployment - one simple flow - one realistic production-like flow - then expand That tends to catch: - missing Python deps - EKS RBAC / IRSA issues - bad
job_variables
- code-source assumptions
If you want, send me one of your current Python deployment scripts and I can help convert it from Prefect 2.20.16 style to the recommended Prefect 3 pattern for EKS.
s
@Marvin the code we use for deployment is
Copy code
mu_flow_instance = my_flow.from_source(...)
and
Copy code
deployment_id = my_flow_instance.deploy(...)
m
thought for 13.0 seconds
That’s good news — if your deployment code already looks like:
Copy code
python
my_flow_instance = my_flow.from_source(...)
deployment_id = my_flow_instance.deploy(...)
then your deployment code is already very close to the recommended Prefect 3 style. So for Cloud + workers + EKS + Python deployment code, your migration risk is much lower than for teams still using old 2.x deployment builders. What this means for your upgrade You likely do not need a major deployment-code rewrite. Your focus should instead be on: - upgrading the Prefect version in your worker image/runtime - upgrading Prefect in the environment that runs the deployment script - validating that your
.from_source(...)
and
.deploy(...)
arguments still behave as expected - testing Kubernetes job configuration coming from the work pool /
job_variables
What I would specifically verify in your code Even though the pattern is correct, I’d still check for these details: 1.
from_source(...)
source type
Make sure the source mechanism is still one of the supported/recommended ones, e.g.: - Git URL - supported storage/source object If you're using an old block-based source pattern, that may still need adjustment. 2.
deploy(...)
arguments
Check whether you are passing any Kubernetes-specific values like: -
work_pool_name
-
image
-
job_variables
- schedule-related args Those should be tested against your current work pool template. 3. Schedule definitions If your deployment code defines schedules, verify those arguments carefully during testing. This is one area where upgrade details can be easy to miss. 4. Runtime dependencies The environment that executes:
Copy code
python
my_flow.from_source(...).deploy(...)
also needs to be on Prefect 3, not just the workers. So in practice you have two upgrade surfaces: - the worker environment/image - the deployment-creation environment (CI/CD job, laptop, release image, etc.) Revised migration checklist for your actual setup Probably not needed - [ ] large-scale rewrite of deployment code - [ ] migration away from old
Deployment.build_from_flow()
APIs - [ ] migration from agents to workers Still needed - [ ] upgrade worker image to Prefect 3 - [ ] upgrade deployment-script runtime to Prefect 3 - [ ] upgrade integration packages used by either environment - [ ] test one deployment creation end-to-end - [ ] test one flow run end-to-end on EKS - [ ] validate Kubernetes job config still renders correctly Concrete validation steps I’d run Step 1: upgrade deployment environment Wherever this code runs:
Copy code
python
my_flow_instance = my_flow.from_source(...)
deployment_id = my_flow_instance.deploy(...)
upgrade that environment first in non-prod:
Copy code
bash
pip install -U prefect prefect-kubernetes prefect-aws
Step 2: run deployment creation in non-prod Confirm: - deployment is created successfully - deployment shows up in Prefect Cloud - work pool is correct - schedule/triggers are correct - no deprecation or import issues Step 3: upgrade one worker image Upgrade one non-prod EKS worker to Prefect 3 and verify it connects normally. Step 4: trigger a test run Confirm the EKS execution path still works: - pod launches - image pulls - env/secrets load - IAM/IRSA works - logs appear in Cloud - final state is correct What I’d watch for specifically Because your deployment code is already modern, the remaining issues are more likely to be around: - Kubernetes job template behavior - dependency mismatches between worker image and flow image - integration package versions - state/caching behavior changes in actual flow execution Bottom line If you are already using:
Copy code
python
from_source(...).deploy(...)
then your worker upgrade path is basically operational, not architectural. That means: - upgrade Prefect to 3 in deployment and worker environments - test non-prod - validate EKS execution behavior - roll forward gradually
If you want, I can help with a very targeted pre-flight checklist for your exact code by reviewing the actual arguments you pass into
.from_source(...)
and
.deploy(...)
.
s
@Marvin a targets pre-flight checklist would be useful
m
thought for 16.8 seconds
Absolutely — here’s a targeted pre-flight checklist for your setup: - Prefect Cloud - workers already in use - Kubernetes / EKS - Python deployment code already using
from_source(...).deploy(...)
Since your deployment pattern is already modern, this checklist is focused on what can still break or drift during a 2.20.16 → 3.x upgrade. Relevant docs - Upgrade to Prefect 3 - Workers - Work pools - Deployments - Deploy via Python - Store flow code - States - Caching Targeted pre-flight checklist 1. Deployment creation environment Wherever your deployment code runs — CI job, release container, local automation, GitHub Actions, etc. — verify: - [ ] Prefect is pinned/upgraded to a 3.x version - [ ] Required integrations are installed there too: - [ ]
prefect-kubernetes
- [ ]
prefect-aws
if you use AWS APIs in deployment setup - [ ] The environment can still authenticate to Prefect Cloud - [ ] The environment can still import every module referenced by the deployment script - [ ] The environment can still resolve the flow entrypoint passed to
.from_source(...)
Recommended check Run your deployment script in non-prod and confirm it successfully creates or updates a deployment. 2. Worker runtime / image For each EKS worker image/deployment: - [ ] Worker image uses Prefect 3.x - [ ] Matching integrations are installed: - [ ]
prefect-kubernetes
- [ ]
prefect-aws
if needed - [ ] Worker pod starts successfully in EKS - [ ] Worker connects to the correct Prefect Cloud workspace - [ ] Worker registers against the correct work pool - [ ] No startup warnings/errors about missing packages, auth, or config Recommended check Upgrade one non-prod worker deployment first and watch startup logs. 3.
from_source(...)
validation
For each deployment script using
my_flow.from_source(...)
, confirm: - [ ]
source=
still points to the intended repo/storage location - [ ] credentials for source retrieval still work - [ ]
entrypoint=
exactly matches the flow file and flow function - [ ] referenced branch/tag/revision is explicit if reproducibility matters - [ ] no old storage-block assumption is hidden inside the source configuration Questions to answer - Is source code pulled from Git at runtime? - Or is code baked into the execution image? - If Git-based, where do credentials come from? - If image-based, is
.from_source(...)
still necessary in that case? 4.
deploy(...)
validation
For each
.deploy(...)
call, verify the arguments you pass are still intentional and tested. Check specifically: - [ ]
name=
- [ ]
work_pool_name=
- [ ]
work_queue_name=
if you use queues - [ ]
job_variables=
- [ ]
image=
if set here instead of in pool defaults - [ ] schedule-related args - [ ] tags, description, version, parameters if used Important If you rely on per-deployment Kubernetes overrides, make sure every important value is represented either: - in the work pool base job template, or - in deployment-level
job_variables
5. Kubernetes/EKS execution settings For the actual jobs launched by the worker, verify all execution-critical settings. Container/image - [ ] correct execution image is used - [ ] image exists in ECR or registry - [ ] tag/version is explicit enough for rollback/debugging - [ ] image pull policy is what you expect Kubernetes placement/config - [ ] namespace is correct - [ ] service account name is correct - [ ] node selectors still apply if used - [ ] tolerations still apply if used
- [ ] affinity rules still apply if used - [ ] labels/annotations still apply if used Resources - [ ] CPU requests/limits are correct - [ ] memory requests/limits are correct - [ ] ephemeral storage requirements are covered if relevant Secrets/env - [ ] environment variables still appear in job pods - [ ] Kubernetes secrets/configmaps still mount correctly - [ ] Prefect API-related env vars are correct - [ ] AWS region/account env vars are present where expected AWS / IRSA - [ ] pod assumes the intended IAM role - [ ] STS calls succeed from inside the job pod if relevant - [ ] S3/Secrets Manager/ECR access still works 6. Work pool configuration Inspect each work pool and verify: - [ ] pool type is still the intended Kubernetes type - [ ] base job template still matches your execution model - [ ] defaults for namespace/image/service account are still correct - [ ] deployment-level overrides do not conflict with pool defaults - [ ] concurrency limits, if any, are still appropriate Docs: - Work pools 7. Flow behavior changes to explicitly test Even if deployment infrastructure works, Prefect 3 changes some runtime behavior that is worth testing. Failure behavior / final state - [ ] test a task failure and confirm the flow ends in the state you expect - [ ] test exception propagation logic in your flows Docs: - States Caching behavior - [ ] identify side-effecting tasks - [ ] verify they do not unintentionally reuse cached results - [ ] disable caching for tasks that must always run, if needed Docs: - Caching Retries - [ ] test a retrying task - [ ] confirm retry delays and retry outcomes are still as expected Async/sync behavior - [ ] test any async flows/tasks directly if you use them 8. Deployment metadata / schedules If your Python deployment code also sets scheduling or metadata, verify: - [ ] schedules were recreated correctly - [ ] timezone behavior is correct - [ ] paused/active state is intentional after recreation - [ ] parameters defaults are correct - [ ] tags are still present for automation/routing/visibility 9. Observability / operations Make sure your operational path still works after the upgrade: - [ ] worker logs are visible - [ ] flow run logs are visible in Cloud - [ ] failed Kubernetes jobs are inspectable - [ ] alerts/automations tied to tags or states still fire as expected - [ ] on-call/debugging docs still match actual deployment behavior 10. Rollout safety Before production rollout: - [ ] validate one non-prod deployment creation - [ ] validate one successful non-prod run - [ ] validate one intentional failure - [ ] validate one flow using secrets/AWS access - [ ] validate one realistic production-sized run - [ ] keep old worker image tag available for fast rollback at the infrastructure level Suggested “single dry run” test matrix If you want the fastest confidence-building pass, I’d test these 5 scenarios: - [ ] simple happy path flow - [ ] failing task flow - [ ] retrying task flow - [ ] AWS-accessing flow - [ ] resource-heavy / longer-running flow Practical “go / no-go” criteria I’d consider you ready to roll when all of these are true: - [ ] deployment script on Prefect 3 successfully creates deployments - [ ] one upgraded EKS worker polls the right pool - [ ] one flow run launches a Kubernetes job correctly - [ ] the job can access required AWS resources/secrets - [ ] logs and states appear correctly in Prefect Cloud - [ ] failure and retry behavior look correct If you want, I can turn this into a copy/pasteable upgrade runbook for your team, or a line-by-line checklist specifically for EKS workers + deployment scripts.
s
@Marvin, what prefect docker image would you recommend for prefect 3?
m
thought for 94.3 seconds
For your setupCloud + EKS/Kubernetes workers + Python deployment code — I’d recommend: For worker containers
Copy code
text
prefecthq/prefect:3-latest-python3.12-kubernetes
For production, better to pin it Use a specific 3.x release instead of
3-latest
, for example:
Copy code
text
prefecthq/prefect:3.7.5-python3.12-kubernetes
Why this one - it’s the official Prefect 3 image - the
-kubernetes
variant is the best fit for EKS/Kubernetes execution - pinning a version gives you reproducibility and easier rollback My practical recommendation - non-prod / validation:
prefecthq/prefect:3-latest-python3.12-kubernetes
- prod:
prefecthq/prefect:<exact-version>-python3.12-kubernetes
So for a real rollout, I’d prefer something like:
Copy code
text
prefecthq/prefect:3.7.5-python3.12-kubernetes
Important caveat for your environment If your workers or flow-run pods need extra dependencies like: -
prefect-aws
-
boto3
- database drivers - internal libs - your own project package then I would not run the raw upstream image directly in production. Instead, build your own image from the official Prefect base image. Example:
Copy code
dockerfile
FROM prefecthq/prefect:3.7.5-python3.12-kubernetes

RUN pip install --no-cache-dir prefect-aws boto3

COPY . /opt/prefect/my-project
WORKDIR /opt/prefect/my-project
That is usually the best production pattern because it gives you: - pinned Prefect version - pinned Python base - your required dependencies - repeatable worker/runtime behavior Rule of thumb - worker image only needs Prefect + worker/integration deps if workers just submit K8s jobs - flow-run image needs everything the flow actually imports If the same image is used for both worker and run pods, include both sets of dependencies. Official images Prefect publishes official images here: - Prefect Docker Hub - Workers My recommendation in one line For EKS, start from:
Copy code
text
prefecthq/prefect:<pinned-3.x-version>-python3.12-kubernetes
and build your own derived image for production. If you want, I can also sketch: - a worker Dockerfile for EKS, or - a flow-run Dockerfile based on your current dependency stack.
s
@Marvin when upgrading pydantic should I be using pydantic or pydantic-core?
m
thought for 10.0 seconds
Use
pydantic
. You generally should not depend on
pydantic-core
directly unless you have a very specialized reason. Why -
pydantic
is the public library/API you use in application code -
pydantic-core
is the lower-level engine used internally by Pydantic v2 - Prefect 3 expects compatibility with Pydantic v2, meaning your models/code should work with the
pydantic
package’s v2 API So for an upgrade, your dependency should look like:
Copy code
text
pydantic>=2
not:
Copy code
text
pydantic-core
What to change in your code If you have custom models used with Prefect flows, parameters, or blocks, update them for Pydantic v2 syntax. Example:
Copy code
python
from pydantic import BaseModel, ConfigDict

class MyModel(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
instead of old v1-style config:
Copy code
python
from pydantic import BaseModel

class MyModel(BaseModel):
    class Config:
        arbitrary_types_allowed = True
Recommended reading - Upgrade to Prefect 3 - Pydantic migration guide Short answer - Use:
pydantic
- Do not switch your app dependency to:
pydantic-core
If you want, I can also give you a Prefect-focused Pydantic v1 → v2 checklist for deployment/flow parameter models.
s
@Marvin give me a Prefect-focused Pydantic v1 → v2 checklist for deployment/flow parameter models.
m
thought for 25.1 seconds
Absolutely — here’s a Prefect-focused Pydantic v1 → v2 checklist specifically for flow parameter models, deployment parameter models, and other models that interact with Prefect 3. Primary references - Upgrade to Prefect 3 - Pydantic migration guide --- Prefect-focused Pydantic v1 → v2 checklist 1. Upgrade the dependency correctly - [ ] depend on
pydantic>=2
- [ ] do not depend on
pydantic-core
directly unless you have a special low-level use case - [ ] make sure any libraries that define models used by your flows are also Pydantic v2-compatible Example:
Copy code
text
pydantic>=2,<3
--- 2. Inventory where Pydantic models touch Prefect Find all models used in any of these places: - [ ] flow parameters - [ ] task parameters - [ ] deployment parameter defaults - [ ] variables passed into
.deploy(parameters=...)
- [ ] block subclasses, if you have any custom ones - [ ] config objects returned by helper functions and then passed into flows/tasks - [ ] models serialized into artifacts, events, or result payloads A simple search target list: - [ ]
BaseModel
- [ ]
class Config
- [ ]
@validator
- [ ]
@root_validator
- [ ]
.dict(
- [ ]
.json(
- [ ]
parse_obj
- [ ]
from_orm
--- 3. Update model config syntax Pydantic v1:
Copy code
python
from pydantic import BaseModel

class Params(BaseModel):
    x: int

    class Config:
        arbitrary_types_allowed = True
        orm_mode = True
Pydantic v2:
Copy code
python
from pydantic import BaseModel, ConfigDict

class Params(BaseModel):
    x: int

    model_config = ConfigDict(
        arbitrary_types_allowed=True,
        from_attributes=True,
    )
Checklist - [ ] replace
class Config
with
model_config = ConfigDict(...)
- [ ] replace
orm_mode = True
with
from_attributes=True
- [ ] verify any other config keys against the v2 migration guide --- 4. Replace v1 validators Pydantic v1:
Copy code
python
from pydantic import BaseModel, validator

class Params(BaseModel):
    name: str

    @validator("name")
    def validate_name(cls, v):
        return v.strip()
Pydantic v2:
Copy code
python
from pydantic import BaseModel, field_validator

class Params(BaseModel):
    name: str

    @field_validator("name")
    @classmethod
    def validate_name(cls, v):
        return v.strip()
Checklist - [ ] replace
@validator
with
@field_validator
- [ ] replace
@root_validator
with
@model_validator
- [ ] re-test any validator that depended on v1 validation order or
values
- [ ] add
@classmethod
where appropriate --- 5. Replace root validators carefully Pydantic v1:
Copy code
python
from pydantic import BaseModel, root_validator

class Params(BaseModel):
    start: int
    end: int

    @root_validator
    def check_range(cls, values):
        if values["end"] < values["start"]:
            raise ValueError("end must be >= start")
        return values
Pydantic v2:
Copy code
python
from pydantic import BaseModel, model_validator

class Params(BaseModel):
    start: int
    end: int

    @model_validator(mode="after")
    def check_range(self):
        if self.end < self.start:
            raise ValueError("end must be >= start")
        return self
Checklist - [ ] replace
@root_validator
with
@model_validator
- [ ] decide whether
mode="before"
or
mode="after"
is correct - [ ] test cross-field validation thoroughly --- 6. Replace serialization methods used in deployment/flow code Many Prefect users serialize parameter models before passing them to deployments or APIs. Common v1 methods: -
.dict()
-
.json()
Preferred v2 methods: -
.model_dump()
-
.model_dump_json()
Example:
Copy code
python
params = Params(name="abc")

payload = params.model_dump()
payload_json = params.model_dump_json()
Checklist - [ ] replace
.dict()
with
.model_dump()
- [ ] replace
.json()
with
.model_dump_json()
- [ ] check whether you relied on old include/exclude behavior and retest it - [ ] if passing deployment parameters, ensure the dumped structure is JSON-serializable --- 7. Replace parsing helpers Pydantic v1:
Copy code
python
Params.parse_obj(data)
Pydantic v2:
Copy code
python
Params.model_validate(data)
Pydantic v1:
Copy code
python
Params.from_orm(obj)
Pydantic v2:
Copy code
python
Params.model_validate(obj)
with:
Copy code
python
model_config = ConfigDict(from_attributes=True)
Checklist - [ ] replace
parse_obj
with
model_validate
- [ ] replace
from_orm
with
model_validate(... )
+
from_attributes=True
- [ ] re-test object-based validation if you build parameters from class instances --- 8. Re-test flow parameter coercion and validation This is one of the most important Prefect-specific checks. If you use models like:
Copy code
python
from prefect import flow
from pydantic import BaseModel

class Params(BaseModel):
    customer_id: int
    dry_run: bool = False

@flow
def my_flow(params: Params):
    ...
test all of these: - [ ] direct Python invocation with a model instance - [ ] invocation with a plain dict - [ ] deployment-triggered run with default parameters - [ ] manual run from UI/API with supplied parameters - [ ] invalid parameter values - [ ] optional/missing fields - [ ] nested model fields - [ ] datetime/date fields - [ ] enum fields Why this matters: Prefect validates and serializes parameters for deployments and runs, so even small Pydantic behavior changes can show up here first. --- 9. Check deployment defaults that embed Pydantic models If you do something like:
Copy code
python
my_flow.deploy(
    name="prod",
    work_pool_name="eks-pool",
    parameters={
        "params": Params(customer_id=123).dict()
    },
)
update it to:
Copy code
python
my_flow.deploy(
    name="prod",
    work_pool_name="eks-pool",
    parameters={
        "params": Params(customer_id=123).model_dump()
    },
)
Checklist - [ ] replace model serialization in deployment creation code - [ ] verify deployed parameter defaults render correctly in Cloud - [ ] verify manual triggers from the UI still show/edit those parameters correctly --- 10. Check datetime, timezone, and enum fields carefully These types often surface subtle migration issues in deployed flows. For models used as flow parameters, test: - [ ] naive datetimes - [ ] timezone-aware datetimes - [ ] date-only values - [ ] enums - [ ]
Literal
values - [ ] nested optional fields Why: these types are often serialized through deployment metadata, UI forms, and API payloads. --- 11. Avoid passing raw non-serializable objects as parameters In Prefect 3, be especially careful that your flow/deployment parameters are API-safe. Bad candidates: - database sessions - boto clients - open file handles - custom classes without clear serialization - Kubernetes client objects Checklist - [ ] ensure flow/deployment parameter models contain serializable data - [ ] if arbitrary types are allowed, confirm they are not being passed through deployment/API boundaries - [ ] keep runtime-only objects out of deployment defaults A good pattern: - validate simple config data with Pydantic - construct clients/resources inside the flow/task at runtime --- 12. Test schema generation if you rely on the UI/API Prefect uses parameter schemas for deployments and UI forms. If you use Pydantic models as flow parameters: - [ ] confirm deployment creation succeeds - [ ] confirm parameter schema is generated - [ ] confirm the UI renders expected fields - [ ] confirm defaults/descriptions still show up as expected This is especially important for: - [ ] nested models - [ ] unions - [ ] enums - [ ] constrained types - [ ] custom field metadata --- 13. Revisit any custom JSON encoders / schema customization If your v1 models used advanced config like: -
json_encoders
- custom schema tweaks - aliases / population rules re-test them carefully in deployed parameter flows.
Checklist - [ ] verify alias behavior - [ ] verify custom serialization behavior - [ ] verify any schema customizations still affect UI/API as expected --- 14. Build a small Prefect-specific parameter test suite For each important parameter model, create a smoke test that covers: - [ ]
Model(...)
- [ ]
Model.model_validate({...})
- [ ]
Model.model_dump()
- [ ] pass into a flow directly - [ ] pass as deployment default parameters - [ ] trigger a deployment run and inspect received values A minimal example:
Copy code
python
from prefect import flow
from pydantic import BaseModel, ConfigDict

class Params(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    customer_id: int
    dry_run: bool = False

@flow
def test_flow(params: Params):
    print(params)

if __name__ == "__main__":
    p = Params.model_validate({"customer_id": 123})
    print(p.model_dump())
    test_flow(p)
--- 15. Prioritize these migration hotspots first If you want the fastest review, check these in order: 1. [ ]
class Config
2. [ ]
@validator
3. [ ]
@root_validator
4. [ ]
.dict()
5. [ ]
.json()
6. [ ]
parse_obj
7. [ ]
from_orm
8. [ ] nested models used as flow parameters 9. [ ] deployment default parameters built from models --- Common before/after example Before:
Copy code
python
from pydantic import BaseModel, validator

class RunConfig(BaseModel):
    customer_id: int
    env: str

    class Config:
        orm_mode = True

    @validator("env")
    def validate_env(cls, v):
        return v.lower()
After:
Copy code
python
from pydantic import BaseModel, ConfigDict, field_validator

class RunConfig(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    customer_id: int
    env: str

    @field_validator("env")
    @classmethod
    def validate_env(cls, v):
        return v.lower()
And if you serialize it for deployment defaults:
Copy code
python
parameters = {
    "config": RunConfig(customer_id=1, env="PROD").model_dump()
}
--- Recommended go/no-go criteria Before rolling your Prefect 3 upgrade, I’d want all of these true: - [ ] all Pydantic models used by flows import successfully under v2 - [ ] all validators/config were migrated off v1-only syntax - [ ] deployment creation works with model-backed parameters - [ ] deployed parameter schemas render properly in Prefect Cloud - [ ] one manual run and one scheduled/API-triggered run both validate correctly - [ ] datetime/nested/enum fields were tested if you use them If you want, I can also turn this into: - a repo search checklist with grep patterns, or - a before/after refactor guide for one of your actual parameter models.
s
@Marvin does the following environment variable
PREFECT_KUBERNETES_WORKER_STORE_PREFECT_API_IN_SECRET
still apply to Prefect v3.x.x? If this is no longer required what changes will be required.
m
thought for 149.1 seconds
Yes — it still applies in Prefect 3.x. Short answer -
PREFECT_KUBERNETES_WORKER_STORE_PREFECT_API_IN_SECRET
is still supported in Prefect 3 - you do not have to change it immediately during your upgrade - there is a newer preferred name for the same setting: -
PREFECT_INTEGRATIONS_KUBERNETES_WORKER_CREATE_SECRET_FOR_API_KEY
What it does This setting tells the Kubernetes worker to store the Prefect API key in a Kubernetes
Secret
and reference that secret from created job manifests, instead of embedding the API key directly in the pod/job environment. That’s still relevant for Prefect 3 Kubernetes workers. What I recommend For an upgrade from 2.20.16 → 3.x: - no immediate change required if your current setup works - but I would recommend updating to the newer variable name over time for clarity/future consistency So this is fine short-term:
Copy code
bash
PREFECT_KUBERNETES_WORKER_STORE_PREFECT_API_IN_SECRET=true
Preferred going forward:
Copy code
bash
PREFECT_INTEGRATIONS_KUBERNETES_WORKER_CREATE_SECRET_FOR_API_KEY=true
If you do nothing, what happens? Most likely: nothing breaks. The old name is still accepted for backward compatibility. If you switch to the new name, what changes are required? Usually just: 1. replace the env var name in your worker Deployment / Helm values / manifests 2. restart the worker pods 3. verify the worker can still create and reference secrets for launched jobs So, practically:
Copy code
bash
# old
PREFECT_KUBERNETES_WORKER_STORE_PREFECT_API_IN_SECRET=true

# new
PREFECT_INTEGRATIONS_KUBERNETES_WORKER_CREATE_SECRET_FOR_API_KEY=true
RBAC still matters If you use this behavior, your worker service account still needs permission to manage secrets in the target namespace(s), typically including: -
get
-
create
-
update
- sometimes
delete
on: -
secrets
So after upgrade, I would verify: - worker SA can still create secrets - launched jobs reference the secret correctly - no RBAC regressions in EKS Related newer settings Prefect 3 also supports more explicit secret configuration, including using an existing secret name/key in some setups, so if you want to tighten things further, there are newer integration-scoped Kubernetes worker settings around that. Bottom line - Does it still apply? Yes - Is it still required? If you want the API key stored in a Kubernetes Secret rather than inline, yes - Must you change it for Prefect 3? No - Should you eventually rename it? Yes, I would If you want, I can help you map your current worker env vars from old names to their recommended Prefect 3 equivalents for EKS.
s
@Marvin help me map your current worker env vars from old names to their recommended Prefect 3 equivalents for EKS.
m
thought for 206.4 seconds
Absolutely — for EKS workers, I’d split env vars into 3 buckets: 1. keep as-is 2. rename to the preferred Prefect 3 form 3. don’t use env vars for this anymore unless truly needed Also, one quick correction to avoid confusion: some of the “new in 3.x” Kubernetes settings are actually provided by the
prefect-kubernetes
integration package and may also exist in later 2.x worker setups. The most important thing for you is what to keep vs rename during the 2.20.16 → 3.x move. --- 1. Keep these as-is These are still the normal worker env vars in Prefect 3: | Current | Prefect 3 recommendation | Notes | |---|---|---| |
PREFECT_API_URL
|
PREFECT_API_URL
| no change | |
PREFECT_API_KEY
|
PREFECT_API_KEY
| no change | |
PREFECT_LOGGING_LEVEL
|
PREFECT_LOGGING_LEVEL
| no change | |
PREFECT_KUBERNETES_CLUSTER_UID
|
PREFECT_KUBERNETES_CLUSTER_UID
| keep only if you explicitly need it | When
PREFECT_KUBERNETES_CLUSTER_UID
is useful
- if your worker cannot read
kube-system
namespace metadata due to RBAC restrictions - otherwise you may not need to set it --- 2. Rename these to the preferred Prefect 3 names This is the biggest one from your earlier question. | Old/current | Recommended Prefect 3 name | Action | |---|---|---| |
PREFECT_KUBERNETES_WORKER_STORE_PREFECT_API_IN_SECRET
|
PREFECT_INTEGRATIONS_KUBERNETES_WORKER_CREATE_SECRET_FOR_API_KEY
| rename when convenient | Important: the old name still works, so this is not a forced day-1 migration. But I’d update it when you touch the worker manifest. Example:
Copy code
bash
# old
PREFECT_KUBERNETES_WORKER_STORE_PREFECT_API_IN_SECRET=true

# preferred
PREFECT_INTEGRATIONS_KUBERNETES_WORKER_CREATE_SECRET_FOR_API_KEY=true
--- 3. Consider adding these Prefect 3-era Kubernetes worker settings These are useful in EKS depending on your RBAC/security posture. Observer settings These help the Kubernetes observer watch pods/jobs and improve crash/event detection. | Setting | Recommendation | Why | |---|---|---| |
PREFECT_INTEGRATIONS_KUBERNETES_OBSERVER_ENABLED
| usually leave enabled | better Kubernetes event awareness | |
PREFECT_INTEGRATIONS_KUBERNETES_OBSERVER_NAMESPACES
| set explicitly | reduces RBAC scope and cluster-wide watching | For EKS, I’d usually recommend explicitly scoping namespaces if your runs are isolated, e.g.:
Copy code
bash
PREFECT_INTEGRATIONS_KUBERNETES_OBSERVER_ENABLED=true
PREFECT_INTEGRATIONS_KUBERNETES_OBSERVER_NAMESPACES=prefect,default
That’s usually better than broad cluster-wide observation. API key secret source settings If you want to use a pre-created secret instead of having the worker create one dynamically, Prefect 3 supports more explicit secret settings. Useful when security teams prefer centrally managed secrets. Examples:
Copy code
bash
PREFECT_INTEGRATIONS_KUBERNETES_WORKER_API_KEY_SECRET_NAME=my-prefect-api-key
PREFECT_INTEGRATIONS_KUBERNETES_WORKER_API_KEY_SECRET_KEY=api-key
Use these only if you want the worker to reference an existing secret instead of creating/managing one itself. --- 4. Do not use raw AWS credential env vars in EKS if you can avoid it For EKS, the recommended pattern is still IRSA, not: -
AWS_ACCESS_KEY_ID
-
AWS_SECRET_ACCESS_KEY
-
AWS_SESSION_TOKEN
So for Prefect 3 on EKS: - preferred: use service account + IAM role association - avoid: hardcoded AWS creds in worker env vars That means these are not really “Prefect 2 → 3 renamed vars”; they’re just generally not the preferred EKS pattern. --- 5. Things that should move out of worker env vars entirely If you currently use worker env vars to control flow-run job behavior, I’d review whether they belong instead in: - the work pool base job template, or - deployment-level
job_variables
Examples of things that usually belong there instead of the worker Deployment env: - run pod image - namespace for run jobs - service account for flow-run jobs - labels/annotations - CPU/memory requests
- node selectors/tolerations - environment for run pods That’s not strictly an env var rename, but it’s a good “Prefect 3 cleanup” step. --- Recommended mapping for your worker manifest If I were modernizing an EKS worker Deployment for Prefect 3, I’d expect something roughly like:
Copy code
bash
PREFECT_API_URL=...
PREFECT_API_KEY=...
PREFECT_LOGGING_LEVEL=INFO

# optional, only if needed
PREFECT_KUBERNETES_CLUSTER_UID=...

# preferred secret behavior
PREFECT_INTEGRATIONS_KUBERNETES_WORKER_CREATE_SECRET_FOR_API_KEY=true

# recommended if you want observer scoping
PREFECT_INTEGRATIONS_KUBERNETES_OBSERVER_ENABLED=true
PREFECT_INTEGRATIONS_KUBERNETES_OBSERVER_NAMESPACES=prefect
--- Quick old → new summary | Old/current | Recommended in Prefect 3 | Required? | |---|---|---| |
PREFECT_API_URL
|
PREFECT_API_URL
| yes | |
PREFECT_API_KEY
|
PREFECT_API_KEY
| yes | |
PREFECT_LOGGING_LEVEL
|
PREFECT_LOGGING_LEVEL
| optional | |
PREFECT_KUBERNETES_CLUSTER_UID
|
PREFECT_KUBERNETES_CLUSTER_UID
| only if needed | |
PREFECT_KUBERNETES_WORKER_STORE_PREFECT_API_IN_SECRET
|
PREFECT_INTEGRATIONS_KUBERNETES_WORKER_CREATE_SECRET_FOR_API_KEY
| recommended rename | | none |
PREFECT_INTEGRATIONS_KUBERNETES_OBSERVER_ENABLED
| recommended to review | | none |
PREFECT_INTEGRATIONS_KUBERNETES_OBSERVER_NAMESPACES
| recommended for EKS scoping | --- Practical migration advice For your upgrade, I’d do this: - must keep working now -
PREFECT_API_URL
-
PREFECT_API_KEY
- safe cleanup during upgrade - rename
PREFECT_KUBERNETES_WORKER_STORE_PREFECT_API_IN_SECRET
- add observer namespace scoping if you currently allow cluster-wide watch - review but don’t blindly copy forward - any AWS credential env vars - any env vars intended for run pods rather than worker pods --- Docs - Upgrade to Prefect 3 - Workers - Work pools - Run flows on Kubernetes If you want, send me your current worker Deployment env section and I can translate it into a recommended Prefect 3 EKS worker env block line by line.
s
@Marvin, how does Prefect 3 behaviour for
PrefectFuture.result()
work in async flows? Does the require updating when moving from Prefect 2.20.16 to Prefect 3.x.x
m
thought for 130.1 seconds
Yes — this does require review when moving from Prefect 2.20.16 to Prefect 3.x. Short version In Prefect 3: -
task.submit()
is synchronous even inside async flows -
PrefectFuture.result()
is also synchronous by default - so code like
await future.result()
from 2.x usually needs to change What changes from Prefect 2.20.16 → 3.x In 2.20.16, in async flows, you may have had patterns like:
Copy code
python
future = await my_task.submit()
result = await future.result()
In Prefect 3, the normal pattern is:
Copy code
python
future = my_task.submit()
result = future.result()
So yes, remove the
await
in those places. Recommended Prefect 3 pattern in async flows
Copy code
python
from prefect import flow, task

@task
async def my_task(x: int) -> int:
    return x * 2

@flow
async def my_flow():
    future = my_task.submit(5)
    result = future.result()
    return result
What will break if you keep old 2.x-style code If you keep doing this in Prefect 3:
Copy code
python
future = await my_task.submit()
result = await future.result()
you’re likely to get errors like: -
TypeError: object PrefectConcurrentFuture can't be used in 'await' expression
- or similar async/sync mismatch errors Important migration rule In Prefect 3 async flows: - use
future = task.submit(...)
- not
future = await task.submit(...)
and - use
result = future.result()
- not
result = await future.result()
What about async result methods? Prefect 3 does have async-style result helpers for some future types, but for normal migration purposes, the safest guidance is: - treat
submit()
as sync - treat
.result()
as sync If you have a very advanced pattern around distributed futures, there are async alternatives like
result_async()
/
aresult()
on some future types, but most application code migrating from 2.20.16 should not need those. Migration checklist for this specific area Search your codebase for: - [ ]
await .*\.submit\(
- [ ]
await .*\.result\(
- [ ] type hints or assumptions around async/sync
PrefectFuture
And update to: - [ ] remove
await
on
.submit()
- [ ] remove
await
on
.result()
Before / after example Before:
Copy code
python
@flow
async def my_flow():
    future = await some_task.submit(1)
    value = await future.result()
    return value
After:
Copy code
python
@flow
async def my_flow():
    future = some_task.submit(1)
    value = future.result()
    return value
Docs worth reading alongside this - Upgrade to Prefect 3 - Task runners - Flows - Tasks If you want, I can also give you a repo search/replace checklist specifically for common async Prefect 2 → 3 code patterns.
s
@Marvin for our flow we use
SequentialTaskRunner()
Copy code
@flow(
    name="my-flow",
    task_runner=SequentialTaskRunner(),
)
To keep our flow sequential, what do we need to do under Prefect v3.x.x?
m
thought for 80.3 seconds
To keep your flow sequential in Prefect 3, you should generally remove
SequentialTaskRunner()
. Why
SequentialTaskRunner
does not exist in Prefect 3. Instead, sequential behavior now comes from how you call tasks: - direct task calls -> sequential -
.submit()
-> concurrent via the task runner So if your flow is written like this:
Copy code
python
@flow(name="my-flow")
def my_flow():
    a = task_a()
    b = task_b(a)
    c = task_c(b)
    return c
then it will run sequentially without needing
SequentialTaskRunner()
. What to change Change this:
Copy code
python
from prefect import flow
from prefect.task_runners import SequentialTaskRunner

@flow(
    name="my-flow",
    task_runner=SequentialTaskRunner(),
)
def my_flow():
    ...
to this:
Copy code
python
from prefect import flow

@flow(name="my-flow")
def my_flow():
    ...
Important caveat This only preserves sequential execution if you are calling tasks directly, like:
Copy code
python
x = my_task()
y = other_task(x)
If you use `.submit()`:
Copy code
python
future = my_task.submit()
then you are opting into task-runner execution, and Prefect 3’s default runner is
ThreadPoolTaskRunner
, which is concurrent. So for a truly sequential flow, the main rule is: Do not use
.submit()
unless you intentionally want concurrent execution.
Migration rule for your case If your existing flow used
SequentialTaskRunner()
and direct task calls, the migration is easy: - remove
task_runner=SequentialTaskRunner()
- keep direct task invocation - test once to confirm behavior If you currently use
.submit()
but still wanted sequential behavior
Then you should refactor to direct calls. Before:
Copy code
python
@flow(name="my-flow", task_runner=SequentialTaskRunner())
def my_flow():
    a = task_a.submit()
    b = task_b.submit(wait_for=[a])
    return b.result()
After:
Copy code
python
@flow(name="my-flow")
def my_flow():
    a = task_a()
    b = task_b(a)
    return b
If you need ordering for tasks without data dependencies In cases where tasks don’t share outputs/inputs but still must run in order, Prefect 3 supports
wait_for=
with submitted tasks — but if your goal is simply sequential execution, direct calls are usually cleaner. Docs - Task runners - Tasks - Flows - Upgrade to Prefect 3 Bottom line For Prefect 3, to keep your flow sequential: - remove
SequentialTaskRunner()
- call tasks directly - avoid
.submit()
unless you want concurrency If you want, I can also help you review one of your actual flows and tell you whether it will remain sequential after the upgrade.
s
@Marvin we do have tasks that use
wait_for=
before they should run - also how should we handle tasks that return a state of
Failed()
or
Completed()
, do these still work without
.submit()
?
m
thought for 135.1 seconds
Yes — both
wait_for=
and returning
Failed()
/
Completed()
still work in Prefect 3, but there are some important behavioral details. Short version -
wait_for=
can still be used - tasks can still return
Completed()
/
Failed()
- they do not require
.submit()
- but if you want your flow to stay sequential, prefer direct task calls over
.submit()
- and be careful: returning a
Failed()
state from a task is not the same thing as raising an exception --- 1. Does
wait_for=
still work without
.submit()
?
Yes. You can use
wait_for=
on a direct task call if you need an ordering dependency that is not represented by passing data. Example:
Copy code
python
from prefect import flow, task

@task
def step_a():
    print("A")
    return "a"

@task
def step_b():
    print("B")
    return "b"

@task
def step_c():
    print("C")
    return "c"

@flow
def my_flow():
    a = step_a()
    b = step_b()
    c = step_c(wait_for=[a, b])
    return c
That said, if you are already calling tasks directly in sequence:
Copy code
python
a = step_a()
b = step_b()
c = step_c()
then
wait_for=
is often unnecessary because execution is already sequential. Practical rule - if there is a data dependency, pass the output normally - if there is only an ordering dependency,
wait_for=
is still appropriate --- 2. Do tasks returning
Failed()
or
Completed()
still work without
.submit()
?
Yes. A direct task call can still return a
State
object such as
Completed()
or
Failed()
. Example:
Copy code
python
from prefect import flow, task
from prefect.states import Failed, Completed

@task
def validate(x):
    if x < 0:
        return Failed(message="x must be non-negative")
    return Completed(data=x)

@flow
def my_flow():
    state = validate(-1)
    return state
So from a mechanics perspective, this still works with direct calls. --- 3. Important distinction: state return vs exception This is the part I would pay closest attention to during migration. If a task returns:
Copy code
python
return Failed(message="bad input")
that is not the same as:
Copy code
python
raise ValueError("bad input")
Returning a
Failed()
state gives you an explicit state object to inspect and branch on. It does not behave like a normal Python exception automatically bubbling up through your flow logic. So if your code relies on this pattern, keep checking it explicitly. Example:
Copy code
python
from prefect import flow, task
from prefect.states import Failed, Completed

@task
def validate(x):
    if x < 0:
        return Failed(message="x must be non-negative")
    return Completed(data=x)

@task
def process(x):
    return x * 2

@flow
def my_flow(x):
    validation_state = validate(x)

    if validation_state.is_failed():
        return validation_state

    result = process(validation_state.result())
    return result
That is a good sequential Prefect 3 pattern. --- 4. What should you do if you used
SequentialTaskRunner
+
wait_for=
before?
In Prefect 3, the recommended migration is: - remove
SequentialTaskRunner()
- keep direct task calls for sequential behavior - keep
wait_for=
only where you need ordering without passing data - keep explicit state checks if tasks return
Failed()
/
Completed()
So this old style:
Copy code
python
@flow(
    name="my-flow",
    task_runner=SequentialTaskRunner(),
)
def my_flow():
    a = task_a()
    b = task_b(wait_for=[a])
    return b
becomes:
Copy code
python
@flow(name="my-flow")
def my_flow():
    a = task_a()
    b = task_b(wait_for=[a])
    return b
or, if
b
logically depends on `a`’s output, even better:
Copy code
python
@flow(name="my-flow")
def my_flow():
    a = task_a()
    b = task_b(a)
    return b
--- 5. Recommended patterns in Prefect 3 Best for sequential flows with normal success/failure semantics Raise exceptions instead of returning
Failed()
unless you intentionally want to work with states as values. Example: ```python @task def validate(x):
if x < 0: raise ValueError("x must be non-negative") return x
Copy code
This is usually simpler if failure should actually fail the flow.

*Best for explicit orchestration / branching on task outcome*
Return `Completed()` / `Failed()` if you intentionally want the flow to inspect task states and decide what to do next.

Example:
```python
@task
def validate(x):
    if x < 0:
        return Failed(message="invalid input")
    return Completed(data=x)

@flow
def my_flow(x):
    state = validate(x)
    if state.is_failed():
        return state
    return next_step(state.result())
--- 6. What to watch for in your migration I would search for these patterns and review them carefully: -
task_runner=SequentialTaskRunner()
-
.submit(..., wait_for=...)
- direct task calls with
wait_for=...
-
return Failed(...)
-
return Completed(...)
And ask for each one: - Is this task intended to run sequentially? -> use direct call - Is
wait_for=
only enforcing ordering? -> okay to keep - Is
Failed()
meant to fail the flow, or just signal a state-like result? -> maybe change to
raise
--- My recommendation for your case Since you want to stay sequential: - remove
SequentialTaskRunner()
- use direct task calls - keep
wait_for=
only where there is no data dependency - keep state-returning tasks if you already intentionally branch on them - but consider converting some
return Failed(...)
patterns to exceptions if the desired behavior is “stop the flow and fail” Bottom line - Does
wait_for=
still work without
.submit()
?
Yes - Do
Failed()
/
Completed()
still work without
.submit()
?
Yes - Should you still use
.submit()
for a sequential flow?
Usually no - Do you need explicit state handling if tasks return
Failed()
/
Completed()
?
Yes If you want, I can help you refactor one concrete example flow from your current
SequentialTaskRunner + wait_for + state-returning task
pattern into the cleanest Prefect 3 version.
s
@Marvin do PrefectFutures work without
.submit()
. for example
Copy code
result_future: PrefectFuture[bool, Literal[False]] = get_credentials.submit( ... )
m
thought for 11.1 seconds
No — `PrefectFuture`s come from
.submit()
. If you do not use
.submit()
, you do not get a `PrefectFuture`; you get the task’s returned value directly. So in Prefect 3:
Copy code
python
future = get_credentials.submit(...)
returns a
PrefectFuture[...]
but:
Copy code
python
result = get_credentials(...)
returns the actual task result, not a future. So for your example:
Copy code
python
result_future: PrefectFuture[bool, Literal[False]] = get_credentials.submit(...)
the equivalent without
.submit()
would be something like:
Copy code
python
result: bool = get_credentials(...)
Important migration note for Prefect 3 That old type annotation also looks like a Prefect 2-style future type. In Prefect 3,
PrefectFuture
no longer uses the old two-parameter form like:
Copy code
python
PrefectFuture[bool, Literal[False]]
You should expect a single type parameter form instead, i.e. conceptually:
Copy code
python
PrefectFuture[bool]
So if you stay with
.submit()
, update the typing too. Practical rule - use
.submit()
-> you get a
PrefectFuture
- call task directly -> you get the result/value or returned
State
What this means for your sequential-flow migration If you want the flow to stay sequential, and you remove
.submit()
, then any code that currently does this:
Copy code
python
result_future = get_credentials.submit(...)
result = result_future.result()
should usually become:
Copy code
python
result = get_credentials(...)
No future involved. But be careful if you rely on future-specific behavior If your code uses the future for: -
wait_for=[future]
-
.result()
-
.wait()
- passing the future into other submitted tasks - concurrency orchestration then removing
.submit()
changes that pattern. You may need to replace it with: - direct data passing - direct sequential calls - or explicit handling of returned `State`s Examples With
.submit()
Copy code
python
credentials_future = get_credentials.submit(...)
ok = credentials_future.result()
Without
.submit()
Copy code
python
ok = get_credentials(...)
With a state-returning task and no
.submit()
Copy code
python
credentials_state = get_credentials(...)
if credentials_state.is_failed():
    return credentials_state
ok = credentials_state.result()
Bottom line - Do PrefectFutures work without
.submit()
?
No - Can you get a PrefectFuture from a direct task call? No - Should your old
PrefectFuture[bool, Literal[False]]
annotation be updated in Prefect 3?
Yes If you want, I can help you convert a small block of your current future-based sequential code into the correct Prefect 3 direct-call pattern.
s
@Marvin do I not need the
async
keyword for flows or tasks if the flow is sequential and not asynchronous?
m
thought for 11.0 seconds
Correct — if your flow is sequential and you are not using Python async code, then you usually do not need
async
on the flow or the tasks. Short version Use normal synchronous definitions:
Copy code
python
from prefect import flow, task

@task
def step_1():
    return 1

@task
def step_2(x):
    return x + 1

@flow
def my_flow():
    a = step_1()
    b = step_2(a)
    return b
That is the simplest and recommended pattern for a sequential Prefect 3 flow. When you do need
async
Use
async def
only if the function itself truly needs Python async behavior, for example if it does: -
await ...
- async HTTP/database clients - async libraries like
httpx.AsyncClient
,
asyncpg
, etc. Example:
Copy code
python
from prefect import flow, task

@task
async def fetch_data():
    ...

@flow
async def my_flow():
    data = await fetch_data()
    return data
But if you are not doing real async work, making tasks/flows async just adds complexity. For your migration, this is probably the right simplification If your old code used: -
SequentialTaskRunner()
-
.submit()
- async flow/task definitions - future/result orchestration but your actual intent is just run step A, then B, then C, then a cleaner Prefect 3 form is often:
Copy code
python
from prefect import flow, task

@task
def get_credentials() -> bool:
    return True

@task
def do_work(credentials_ok: bool):
    if not credentials_ok:
        raise ValueError("Missing credentials")
    return "done"

@flow(name="my-flow")
def my_flow():
    ok = get_credentials()
    result = do_work(ok)
    return result
Important distinction These are separate choices: - sync vs async Python functions - sequential vs concurrent task orchestration You can have: - sync + sequential - sync + concurrent - async + sequential - async + concurrent For your case, it sounds like you want: - sync + sequential which is the simplest setup. Migration rule of thumb If a flow/task: - does not use
await
- does not need async libraries - does not benefit from async I/O then convert it to plain
def
. Bottom line - No, you do not need
async
if the flow is sequential and not actually asynchronous - plain
def
for both flows and tasks is usually best in that case If you want, I can help you translate one of your current
async
Prefect 2 flows into a cleaner synchronous Prefect 3 version.
s
@Marvin for Prefect v2 the flow is declared
Copy code
@flow(
    name="generic-dbt-flow",
    task_runner=SequentialTaskRunner(),
    on_cancellation=[handle_cancelled],
    timeout_seconds=24 * 60 * 60,  # 24 hours
)
in Prefect v3
Copy code
task_runner=SequentialTaskRunner(),
no longer exists. There was tasks that are called from the flow and are declared
async def task_a
, others are declared
async def task_b
and use the decorator
@sync_compatible
, where others are
def task_c
. How should each of these be handled in Prefect 3?
m
thought for 20.3 seconds
For Prefect 3, I’d think about this in two separate layers: 1. flow orchestration style — sequential vs submitted/concurrent 2. Python function style
def
vs
async def
Since you want the flow to remain sequential, the main change is: - remove
task_runner=SequentialTaskRunner()
- prefer direct task calls instead of
.submit()
So your flow declaration becomes roughly:
Copy code
python
@flow(
    name="generic-dbt-flow",
    on_cancellation=[handle_cancelled],
    timeout_seconds=24 * 60 * 60,
)
def generic_dbt_flow():
    ...
or
async def
only if the flow body itself really needs async behavior. --- How to handle each task type 1.
def task_c
These are the easiest. If the task is already synchronous and does not need async I/O, keep it as:
Copy code
python
@task
def task_c(...):
    ...
And call it directly from the flow:
Copy code
python
result = task_c(...)
No special migration needed. --- 2.
async def task_a
For these, ask: Does the task actually need to be async? Keep it
async def
only if it does real async work, for example: -
await
calls - async DB client - async HTTP client - async subprocess wrapper - async SDK/library If it does not actually need async behavior, simplify it to plain
def
. Recommended choices If truly async:
Copy code
python
@task
async def task_a(...):
    ...
Then the calling flow should also be
async def
, and you should call/await it normally. Example:
Copy code
python
@flow(
    name="generic-dbt-flow",
    on_cancellation=[handle_cancelled],
    timeout_seconds=24 * 60 * 60,
)
async def generic_dbt_flow():
    a = await task_a(...)
    c = task_c(a)
    return c
If not truly async: convert it to:
Copy code
python
@task
def task_a(...):
    ...
This is usually preferable for a sequential flow. --- 3.
async def task_b
with
@sync_compatible
This is the area I’d review most carefully. In Prefect 3, if your goal is a simple sequential flow, I would generally avoid relying on
@sync_compatible
as a core orchestration pattern unless you really need it for dual sync/async calling semantics. Ask: - Is the underlying implementation actually async? - Is
@sync_compatible
only there to make it callable from sync code in Prefect 2? - Can the task now just be made fully sync? - Or should the whole flow become async? Recommended handling Case A: task doesn’t truly need async Convert to plain sync:
Copy code
python
@task
def task_b(...):
    ...
This is the cleanest outcome. Case B: task truly does async work Then keep it async, and make the flow async too:
Copy code
python
@task
async def task_b(...):
    ...

@flow(
    name="generic-dbt-flow",
    on_cancellation=[handle_cancelled],
    timeout_seconds=24 * 60 * 60,
)
async def generic_dbt_flow():
    b = await task_b(...)
    return b
In other words, for Prefect 3 I would usually prefer: - fully sync path, or - fully async path rather than a mixed “async task plus sync-compatible shim” design unless you have a strong reason to preserve it. --- Practical migration guidance for your mix of task types I’d classify each task into one of these buckets: Bucket 1: no real async operations - convert to
def
- call directly - best fit for sequential Prefect 3 flow Bucket 2: real async operations, but can be isolated - keep
async def
- if even one or two core tasks are truly async and are called directly from the flow, strongly consider making the flow itself
async def
Bucket 3: async only because of old Prefect 2 patterns - likely convert to
def
- remove
@sync_compatible
if no longer needed --- Very important Prefect 3 rule In Prefect 3, you should not rely on old “magic” async bridging assumptions. Particularly: - sync flows cannot freely call async tasks the old 2.x way - if a task is async and you are calling it directly, the flow should generally be async too So if you keep
async def task_a
/
task_b
, then I would generally recommend: ```python
@flow(...) async def generic_dbt_flow(): a = await task_a(...) b = await task_b(...) c = task_c(...) return c
Copy code
If you convert the async tasks to sync, then:

```python
@flow(...)
def generic_dbt_flow():
    a = task_a(...)
    b = task_b(...)
    c = task_c(...)
    return c
--- What I would recommend for your dbt flow specifically For a dbt-oriented flow, many teams do not actually need Python async unless they are using async clients around APIs or orchestration helpers. So my default recommendation would be: - remove
SequentialTaskRunner()
- convert unnecessary
async def
tasks to
def
- remove unnecessary
@sync_compatible
- make the flow plain
def
- call tasks directly for sequential behavior This gives the cleanest Prefect 3 migration. --- Suggested migration decision table | Current task form | Keep as-is? | Prefect 3 recommendation | |---|---|---| |
@task def task_c(...)
| yes | keep as
def
| |
@task async def task_a(...)
with real async work | maybe | keep async, make flow async | |
@task async def task_a(...)
without real async work | no | convert to
def
| |
@task @sync_compatible async def task_b(...)
with no strong reason for dual mode | no | convert to plain
def
| |
@task @sync_compatible async def task_b(...)
with real async work | maybe | keep async, prefer async flow | --- Likely target patterns Target pattern A — simplest, all sync
Copy code
python
from prefect import flow, task

@task
def task_a(...):
    ...

@task
def task_b(...):
    ...

@task
def task_c(...):
    ...

@flow(
    name="generic-dbt-flow",
    on_cancellation=[handle_cancelled],
    timeout_seconds=24 * 60 * 60,
)
def generic_dbt_flow():
    a = task_a(...)
    b = task_b(...)
    c = task_c(...)
    return c
Target pattern B — mixed but truly async
Copy code
python
from prefect import flow, task

@task
async def task_a(...):
    ...

@task
async def task_b(...):
    ...

@task
def task_c(...):
    ...

@flow(
    name="generic-dbt-flow",
    on_cancellation=[handle_cancelled],
    timeout_seconds=24 * 60 * 60,
)
async def generic_dbt_flow():
    a = await task_a(...)
    b = await task_b(...)
    c = task_c(...)
    return c
--- My recommendation in one sentence For Prefect 3, remove
SequentialTaskRunner()
, then simplify as much as possible toward plain synchronous
def
tasks and a synchronous flow unless the async tasks are doing real async I/O.
If you want, paste a small example of: - one
async def task_a
- one
@sync_compatible async def task_b
- one
def task_c
and I can tell you exactly which ones I’d convert to sync vs keep async for Prefect 3.
s
@Marvin even with
async def my_flow
can the flow and tasks still be made to execute sequentially?
m
thought for 11.4 seconds
Yes. An
async def
flow can still execute sequentially in Prefect 3. What determines sequential vs concurrent execution is not just
async def
— it’s mainly how you call the tasks. Sequential in an async flow If you call tasks one at a time and await them in order, execution stays sequential. Example:
Copy code
python
from prefect import flow, task

@task
async def task_a():
    print("a")
    return 1

@task
async def task_b(x):
    print("b")
    return x + 1

@flow
async def my_flow():
    a = await task_a()
    b = await task_b(a)
    return b
That is sequential: -
task_a
runs first - only after it completes does
task_b
run Sequential with sync tasks inside async flow Also valid:
Copy code
python
from prefect import flow, task

@task
def task_a():
    return 1

@task
def task_b(x):
    return x + 1

@flow
async def my_flow():
    a = task_a()
    b = task_b(a)
    return b
That is also sequential. What introduces concurrency Concurrency usually comes from: -
.submit()
- spawning multiple async operations at once - patterns like
asyncio.gather(...)
Example of not sequential:
Copy code
python
f1 = task_a.submit()
f2 = task_b.submit()
or
Copy code
python
a, b = await asyncio.gather(task_a(), task_b())
Those are concurrent patterns. So the rule is -
async def flow
does not automatically mean concurrent - ordered direct calls / ordered awaits can still be fully sequential For your migration If you need to keep some truly async tasks, it is fine to make the flow
async def
and still keep the execution sequential by: - calling each task in order - awaiting async tasks in order - avoiding
.submit()
unless you intentionally want concurrency Example mixed sequential flow
Copy code
python
from prefect import flow, task

@task
async def task_a():
    return "a"

@task
async def task_b(a):
    return f"{a}-b"

@task
def task_c(b):
    return f"{b}-c"

@flow
async def my_flow():
    a = await task_a()
    b = await task_b(a)
    c = task_c(b)
    return c
This is still sequential. Bottom line Yes — an
async def
flow can absolutely remain sequential in Prefect 3, as long as you: - execute tasks in order - await async tasks one-by-one - avoid
.submit()
/ other concurrent patterns unless needed If you want, I can give you a Prefect 3 sequential async-flow template for your dbt flow style.
s
@Marvin, what are the changes between Prefect 2 and Prefect 3
flow.deploy(..)
as from my understanding
is_schedule_active
has been removed in Prefect 3?
m
thought for 179.1 seconds
Yes — your understanding is correct:
is_schedule_active
has been removed
in Prefect 3. For
flow.deploy(...)
, the biggest migration point is that schedule activation is now handled per schedule, instead of with a separate deployment-level
is_schedule_active
flag. --- Short version In Prefect 2 you may have had something like:
Copy code
python
await my_flow.deploy(
    name="my-deployment",
    work_pool_name="my-pool",
    cron="0 0 * * *",
    is_schedule_active=False,
)
In Prefect 3, that becomes something like:
Copy code
python
from prefect.schedules import Cron

my_flow.deploy(
    name="my-deployment",
    work_pool_name="my-pool",
    schedule=Cron("0 0 * * *", active=False),
)
So: -
is_schedule_active
-> removed - replacement ->
Schedule(..., active=...)
or helpers like
Cron(..., active=...)
--- Important
flow.deploy(...)
changes from Prefect 2 → 3
1.
is_schedule_active
removed
This is the one you already noticed. Old:
Copy code
python
is_schedule_active=False
New:
Copy code
python
schedule=Cron("0 0 * * *", active=False)
or for multiple schedules:
Copy code
python
schedules=[
    Cron("0 0 * * *", active=True),
    Interval(3600, active=False),
]
--- 2.
deploy()
is sync in Prefect 3
A big practical difference: In Prefect 2, many people used:
Copy code
python
deployment_id = await my_flow.deploy(...)
In Prefect 3:
Copy code
python
deployment_id = my_flow.deploy(...)
So if your deployment code currently uses
await flow.deploy(...)
, remove the
await
. --- 3.
schedule
/
schedules
are richer
In Prefect 3, the schedule objects are more explicit. Current supported deploy signature includes: -
interval=
-
cron=
-
rrule=
-
schedule=
-
schedules=
-
paused=
and
schedule
/
schedules
use
prefect.schedules
. Docs-related concepts: - Deployments Examples:
Copy code
python
from prefect.schedules import Cron, Interval

my_flow.deploy(
    name="daily",
    work_pool_name="my-pool",
    schedule=Cron("0 0 * * *", active=True),
)
or:
Copy code
python
my_flow.deploy(
    name="multi",
    work_pool_name="my-pool",
    schedules=[
        Cron("0 0 * * *", active=True),
        Interval(3600, active=False),
    ],
)
--- 4.
paused
is not the same as
is_schedule_active
This is an easy migration trap. In Prefect 3,
paused
still exists on
flow.deploy(...)
, but it is not a direct replacement for
is_schedule_active
. -
paused=True
pauses the deployment -
active=False
disables a specific schedule So if you previously used:
Copy code
python
is_schedule_active=False
the closest replacement is not
paused=True
unless your actual intent was “disable the whole deployment from scheduling/running.” Usually the right replacement is:
Copy code
python
schedule=Cron(..., active=False)
--- 5.
job_variables
remains the Kubernetes/EKS customization hook
Since you’re on EKS, this is still important. In Prefect 3,
flow.deploy(...)
still supports:
Copy code
python
job_variables={...}
That’s the main place to override Kubernetes execution settings per deployment. Example:
Copy code
python
my_flow.deploy(
    name="generic-dbt-flow",
    work_pool_name="eks-pool",
    job_variables={
        "namespace": "prefect",
        "service_account_name": "prefect-worker",
        "image": "<http://123456789012.dkr.ecr.us-east-1.amazonaws.com/dbt:prod|123456789012.dkr.ecr.us-east-1.amazonaws.com/dbt:prod>",
    },
)
--- 6.
version_type
was added
Prefect 3 includes:
Copy code
python
version_type=...
This is new relative to older 2.x usage. You may not need it, but it’s part of the signature now. --- 7.
enforce_parameter_schema
is still present
This remains configurable and matters if you use model-backed flow parameters. --- 8.
from_source(...).deploy(...)
still fits your current pattern
Since you already use:
Copy code
python
my_flow_instance = my_flow.from_source(...)
deployment_id = my_flow_instance.deploy(...)
you’re already in the right conceptual model for Prefect 3. Most of your changes are likely: - removing any
await
around
deploy(...)
- replacing
is_schedule_active
- validating schedule objects/arguments --- Practical before/after examples Prefect 2 style
Copy code
python
deployment_id = await my_flow.from_source(
    source=...,
    entrypoint="flows.py:my_flow",
).deploy(
    name="generic-dbt-flow",
    work_pool_name="eks-pool",
    cron="0 0 * * *",
    is_schedule_active=False,
    parameters={"env": "prod"},
)
Prefect 3 style
Copy code
python
from prefect.schedules import Cron

deployment_id = my_flow.from_source(
    source=...,
    entrypoint="flows.py:my_flow",
).deploy(
    name="generic-dbt-flow",
    work_pool_name="eks-pool",
    schedule=Cron("0 0 * * *", active=False),
    parameters={"env": "prod"},
)
--- If you used the shorthand
cron=
,
interval=
, or
rrule=
Those are still present in Prefect 3, but if you need active/inactive behavior, the more explicit schedule-object form is usually the clearest. For example, this:
Copy code
python
my_flow.deploy(
    name="x",
    work_pool_name="pool",
    cron="0 0 * * *",
)
is okay. But if you want to control schedule activation, use:
Copy code
python
from prefect.schedules import Cron

my_flow.deploy(
    name="x",
    work_pool_name="pool",
    schedule=Cron("0 0 * * *", active=False),
)
--- Migration checklist for your
flow.deploy(...)
code
Search for these patterns: - [ ]
await ...deploy(
- [ ]
is_schedule_active=
- [ ] any old assumptions that
paused
means “inactive schedule” - [ ] any use of
cron=
,
interval=
,
rrule=
where you now want per-schedule
active
Then update to: - [ ] remove
await
from
.deploy(...)
- [ ] replace
is_schedule_active=
with
schedule=
/
schedules=
using
active=...
- [ ] use
paused=
only if you want the deployment paused --- Docs / references - Upgrade to Prefect 3 - Deployments Bottom line The main
flow.deploy(...)
changes for you are: -
is_schedule_active
is removed - use
Cron(...)
,
Interval(...)
,
RRule(...)
, or
Schedule(...)
with
active=...
-
deploy()
is sync, so remove
await
-
paused
is not a drop-in replacement for
is_schedule_active
If you want, I can help convert one of your actual Prefect 2
deploy(...)
calls into the exact Prefect 3 equivalent.
s
@Marvin what are the changes required for the job_manifest in regards to infra_overrides?
m
thought for 114.5 seconds
Yes — this is one of the bigger conceptual changes in the Kubernetes migration. Short version In Prefect 3: -
infra_overrides
is replaced by
job_variables
- the old infrastructure block’s
job_manifest
is no longer something you attach to a deployment directly - instead, the
job_manifest
lives in the work pool’s
base_job_template
- deployment-time customization happens through
job_variables
So the mapping is: - Prefect 2:
KubernetesJob.job_manifest
+ deployment
infra_overrides
- Prefect 3: work pool
base_job_template.job_configuration.job_manifest
+ deployment
job_variables
--- How to think about the migration In Prefect 2 you effectively had: 1. a
KubernetesJob
infrastructure block with defaults and a
job_manifest
2.
infra_overrides
on the deployment to override pieces of that config In Prefect 3 you instead have: 1. a Kubernetes work pool with a
base_job_template
2.
job_variables
on the deployment to override the template variables So the most important migration change is: You no longer override the infrastructure block directly; you override the work pool template variables. --- What happens to
job_manifest
?
job_manifest
still exists conceptually for Kubernetes workers, but it is configured in the work pool. Specifically, it lives under the work pool’s advanced/base template configuration, typically as:
Copy code
json
{
  "variables": { ... },
  "job_configuration": {
    "job_manifest": { ... }
  }
}
So if you had a custom
job_manifest
in Prefect 2, you should move that logic into the work pool’s base job template. --- What happens to
infra_overrides
?
infra_overrides
becomes
job_variables
. Example: Prefect 2 style
Copy code
python
Deployment.build_from_flow(
    my_flow,
    name="my-deployment",
    infrastructure=KubernetesJob.load("my-k8s-job"),
    infra_overrides={
        "namespace": "production",
        "image": "my-image:latest",
        "image_pull_policy": "Always",
    },
)
Prefect 3 style
Copy code
python
my_flow.from_source(
    source=...,
    entrypoint="flows.py:my_flow",
).deploy(
    name="my-deployment",
    work_pool_name="my-k8s-pool",
    job_variables={
        "namespace": "production",
        "image": "my-image:latest",
        "image_pull_policy": "Always",
    },
)
--- The key migration requirement for custom manifests If your old
infra_overrides
keys referenced values used in the manifest, then in Prefect 3 those values must be: 1. declared in the work pool template
variables
2. referenced in the
job_configuration.job_manifest
using
{{ variable_name }}
3. overridden via
job_variables
at deploy time if needed That is the main structural requirement. --- Example: old custom manifest + infra_overrides Suppose in Prefect 2 you had a custom manifest like this in `KubernetesJob`:
Copy code
python
job_manifest = {
    "apiVersion": "batch/v1",
    "kind": "Job",
    "metadata": {
        "namespace": "{{ namespace }}"
    },
    "spec": {
        "template": {
            "spec": {
                "serviceAccountName": "{{ service_account_name }}",
                "containers": [
                    {
                        "name": "prefect-job",
                        "image": "{{ image }}",
                        "imagePullPolicy": "{{ image_pull_policy }}"
                    }
                ]
            }
        }
    }
}
and then:
Copy code
python
infra_overrides={
    "namespace": "prod",
    "service_account_name": "prefect-runner",
    "image": "myrepo/dbt:1.2.3",
    "image_pull_policy": "Always",
}
In Prefect 3, your work pool template should explicitly declare those variables and reference them in the manifest. Work pool base template conceptually ```json { "variables": { "namespace": { "type": "string", "default": "default" }, "service_account_name": { "type": "string", "default": "prefect-worker" }, "image": { "type": "string",
"default": null }, "image_pull_policy": { "type": "string", "default": "IfNotPresent" } }, "job_configuration": { "job_manifest": { "apiVersion": "batch/v1", "kind": "Job", "metadata": { "namespace": "{{ namespace }}" }, "spec": { "template": { "spec": { "serviceAccountName": "{{ service_account_name }}", "containers": [ { "name": "prefect-job", "image": "{{ image }}", "imagePullPolicy": "{{ image_pull_policy }}" } ] } } } } } }
Copy code
Then your deployment code becomes:

```python
my_flow.from_source(
    source=...,
    entrypoint="flows.py:my_flow",
).deploy(
    name="my-deployment",
    work_pool_name="my-k8s-pool",
    job_variables={
        "namespace": "prod",
        "service_account_name": "prefect-runner",
        "image": "myrepo/dbt:1.2.3",
        "image_pull_policy": "Always",
    },
)
--- What changes are actually required from you? For your migration, I’d do this: 1. Inventory current
infra_overrides
keys
Make a list of every key you currently pass, for example: -
namespace
-
image
-
service_account_name
-
env
-
labels
-
annotations
-
finished_job_ttl
- CPU/memory values - node selector / tolerations / affinity values 2. Check whether each key exists in the new work pool template If yes: - pass it via
job_variables
If not: - add it to the work pool’s
variables
- wire it into
job_configuration.job_manifest
3. Move deployment overrides from
infra_overrides
to
job_variables
This is the code change in your deployment scripts. 4. Validate the resulting Kubernetes Job manifest in a non-prod pool This is important because the work pool template variable wiring is where migration mistakes show up. --- Common mapping examples | Prefect 2
infra_overrides
| Prefect 3 replacement | |---|---| |
infra_overrides={"namespace": "prod"}
|
job_variables={"namespace": "prod"}
| |
infra_overrides={"image": "repo/image:tag"}
|
job_variables={"image": "repo/image:tag"}
| |
infra_overrides={"image_pull_policy": "Always"}
|
job_variables={"image_pull_policy": "Always"}
| |
infra_overrides={"env": {...}}
|
job_variables={"env": {...}}
| |
infra_overrides
affecting custom manifest fields | add work pool variable + reference in
job_manifest
+ override with
job_variables
| --- Biggest migration gotcha In Prefect 3, defining a variable in the work pool is not enough. You must also reference it inside the template, e.g.:
Copy code
json
"namespace": "{{ namespace }}"
If the variable is not wired into
job_manifest
, then passing it in
job_variables
won’t do anything. That’s the most common thing I’d double-check. --- What I would recommend for your team Because you’re on EKS + Python deployment code, the cleanest approach is: - keep the work pool responsible for the base Kubernetes manifest - use
job_variables
only for per-deployment changes - avoid making every deployment carry a huge manifest override So: - stable defaults -> work pool
base_job_template
- deployment-specific values ->
job_variables
--- Bottom line The required changes are: - replace
infra_overrides
with
job_variables
- move any custom
job_manifest
logic into the work pool’s
base_job_template
- make sure all overridable values are declared in
variables
- make sure those variables are actually referenced in
job_configuration.job_manifest
Docs - Work pools - Deploy via Python - Run flows on Kubernetes - Upgrade to Prefect 3 If you want, paste one of your current:
-
job_manifest
-
infra_overrides
and I can translate it into the exact Prefect 3 work pool template +
job_variables
form
.
s
@Marvin, how much has the helm chart changed between Prefect v2 and Prefect v3. What would be the recommended way to compare the changes?
m
thought for 159.0 seconds
There have been meaningful Helm chart changes between the Prefect 2 era and Prefect 3 era, but the impact depends a lot on which chart you’re using today. For your setupCloud + workers + EKS — the key question is: - are you already using the
prefect-worker
Helm chart? - or are you still on an older agent chart / older worker chart revision? If you're already on the worker chart, this is usually more of a careful values diff and render diff exercise than a full architecture migration. If you’re still on an agent chart, that’s a bigger breaking change because the agent chart is gone and you should be on
prefect-worker
. --- Recommended way to compare the changes The safest approach is to compare at three levels: 1. your current release values 2. old chart defaults vs new chart defaults 3. rendered Kubernetes manifests That gives you both: - config drift visibility - actual workload/RBAC/resource changes --- Recommended comparison workflow 1. Export your current Helm values Start with what is actually deployed:
Copy code
bash
helm get values <release-name> -n <namespace> > current-values.yaml
helm get values -a <release-name> -n <namespace> > current-values-all.yaml
Use: -
current-values.yaml
for your explicit overrides -
current-values-all.yaml
to understand the fully resolved release config --- 2. Download/inspect the target chart defaults Get the default values for the new chart version you want to upgrade to:
Copy code
bash
helm inspect values prefect/prefect-worker --version <target-chart-version> > new-defaults.yaml
If you want to compare old and new defaults:
Copy code
bash
helm inspect values prefect/prefect-worker --version <old-chart-version> > old-defaults.yaml
helm inspect values prefect/prefect-worker --version <target-chart-version> > new-defaults.yaml
diff -u old-defaults.yaml new-defaults.yaml
This is one of the best ways to see renamed keys, added sections, and deleted defaults. --- 3. Compare rendered manifests This is the most important practical check. Render the old and new versions:
Copy code
bash
helm template prefect-worker prefect/prefect-worker \
  --version <old-chart-version> \
  -f current-values.yaml > old-rendered.yaml

helm template prefect-worker prefect/prefect-worker \
  --version <target-chart-version> \
  -f current-values.yaml > new-rendered.yaml

diff -u old-rendered.yaml new-rendered.yaml
This shows the actual Kubernetes differences, including: - Deployment changes - ServiceAccount changes - RBAC changes - init containers - env vars - probes - annotations - mounts - image references For EKS, this is especially important. --- 4. Use
helm diff
before upgrading
This is probably the best “near-real” upgrade preview. Install plugin:
Copy code
bash
helm plugin install <https://github.com/databus23/helm-diff>
Then run:
Copy code
bash
helm diff upgrade <release-name> prefect/prefect-worker \
  --namespace <namespace> \
  --version <target-chart-version> \
  -f current-values.yaml \
  --suppress-secrets
This shows what Helm would change in-place, which is usually the most actionable comparison. --- 5. Also compare chart README / values schema For chart-level migration questions, also compare: - chart
values.yaml
- README / chart docs - release notes / repo history if needed Useful links: - Prefect Helm repo - Prefect Helm chart registry - prefect-worker chart - Helm docs --- What tends to change most between older and newer worker charts For EKS/worker deployments, I would pay special attention to: - worker config structure - base job template / work pool sync behavior - API key propagation / secret behavior - RBAC rules - ServiceAccount annotations for IRSA - namespace scoping / observer settings
- init containers - health probes - image defaults / tags - env var naming and placement --- For your EKS setup, review these areas closely 1. ServiceAccount / IRSA Check if your chart values still correctly set: - service account name -
<http://eks.amazonaws.com/role-arn|eks.amazonaws.com/role-arn>
- any other AWS annotations 2. Worker env vars Make sure your Prefect env vars still land where expected, especially Kubernetes integration settings like: -
PREFECT_API_URL
-
PREFECT_API_KEY
-
PREFECT_INTEGRATIONS_KUBERNETES_WORKER_CREATE_SECRET_FOR_API_KEY
- observer settings - cluster UID if used 3. Work pool association Confirm the chart still points the worker at the intended pool and type. 4. Base job template behavior If the chart version introduces/changes automatic work pool template syncing, validate that carefully. 5. RBAC scope Especially in EKS, compare: - Role / ClusterRole rules - secret permissions - pod/job watch permissions - namespace watch scope --- My recommended migration method for your team Since you’re already on workers, I would do this: 1. export current values 2. inspect target chart defaults 3. run
helm template
old vs new 4. run
helm diff upgrade
5. create a cleaned-up new values file instead of blindly reusing an old one 6. test in non-prod namespace/cluster first That “cleaned-up values file” point matters a lot — older values files often carry forward stale keys that no longer do anything or have changed meaning. --- Practical command checklist
Copy code
bash
# current release values
helm get values <release> -n <ns> > current-values.yaml
helm get values -a <release> -n <ns> > current-values-all.yaml

# inspect chart defaults
helm inspect values prefect/prefect-worker --version <old-version> > old-defaults.yaml
helm inspect values prefect/prefect-worker --version <new-version> > new-defaults.yaml

# compare defaults
diff -u old-defaults.yaml new-defaults.yaml

# compare rendered output
helm template <release> prefect/prefect-worker \
  --version <old-version> \
  -f current-values.yaml > old-rendered.yaml

helm template <release> prefect/prefect-worker \
  --version <new-version> \
  -f current-values.yaml > new-rendered.yaml

diff -u old-rendered.yaml new-rendered.yaml

# preview upgrade
helm diff upgrade <release> prefect/prefect-worker \
  --namespace <ns> \
  --version <new-version> \
  -f current-values.yaml \
  --suppress-secrets
--- Bottom line The recommended way to compare Helm chart changes is: - values diff for config-level changes - rendered manifest diff for actual Kubernetes changes -
helm diff upgrade
for release-level preview That combination is much better than just eyeballing
values.yaml
. If you want, I can help you make a Prefect Helm upgrade review checklist specifically for EKS workers, or help interpret a diff if you paste part of your current values file.
s
@Marvin where do I get the version of the helm chart from?
m
thought for 9.5 seconds
You can get the Helm chart version a few ways. If you want the version currently installed in your cluster Run:
Copy code
bash
helm list -n <namespace>
or more specifically:
Copy code
bash
helm status <release-name> -n <namespace>
That will show the deployed chart version and often the app version. You can also use:
Copy code
bash
helm get metadata <release-name> -n <namespace>
--- If you want to see available Prefect chart versions First add/update the repo if needed:
Copy code
bash
helm repo add prefect <https://prefecthq.github.io/prefect-helm>
helm repo update
Then list versions:
Copy code
bash
helm search repo prefect/prefect-worker --versions
That will show available chart versions for the worker chart. If you also want server chart versions:
Copy code
bash
helm search repo prefect/prefect-server --versions
--- If you want to inspect a specific chart version Example:
Copy code
bash
helm show chart prefect/prefect-worker --version <chart-version>
This shows metadata including: - chart version - app version --- Important distinction There are two versions to keep straight: - chart version — the Helm package version - app version — the Prefect version the chart is oriented around They are not always the same thing. So when planning an upgrade, I’d check both: - what chart version are we on now? - what Prefect image/app version are we actually deploying? --- Useful links - Prefect Helm chart registry - Prefect Helm repo Most likely command you want right now If you already know the release name:
Copy code
bash
helm status <release-name> -n <namespace>
If you want, I can also help you interpret: -
CHART
-
APP VERSION
- and how they relate to your Prefect 2.20.16 → 3.x migration.
s
@Marvin can prefect v2 and prefect v3 workers be deployed to the same kubernetes namespace?
m
thought for 165.5 seconds
Yes — they can be deployed to the same Kubernetes namespace. But there’s an important distinction: - same Kubernetes namespace → generally yes - same Prefect work pool / same control path → generally no, not as a migration strategy For your Cloud + EKS migration, the practical answer is: Yes, Prefect 2.x and 3.x worker-style components can coexist in the same namespace during migration, as long as you keep the Prefect-side routing/configuration separated and validate RBAC/secrets carefully. --- What “same namespace” means in practice Kubernetes itself does not care that one Deployment/pod is running Prefect 2 and another is running Prefect 3. So you can have, in the same namespace: - an older Prefect 2 worker/agent deployment - a newer Prefect 3 worker deployment That is technically fine from a namespace perspective. --- The real caution: separate control paths The safer migration pattern is to keep them logically separate by using: - separate work pools - separate deployments pointed at the intended pool - optionally separate service accounts/secrets if you want isolation I would not treat “same namespace” as meaning “same everything”. --- Recommended coexistence pattern during migration For a staged migration, I’d recommend: - Prefect 2-side workers handle only the existing 2.x-targeted deployments/pools - Prefect 3-side workers handle only the new 3.x work pool(s) - both worker Deployments can live in the same EKS namespace if that’s operationally convenient --- What I would be careful about 1. Work pool compatibility This is the biggest operational concern. Even if both worker versions are in the same namespace, they should not be assumed to be interchangeable against the same pool/configuration model during migration. Use clear pool separation. --- 2. Service account / RBAC Same namespace means shared RBAC scope is possible, so decide whether you want: - shared service account simpler, but less isolated or - separate service accounts cleaner during migration, especially if you want to roll back or compare behaviors safely For EKS, separate service accounts can also make IRSA cleaner. --- 3. Secrets / env vars You can run both in the same namespace, but watch for: - similarly named Secrets - ConfigMaps - Helm release resource names - worker env vars with changed names/recommended names in Prefect 3 Try to avoid reusing ambiguous secret names if both stacks are present temporarily. --- 4. Resource naming / Helm release names If you deploy both into the same namespace, make sure they have distinct: - Helm release names - Deployment names - ServiceAccount names - Secret names if templated by chart This is usually more important than the namespace itself. --- 5. Observer / pod watching behavior If your Prefect 3 Kubernetes worker setup includes observer/watch behavior, I’d scope it carefully so it doesn’t create confusion while both versions coexist. Namespace sharing is okay, but broad watch behavior can make debugging noisier. --- 6. Job resource contention Even if coexistence is logically fine, both versions may launch run jobs into the same cluster/namespace, so watch: - quotas - node capacity - service account permissions - network policies - image pull limits This is a Kubernetes operational concern more than a Prefect one. --- My recommendation for your migration For a temporary migration window, yes, same namespace is reasonable if you do all of the following: - use separate work pools - use distinct worker Deployments/releases - preferably use separate service accounts - verify no secret/config naming collisions - test one 3.x worker path end-to-end before moving traffic --- What I would avoid I would avoid: - trying to “in-place” switch one worker Deployment from 2.x to 3.x without clear separation/testing - assuming a 3.x worker can just replace a 2.x one against unchanged pool/config semantics
- mixing both versions under one ambiguous Helm release/config set --- Bottom line - Can Prefect 2 and Prefect 3 workers be deployed to the same Kubernetes namespace? Yes - Is that a recommended migration pattern? Yes, temporarily, if you separate work pools/config clearly - Should they share the exact same logical worker setup? No, I’d keep them distinct during migration If you want, I can sketch a safe side-by-side EKS migration layout showing: - worker-v2 deployment - worker-v3 deployment - service accounts - secrets - work pool separation
s
@Marvin can you validate this statement? "*Sync task called directly from an async flow.* The flow becomes
async
(it runs on an event loop), but some tasks stay plain
def
. A synchronous task called directly inside an async flow can stall or hang the run (it holds the event loop that Prefect's own orchestration shares), so it will be validated on the staging app and staging environment under the real v3 image (ideally alongside a
wait_for=
gate on a returned
Failed()
state)."
m
thought for 283.9 seconds
I’d soften and correct part of that statement. The safest validated version is: - yes, this is worth testing in staging - yes, mixing
async
flows with directly-called sync tasks deserves care - but I would not confidently state that a sync task called directly from an async flow will inherently “stall or hang the run because it holds the event loop that Prefect orchestration shares” without qualification For Prefect 3, the more defensible statement is: Recommended wording
Sync task called directly from an async flow: this mixed sync/async pattern should be tested carefully during the Prefect 3 migration. An
async
flow can still execute sequentially, and plain
def
tasks may still be used, but directly calling synchronous tasks inside an async flow can block progress in ways that are less predictable than a fully sync flow or a fully async flow. If the flow remains async, validate these sync tasks under the real Prefect 3 runtime image in staging, especially around
wait_for=
dependencies and tasks that return
Failed()
/
Completed()
states.
--- What I would validate vs what I would not overclaim Reasonable to say - your flow becomes an
async
flow if declared
async def
- some tasks may still remain plain
def
- mixed sync/async patterns should be tested carefully in Prefect 3 -
wait_for=
and returned
Failed()
states are especially worth validating in staging Too strong / too specific unless you’ve reproduced it in your app - “a synchronous task called directly inside an async flow can stall or hang the run” - “it holds the event loop that Prefect’s own orchestration shares” That wording implies a very specific internal execution guarantee/failure mode. I would not present that as fact unless you’ve already observed it in your environment. --- What I’d recommend instead architecturally For Prefect 3, prefer one of these two clean patterns: Pattern A: fully sync sequential flow If you do not need true async I/O:
Copy code
python
@flow
def my_flow():
    a = task_a()
    b = task_b(a)
    c = task_c(b)
    return c
This is the cleanest option. Pattern B: async flow with genuinely async tasks If you do need async behavior:
Copy code
python
@flow
async def my_flow():
    a = await task_a()
    b = await task_b(a)
    c = task_c(b)
    return c
This can still be sequential if awaited in order. What I’d avoid if possible is a heavily mixed model where: - the flow is
async def
- some tasks are plain sync
def
- orchestration depends on subtle future/state/wait ordering That pattern may still work, but it’s exactly the sort of thing that deserves staged validation. --- About
wait_for=
and returned
Failed()
states
Your instinct to test these is correct. I would say: -
wait_for=
should be explicitly tested in the migrated flow - tasks returning
Failed()
/
Completed()
should also be explicitly tested - especially if those returned states are used as control flow rather than exceptions That’s a stronger and safer claim than tying the risk specifically to event-loop starvation. --- Recommended final phrasing for your document I’d suggest this wording:
Mixed sync/async execution in Prefect 3: The flow may remain
async
, while some tasks remain plain synchronous
def
tasks. This mixed pattern should be validated carefully in the staging app and staging environment under the real Prefect 3 image. In particular, directly calling sync tasks from an async flow, using
wait_for=
dependencies, and branching on returned
Failed()
/
Completed()
states should all be tested end-to-end, since these patterns are more migration-sensitive than a fully synchronous flow or a fully async flow.
That keeps the caution, avoids overclaiming, and is much easier to defend. Bottom line - Validate the pattern in staging? Yes, absolutely
- Claim that it will hang because it blocks Prefect’s orchestration event loop? I would not state that so strongly without reproducing it - Best recommendation? Prefer either fully sync or clearly async patterns, and test mixed patterns explicitly If you want, I can help rewrite that whole migration note into a short, technically cautious risk statement for your upgrade plan.
s
@Marvin are there any documents available that outline the major changes between the Prefect v2 helm chart (2024.3.23210927) and the Prefect v3 helm chart (2026.6.18212926) and what to look out for when upgrading?
m
thought for 19.1 seconds
I’m not aware of a single official migration document that specifically compares Helm chart
2024.3.23210927
to
2026.6.18212926
side-by-side. For that upgrade, the best available sources are usually a combination of: - the Prefect Helm chart repo - the chart
values.yaml
/ README for each version -
helm diff
/ rendered manifest comparison - Prefect 3 migration docs Best docs to read together - Prefect Helm repo - Prefect Helm chart registry - prefect-worker chart - Upgrade to Prefect 3 - Run flows on Kubernetes - Work pools - Workers What I’d watch out for specifically between those chart eras Even without a dedicated “2024.3 → 2026.6” migration doc, these are the main things I’d expect to review carefully: 1. Worker chart values structure Check for changes in: -
worker.config
- work pool name/type wiring - base job template sync/config - API config sections - service account settings 2. Environment variable naming / placement Especially: - Prefect Kubernetes integration env vars - API key propagation behavior - observer-related settings - secret creation settings 3. RBAC and ServiceAccount changes For EKS, re-check: -
Role
/
ClusterRole
-
RoleBinding
- IRSA annotations - whether new permissions are needed for secrets, jobs, pods, logs, observer behavior 4. Base job template / work pool integration Newer worker chart behavior may handle base job template syncing differently than older worker chart versions, so validate: - what is configured in the chart - what is configured in the Prefect work pool - which side is authoritative 5. Kubernetes resource/rendered manifest changes Look for diffs in: - Deployment spec - probes - init containers - env vars - mounts - labels/annotations - pod security settings 6. Image/tag assumptions Check whether: - chart defaults changed image repo/tag behavior - your values file pins the right Prefect image - your worker image and Prefect version stay aligned 7. Stale values in your current
values.yaml
This is a common upgrade problem. Old keys may: - be ignored - have changed meaning - conflict with new defaults So I strongly recommend building a cleaned-up target values file instead of just reusing the old one unchanged. --- Recommended way to compare these two chart versions 1. Export what you’re actually running now
Copy code
bash
helm get values <release-name> -n <namespace> > current-values.yaml
helm get values -a <release-name> -n <namespace> > current-values-all.yaml
2. Pull defaults for both chart versions
Copy code
bash
helm inspect values prefect/prefect-worker --version 2024.3.23210927 > old-defaults.yaml
helm inspect values prefect/prefect-worker --version 2026.6.18212926 > new-defaults.yaml
diff -u old-defaults.yaml new-defaults.yaml
3. Compare rendered manifests
Copy code
bash
helm template prefect-worker prefect/prefect-worker \
  --version 2024.3.23210927 \
  -f current-values.yaml > old-rendered.yaml

helm template prefect-worker prefect/prefect-worker \
  --version 2026.6.18212926 \
  -f current-values.yaml > new-rendered.yaml

diff -u old-rendered.yaml new-rendered.yaml
4. Use
helm diff
before upgrading
Copy code
bash
helm diff upgrade <release-name> prefect/prefect-worker \
  --namespace <namespace> \
  --version 2026.6.18212926 \
  -f current-values.yaml \
  --suppress-secrets
This is usually the most useful “what will actually change” preview. --- My practical recommendation for your upgrade Since you’re on: - Cloud - workers - EKS I would create a small upgrade checklist focused on: - [ ] chart values schema changes
- [ ] worker env vars - [ ] service account / IRSA annotations - [ ] RBAC permissions - [ ] base job template handling - [ ] work pool configuration - [ ] worker image version - [ ] secret creation behavior - [ ] one non-prod worker rollout - [ ] one non-prod flow run end-to-end --- Bottom line - Is there one official doc for those exact two Helm chart versions? I don’t believe so - What’s the best way to understand the changes? Compare chart defaults, rendered manifests, and run
helm diff
- What should you watch most closely? Worker config, env vars, RBAC/IRSA, and work-pool/base-job-template behavior If you want, I can help you build a targeted Helm chart upgrade review checklist specifically for: -
2024.3.23210927
2026.6.18212926
-
prefect-worker
- EKS / IRSA / Cloud workers
s
@Marvin can a Prefect version 2 and a version 3 work pool be run within the same Prefect workspace?
m
thought for 10.9 seconds
Yes — within the same Prefect Cloud workspace, you can have both Prefect 2-era and Prefect 3-era work pools present during migration. But the important distinction is: - same workspaceyes - same work pool serving both versions interchangeably → not the safe assumption For your migration, the recommended pattern is: - keep your existing Prefect 2-associated pool(s) - create new Prefect 3 pool(s) - move deployments over gradually Practical answer You can run both in the same workspace at the same time as part of a staged migration. That means you can have: - existing 2.x deployments still targeting old pool(s) - new or migrated 3.x deployments targeting new pool(s) all within the same workspace. What I would recommend Use separate work pools for clarity and safety. For example: -
eks-v2
-
eks-v3
That makes it much easier to: - route deployments intentionally - test migration safely - roll back by pool if needed - avoid confusion over job template / worker behavior Why separate pools are better Even if the workspace can hold both, the pool config and worker behavior are where the real differences show up: - Kubernetes base job template behavior - worker image/runtime version - deployment expectations - env var / secret handling So workspace sharing is fine, but pool separation is the clean migration boundary. What to watch out for If both exist in the same workspace, be careful about: - deployment targeting the wrong pool - similarly named pools causing operator confusion - reusing old deployment assumptions against the new pool - automations/concurrency/tag rules that may span both old and new deployments Recommended migration pattern 1. keep current v2-oriented pool running 2. create a new v3 pool 3. start a v3 worker for that pool 4. deploy one migrated flow to the v3 pool 5. validate end-to-end 6. move flows gradually 7. retire the old pool when complete Bottom line - Can Prefect 2 and Prefect 3 work pools exist in the same workspace? Yes - Should you use separate pools during migration? Yes, absolutely - Is same-workspace coexistence a normal migration strategy? Yes If you want, I can also sketch a safe workspace layout for your migration with: - pool naming - worker naming - namespace/service account separation - staged cutover steps
s
@Marvin If you want, I can also sketch a safe workspace layout for your migration with: - pool naming - worker naming - namespace/service account separation - staged cutover steps
m
thought for 19.8 seconds
Here’s a safe workspace layout I’d recommend for your migration. Since you’re on: - Prefect Cloud - EKS / Kubernetes workers - Python deployment code - same workspace during migration the goal is to make version boundaries obvious and rollback easy. --- 1. Work pool naming Use clearly versioned pool names. Example:
Copy code
text
eks-v2
eks-v3
If you have multiple environments:
Copy code
text
eks-dev-v2
eks-dev-v3
eks-stage-v2
eks-stage-v3
eks-prod-v2
eks-prod-v3
If multiple workload classes exist:
Copy code
text
eks-dbt-v2
eks-dbt-v3
eks-etl-v2
eks-etl-v3
Recommendation Keep names explicit enough that nobody can accidentally deploy a v3 flow to the old pool. --- 2. Worker naming / Helm release naming Also make worker releases obviously versioned. Examples:
Copy code
text
prefect-worker-v2
prefect-worker-v3
If separated by env:
Copy code
text
prefect-worker-stage-v2
prefect-worker-stage-v3
prefect-worker-prod-v2
prefect-worker-prod-v3
This helps with: - Helm upgrades - log filtering - rollback - on-call debugging --- 3. Kubernetes namespace strategy You can use the same namespace, but I’d recommend one of these two approaches. Option A: same namespace, separate identities Good if you want minimal Kubernetes rework. Example:
Copy code
text
namespace: prefect
with separate: - Deployments - ServiceAccounts - Secrets - Helm release names This is acceptable for a short migration window. Option B: separate namespaces Safer and cleaner if you want stronger isolation. Example:
Copy code
text
prefect-v2
prefect-v3
or:
Copy code
text
prefect-stage-v2
prefect-stage-v3
This makes it easier to isolate: - RBAC - secrets - IRSA roles - resource quotas - debugging My recommendation For production migration, I’d prefer separate namespaces if practical. If that’s too heavy operationally, same namespace is fine as long as identities are separate. --- 4. Service account separation I strongly recommend separate service accounts. Example:
Copy code
text
prefect-worker-v2
prefect-worker-v3
For flow-run jobs, if you use a distinct service account from the worker pod, version those too:
Copy code
text
prefect-run-v2
prefect-run-v3
Why this helps: - clearer IRSA role binding - easier RBAC auditing - lower risk of accidental policy overlap - easier rollback/cleanup --- 5. IAM / IRSA separation For EKS, I’d prefer separate IAM roles for the v2 and v3 workers if feasible. Example:
Copy code
text
arn:aws:iam::<acct>:role/prefect-worker-v2
arn:aws:iam::<acct>:role/prefect-worker-v3
If run pods use IRSA too:
Copy code
text
arn:aws:iam::<acct>:role/prefect-run-v2
arn:aws:iam::<acct>:role/prefect-run-v3
Even if the permissions are initially identical, separating roles gives you: - cleaner audit trail - easier rollback - freedom to tighten v3 later --- 6. Secret/config naming Avoid reusing generic names if both versions coexist. Instead of:
Copy code
text
prefect-api-key
prefer:
Copy code
text
prefect-api-key-v2
prefect-api-key-v3
Same idea for ConfigMaps or worker env Secret refs:
Copy code
text
prefect-worker-config-v2
prefect-worker-config-v3
This avoids “which deployment is using this secret?” confusion. --- 7. Deployment targeting strategy In your Python deployment code, explicitly target the pool by version. Example:
Copy code
python
my_flow.from_source(
    source=...,
    entrypoint="flows/my_flow.py:my_flow",
).deploy(
    name="generic-dbt-flow",
    work_pool_name="eks-v3",
    job_variables={
        "namespace": "prefect-v3",
        "service_account_name": "prefect-run-v3",
    },
)
That makes the cutover explicit in source control. --- 8. Suggested migration layout Workspace
Copy code
text
Workspace: my-team-prod
Pools
Copy code
text
eks-prod-v2
eks-prod-v3
Worker deployments
Copy code
text
prefect-worker-prod-v2
prefect-worker-prod-v3
Namespaces Option A:
Copy code
text
prefect
Option B:
Copy code
text
prefect-v2
prefect-v3
Service accounts ```text prefect-worker-v2 prefect-worker-v3
prefect-run-v2 prefect-run-v3
Copy code
*Secrets*
```text
prefect-api-key-v2
prefect-api-key-v3
--- 9. Staged cutover steps Phase 1: Prepare v3 alongside v2 - [ ] keep existing v2 pool/worker running - [ ] create new v3 work pool - [ ] deploy new v3 worker - [ ] configure v3 work pool base job template - [ ] validate v3 worker registration in Cloud Phase 2: Test one non-prod or low-risk flow - [ ] migrate one deployment to
eks-v3
- [ ] validate deployment creation - [ ] run one successful flow - [ ] run one failing flow - [ ] validate logs, secrets, IRSA, Kubernetes job settings Phase 3: Migrate selected production flows - [ ] move a small batch of flows to v3 pool - [ ] monitor first scheduled/manual runs - [ ] validate run pod creation and cleanup - [ ] validate expected state/failure behavior Phase 4: Full cutover - [ ] migrate remaining deployments to v3 pool - [ ] confirm no active production deployments point to v2 pool - [ ] scale down v2 workers - [ ] observe for a safe window - [ ] remove v2 pool/workers after confidence window --- 10. Rollback strategy Design the migration so rollback is “move deployment target back to v2 pool”. That means: - don’t delete v2 worker/pool immediately - keep old worker image available - keep old service account/IRSA role in place until the migration stabilizes If one migrated flow fails in v3, rollback should ideally be: - redeploy flow to
eks-v2
- resume service - investigate separately --- 11. My strongest recommendation If you only adopt one safety measure, make it this: use separate work pools and separate service accounts with explicit versioned names That alone makes the migration much less error-prone. --- Example “clean” target naming scheme
Copy code
text
Workspace: acme-prod

Work pools:
- eks-prod-v2
- eks-prod-v3

Worker Helm releases:
- prefect-worker-prod-v2
- prefect-worker-prod-v3

Namespaces:
- prefect-prod-v2
- prefect-prod-v3

ServiceAccounts:
- prefect-worker-v2
- prefect-worker-v3
- prefect-run-v2
- prefect-run-v3

Secrets:
- prefect-cloud-api-key-v2
- prefect-cloud-api-key-v3
--- If you want, I can turn this into either: 1. a one-page migration runbook, or 2. a Helm values skeleton for the v3 worker deployment on EKS.