Migrating from Prefect

A practical map from Prefect flows, tasks, deployments, and blocks to Flux — including a coexistence pattern for teams that share a Python platform.

Prefect and Flux are the closest siblings in Python workflow tooling: both are decorator-driven, both feel like writing ordinary Python, both let you compose tasks into larger units with retries and scheduling on top. The shapes of the APIs are close enough that the translation is mostly mechanical. The question is therefore less “how do I write this in Flux?” and more “given that both tools exist, why move?”

The honest case for moving: durable execution as a first-class property. Prefect records run state and lets you retry from failure, but the determinism contract and crash-resume guarantees are not the same as Flux’s. If you have workflows where partial progress is expensive — multi-step pipelines that hit external APIs, long-running jobs, workflows with human pauses — Flux’s event-log replay model is the differentiator. If your workflows are short and your team is happy in Prefect Cloud, do not move.

Mental-model translation

PrefectFluxNotes
@flow@workflowSame role: the orchestrator.
@task@taskSame role: the unit of I/O.
.submit() / .map()parallel() from flux.tasks or comprehensionsDifferent surface, same intent.
.with_options(...).with_options(...)Spelled the same. Different option names.
Deploymentflux workflow register + a Flux serverThe registration step is the deployment.
Work pool / work queueWorker affinity via affinity={...}Conceptually similar, mechanically different.
Block (Secret, S3, Snowflake, etc.)secret_requests + Flux’s secrets store + ordinary Python clientsFlux ships a secrets store, not a typed-integration ecosystem.
Prefect Cloud UIFlux REST API + CLI (flux execution show)No managed UI today.
State (Pending / Running / Failed / Cancelled)Execution status + event logRead via flux execution show <id>.
prefect.runtime contextExecutionContext (typed parameter)Passed explicitly to the workflow function.

Code-pattern translation

A flow with a task.

# Prefect
from prefect import flow, task

@task(retries=3, retry_delay_seconds=5)
def fetch(url: str) -> str:
    return httpx.get(url).text

@flow
def pipeline(url: str) -> int:
    body = fetch(url)
    return len(body)
# Flux
from flux import ExecutionContext
from flux.task import task
from flux.workflow import workflow

@task.with_options(retry_max_attempts=3, retry_delay=5)
async def fetch(url: str) -> str:
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.text

@workflow
async def pipeline(ctx: ExecutionContext[str]):
    body = await fetch(ctx.input)
    return len(body)

The shapes match. The decorator names change, the task becomes async, the workflow takes an ExecutionContext typed by its input. Retries become retry_max_attempts and retry_delay.

Mapped tasks.

# Prefect — .map for parallelism over a list
@task
def process(item: dict) -> dict:
    return work(item)

@flow
def fan_out(items: list[dict]):
    results = process.map(items)
    return summarize(results)
# Flux — parallel() over a comprehension
from flux.tasks import parallel

@task
async def process(item: dict) -> dict:
    return await work(item)

@workflow
async def fan_out(ctx: ExecutionContext[list[dict]]):
    results = await parallel(*(process(item) for item in ctx.input))
    return await summarize(results)

.map(items) becomes parallel(*(task(item) for item in items)). The results are returned in input order, same as Prefect’s behaviour. For very large input sets, chunk explicitly — Flux does not impose a default concurrency limit on parallel.

A scheduled deployment.

# Prefect — schedule via Deployment
from prefect.client.schemas.schedules import CronSchedule

deployment = pipeline.to_deployment(
    name="daily-pipeline",
    schedule=CronSchedule(cron="0 3 * * *", timezone="UTC"),
)
deployment.apply()
# Flux — schedule on the workflow itself
from flux import cron

@workflow.with_options(name="daily_pipeline", schedule=cron("0 3 * * *", timezone="UTC"))
async def pipeline(ctx: ExecutionContext[None]):
    body = await fetch("https://api.example.com/daily")
    return len(body)
flux workflow register my_module.py

The schedule moves from a separate deployment manifest to a decorator option. When the workflow is registered against a Flux server, the schedule is created automatically. The scheduler runs inside the server process and coordinates across replicas through PostgreSQL advisory locks, so schedules fire once regardless of how many server replicas you run.

Secrets and configuration.

