Scott Pedersen
06/24/2026, 1:28 AMMarvin
06/24/2026, 1:28 AMMarvin
06/24/2026, 1:30 AMDeployment.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:
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:Marvin
06/24/2026, 1:30 AMprefect.yaml vs CLI-driven deploymentScott Pedersen
06/24/2026, 1:32 AMMarvin
06/24/2026, 1:32 AMMarvin
06/24/2026, 1:33 AMbash
pip install -U prefect
2. Upgrade any integration packages your workers use:
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:
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 PythonMarvin
06/24/2026, 1:33 AMScott Pedersen
06/24/2026, 1:36 AMMarvin
06/24/2026, 1:36 AMMarvin
06/24/2026, 1:36 AMDeployment.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:
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:
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:
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",Marvin
06/24/2026, 1:36 AM*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 assumptionsMarvin
06/24/2026, 1:36 AMScott Pedersen
06/24/2026, 1:49 AMmu_flow_instance = my_flow.from_source(...)
and
deployment_id = my_flow_instance.deploy(...)Marvin
06/24/2026, 1:49 AMMarvin
06/24/2026, 1:49 AMpython
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:
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:
python
my_flow_instance = my_flow.from_source(...)
deployment_id = my_flow_instance.deploy(...)
upgrade that environment first in non-prod:
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:
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 graduallyMarvin
06/24/2026, 1:49 AM.from_source(...) and .deploy(...).Scott Pedersen
06/24/2026, 1:52 AMMarvin
06/24/2026, 1:52 AMMarvin
06/24/2026, 1:53 AMfrom_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 usedMarvin
06/24/2026, 1:53 AMScott Pedersen
06/25/2026, 9:59 PMMarvin
06/25/2026, 9:59 PMMarvin
06/25/2026, 10:01 PMtext
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:
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:
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:
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:
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.Scott Pedersen
06/25/2026, 11:13 PMMarvin
06/25/2026, 11:13 PMMarvin
06/25/2026, 11:13 PMpydantic.
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:
text
pydantic>=2
not:
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:
python
from pydantic import BaseModel, ConfigDict
class MyModel(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
instead of old v1-style config:
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.Scott Pedersen
06/25/2026, 11:13 PMMarvin
06/25/2026, 11:13 PMMarvin
06/25/2026, 11:14 PMpydantic>=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:
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:
python
from pydantic import BaseModel
class Params(BaseModel):
x: int
class Config:
arbitrary_types_allowed = True
orm_mode = True
Pydantic v2:
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:
python
from pydantic import BaseModel, validator
class Params(BaseModel):
name: str
@validator("name")
def validate_name(cls, v):
return v.strip()
Pydantic v2:
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:
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:
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:
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()Marvin
06/25/2026, 11:14 PMpython
Params.parse_obj(data)
Pydantic v2:
python
Params.model_validate(data)
Pydantic v1:
python
Params.from_orm(obj)
Pydantic v2:
python
Params.model_validate(obj)
with:
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:
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:
python
my_flow.deploy(
name="prod",
work_pool_name="eks-pool",
parameters={
"params": Params(customer_id=123).dict()
},
)
update it to:
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.Marvin
06/25/2026, 11:14 PMModel(...)
- [ ] 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:
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:
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:
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:
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.Scott Pedersen
06/28/2026, 8:52 PMPREFECT_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.Marvin
06/28/2026, 8:52 PMMarvin
06/28/2026, 8:54 PMPREFECT_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:
bash
PREFECT_KUBERNETES_WORKER_STORE_PREFECT_API_IN_SECRET=true
Preferred going forward:
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:
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.Scott Pedersen
06/28/2026, 9:00 PMMarvin
06/28/2026, 9:00 PMMarvin
06/28/2026, 9:03 PMprefect-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:
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.:
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:
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 requestsMarvin
06/28/2026, 9:03 PMbash
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.Scott Pedersen
07/09/2026, 4:57 AMPrefectFuture.result() work in async flows? Does the require updating when moving from Prefect 2.20.16 to Prefect 3.x.xMarvin
07/09/2026, 4:57 AMMarvin
07/09/2026, 4:59 AMtask.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:
python
future = await my_task.submit()
result = await future.result()
In Prefect 3, the normal pattern is:
python
future = my_task.submit()
result = future.result()
So yes, remove the await in those places.
Recommended Prefect 3 pattern in async flows
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:
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:
python
@flow
async def my_flow():
future = await some_task.submit(1)
value = await future.result()
return value
After:
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.Scott Pedersen
07/09/2026, 5:02 AMSequentialTaskRunner()
@flow(
name="my-flow",
task_runner=SequentialTaskRunner(),
)
To keep our flow sequential, what do we need to do under Prefect v3.x.x?Marvin
07/09/2026, 5:02 AMMarvin
07/09/2026, 5:03 AMSequentialTaskRunner().
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:
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:
python
from prefect import flow
from prefect.task_runners import SequentialTaskRunner
@flow(
name="my-flow",
task_runner=SequentialTaskRunner(),
)
def my_flow():
...
to this:
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:
python
x = my_task()
y = other_task(x)
If you use `.submit()`:
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:
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:
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.Scott Pedersen
07/10/2026, 11:07 PMwait_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()?Marvin
07/10/2026, 11:07 PMMarvin
07/10/2026, 11:10 PMwait_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:
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:
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:
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:
python
return Failed(message="bad input")
that is not the same as:
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:
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:
python
@flow(
name="my-flow",
task_runner=SequentialTaskRunner(),
)
def my_flow():
a = task_a()
b = task_b(wait_for=[a])
return b
becomes:
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:
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):Marvin
07/10/2026, 11:10 PMThis 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.Scott Pedersen
07/10/2026, 11:17 PM.submit(). for example
result_future: PrefectFuture[bool, Literal[False]] = get_credentials.submit( ... )Marvin
07/10/2026, 11:17 PMMarvin
07/10/2026, 11:17 PM.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:
python
future = get_credentials.submit(...)
returns a PrefectFuture[...]
but:
python
result = get_credentials(...)
returns the actual task result, not a future.
So for your example:
python
result_future: PrefectFuture[bool, Literal[False]] = get_credentials.submit(...)
the equivalent without .submit() would be something like:
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:
python
PrefectFuture[bool, Literal[False]]
You should expect a single type parameter form instead, i.e. conceptually:
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:
python
result_future = get_credentials.submit(...)
result = result_future.result()
should usually become:
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()
python
credentials_future = get_credentials.submit(...)
ok = credentials_future.result()
Without .submit()
python
ok = get_credentials(...)
With a state-returning task and no .submit()
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.Scott Pedersen
07/13/2026, 6:34 AMasync keyword for flows or tasks if the flow is sequential and not asynchronous?Marvin
07/13/2026, 6:34 AMMarvin
07/13/2026, 6:34 AMasync on the flow or the tasks.
Short version
Use normal synchronous definitions:
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:
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:
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.Scott Pedersen
07/13/2026, 6:52 AM@flow(
name="generic-dbt-flow",
task_runner=SequentialTaskRunner(),
on_cancellation=[handle_cancelled],
timeout_seconds=24 * 60 * 60, # 24 hours
)
in Prefect v3
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?Marvin
07/13/2026, 6:52 AMMarvin
07/13/2026, 6:52 AMdef 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:
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:
python
@task
def task_c(...):
...
And call it directly from the flow:
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:
python
@task
async def task_a(...):
...
Then the calling flow should also be async def, and you should call/await it normally.
Example:
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:
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:
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:
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:
```pythonMarvin
07/13/2026, 6:52 AMIf 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
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
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.Scott Pedersen
07/13/2026, 7:22 AMasync def my_flow can the flow and tasks still be made to execute sequentially?Marvin
07/13/2026, 7:22 AMMarvin
07/13/2026, 7:22 AMasync 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:
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:
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:
python
f1 = task_a.submit()
f2 = task_b.submit()
or
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
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.Scott Pedersen
07/13/2026, 10:30 PMflow.deploy(..) as from my understanding is_schedule_active has been removed in Prefect 3?Marvin
07/13/2026, 10:30 PMMarvin
07/13/2026, 10:33 PMis_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:
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:
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:
python
is_schedule_active=False
New:
python
schedule=Cron("0 0 * * *", active=False)
or for multiple schedules:
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:
python
deployment_id = await my_flow.deploy(...)
In Prefect 3:
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:
python
from prefect.schedules import Cron, Interval
my_flow.deploy(
name="daily",
work_pool_name="my-pool",
schedule=Cron("0 0 * * *", active=True),
)
or:
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:
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:
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:
python
job_variables={...}
That’s the main place to override Kubernetes execution settings per deployment.
Example:
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:
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:
python
my_flow_instance = my_flow.from_source(...)
deployment_id = my_flow_instance.deploy(...)Marvin
07/13/2026, 10:33 PMawait around deploy(...)
- replacing is_schedule_active
- validating schedule objects/arguments
---
Practical before/after examples
Prefect 2 style
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
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:
python
my_flow.deploy(
name="x",
work_pool_name="pool",
cron="0 0 * * *",
)
is okay.
But if you want to control schedule activation, use:
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.Scott Pedersen
07/13/2026, 11:07 PMMarvin
07/13/2026, 11:07 PMMarvin
07/13/2026, 11:09 PMinfra_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:
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
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
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`:
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:
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",Marvin
07/13/2026, 11:09 PMThen 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.:
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:Marvin
07/13/2026, 11:09 PMjob_manifest
- infra_overrides
and I can translate it into the exact Prefect 3 work pool template + job_variables form.Scott Pedersen
07/20/2026, 5:17 AMMarvin
07/20/2026, 5:17 AMMarvin
07/20/2026, 5:19 AMprefect-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:
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:
bash
helm inspect values prefect/prefect-worker --version <target-chart-version> > new-defaults.yaml
If you want to compare old and new defaults:
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:
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:
bash
helm plugin install <https://github.com/databus23/helm-diff>
Then run:
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 settingsMarvin
07/20/2026, 5:19 AM<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
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.Scott Pedersen
07/20/2026, 5:48 AMMarvin
07/20/2026, 5:48 AMMarvin
07/20/2026, 5:48 AMbash
helm list -n <namespace>
or more specifically:
bash
helm status <release-name> -n <namespace>
That will show the deployed chart version and often the app version.
You can also use:
bash
helm get metadata <release-name> -n <namespace>
---
If you want to see available Prefect chart versions
First add/update the repo if needed:
bash
helm repo add prefect <https://prefecthq.github.io/prefect-helm>
helm repo update
Then list versions:
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:
bash
helm search repo prefect/prefect-server --versions
---
If you want to inspect a specific chart version
Example:
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:
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.Scott Pedersen
07/20/2026, 8:14 AMMarvin
07/20/2026, 8:14 AMMarvin
07/20/2026, 8:17 AMMarvin
07/20/2026, 8:17 AMScott Pedersen
07/20/2026, 9:13 AMasync (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)."Marvin
07/20/2026, 9:13 AMMarvin
07/20/2026, 9:18 AMasync 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--- What I would validate vs what I would not overclaim Reasonable to say - your flow becomes anflow can still execute sequentially, and plainasynctasks 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 arounddefdependencies and tasks that returnwait_for=/Failed()states.Completed()
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:
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:
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 remainThat keeps the caution, avoids overclaiming, and is much easier to defend. Bottom line - Validate the pattern in staging? Yes, absolutely, while some tasks remain plain synchronousasynctasks. 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, usingdefdependencies, and branching on returnedwait_for=/Failed()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.Completed()
Marvin
07/20/2026, 9:18 AMScott Pedersen
07/20/2026, 11:45 PMMarvin
07/20/2026, 11:45 PMMarvin
07/20/2026, 11:45 PM2024.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
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
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
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
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 changesMarvin
07/20/2026, 11:45 PMhelm 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 workersScott Pedersen
07/27/2026, 7:54 AMMarvin
07/27/2026, 7:54 AMMarvin
07/27/2026, 7:54 AMeks-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 stepsScott Pedersen
07/27/2026, 8:06 AMMarvin
07/27/2026, 8:06 AMMarvin
07/27/2026, 8:06 AMtext
eks-v2
eks-v3
If you have multiple environments:
text
eks-dev-v2
eks-dev-v3
eks-stage-v2
eks-stage-v3
eks-prod-v2
eks-prod-v3
If multiple workload classes exist:
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:
text
prefect-worker-v2
prefect-worker-v3
If separated by env:
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:
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:
text
prefect-v2
prefect-v3
or:
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:
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:
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:
text
arn:aws:iam::<acct>:role/prefect-worker-v2
arn:aws:iam::<acct>:role/prefect-worker-v3
If run pods use IRSA too:
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:
text
prefect-api-key
prefer:
text
prefect-api-key-v2
prefect-api-key-v3
Same idea for ConfigMaps or worker env Secret refs:
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:
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
text
Workspace: my-team-prod
Pools
text
eks-prod-v2
eks-prod-v3
Worker deployments
text
prefect-worker-prod-v2
prefect-worker-prod-v3
Namespaces
Option A:
text
prefect
Option B:
text
prefect-v2
prefect-v3
Service accounts
```text
prefect-worker-v2
prefect-worker-v3Marvin
07/27/2026, 8:06 AM*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
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.