# Prefect — Secret block
from prefect.blocks.system import Secret

@task
def query():
    api_key = Secret.load("snowflake-api-key").get()
    return run_query(api_key)
# Flux — secret declared on the task, resolved through the server
@task.with_options(secret_requests=["snowflake-api-key"])
async def query(secrets: dict[str, str] = {}):
    return await run_query(secrets["snowflake-api-key"])
flux secrets set snowflake-api-key "..."

Flux ships a secrets store: values are encrypted at rest in the Flux database and resolved through the server at task time, so worker processes and runner children never hold a standing credential set. The typed configuration object part of Prefect Blocks — S3 buckets, Snowflake connections, Slack webhooks as first-class classes — has no peer in Flux 0.56.0; for those, keep the client construction as ordinary Python inside the task and feed it credentials via secret_requests. See Secrets management.

Routing work to specific workers.

# Prefect — work pool
@flow
def gpu_workflow(): ...

# At deployment time:
gpu_workflow.to_deployment(name="train", work_pool_name="gpu-pool").apply()
# Flux — affinity declared on the workflow
@workflow.with_options(affinity={"gpu": "true"})
async def gpu_workflow(ctx: ExecutionContext[dict]):
    return await train(ctx.input)

A Flux worker advertises its labels at startup (flux start worker my-worker --label gpu=true), and the server only dispatches workflows whose affinity clause matches the worker’s labels. The same primitive covers work-pool-style routing without a separate pool object.

What you give up

Prefect Cloud’s UI is the most-cited reason to stay. The run timeline, the per-flow heatmap, the deployment dashboard, the cloud-hosted scheduler — these are good products, and there is no managed Flux equivalent today. If your operations team lives in Prefect Cloud, plan for a CLI-and-API workflow instead.

The Block ecosystem is the second-largest gap. Prefect ships first-class integrations for AWS, GCP, Azure, Snowflake, Databricks, Slack, GitHub, dbt, and dozens of others — each one a typed configuration object with a clean load-from-storage pattern. Flux does not. You write the integration as ordinary Python inside a @task. For the handful of integrations you actually use, this is usually less code than the Block configuration; for ones you might want to add later, it is more upfront work.

You give up some scheduling sophistication. Prefect’s deployments can target multiple schedules, can be paused, and have a rich notion of versions. Flux’s scheduling surface is intentionally simple: one schedule per workflow, declared on the workflow function, registered when the workflow is registered. If you need to manage hundreds of schedules across a single workflow definition with different inputs, you will rebuild that pattern at the application layer.

Notification integrations. Prefect’s automation feature lets you wire Slack / PagerDuty / Email notifications to run state transitions out of the box. Flux exposes the same data through the REST API; you wire your own watcher.

Coexistence pattern

The strongest practical advice for Prefect-to-Flux is: do not migrate. Coexist.

Prefect and Flux share nothing operationally. Different decorators, different runners, different databases, different control planes. Two teams in the same company can own different workloads on different tools and pay essentially no integration tax. A platform team that wants to evaluate Flux without committing the whole organisation should pick one team, one workload class, and run it on Flux while everyone else continues on Prefect.

The signal to consolidate (in either direction) is operational: the two systems start producing duplicated alerting, duplicated on-call rotations, or duplicated infrastructure. If you can keep them in separate operational lanes, coexistence is a stable end state, not an interim one.

Migration order

Usually one team at a time.

If you’ve decided the move is worth it, pick a team whose workloads benefit most from durable execution — multi-step pipelines, long-running jobs, workflows with pauses. That team ports its flows. They keep the Prefect deployments running for a release cycle and run the Flux version in parallel, comparing outputs on the same inputs. After two or three deployments survive the soak test, they cut over and decommission the Prefect deployments.

The next team only moves if the first team’s migration paid off. “We tried Flux on one team, it’s fine, mandate a company-wide migration” is the failure pattern. The interesting question is whether the team that moved is meaningfully more reliable than they were before. If yes, the second team can move. If the answer is “about the same,” coexist permanently — Prefect is good software, and a wholesale migration to Flux that doesn’t produce a reliability win is not worth the cost.

Where to read more


Compared against Prefect 3.x and Prefect Cloud as of July 2026. Flux 0.56.0.