Migrate Argo Workflows manifests to Prefect 3 Python flows.
Project description
argo2prefect
Migrate Argo Workflows manifests into runnable Prefect 3 Python flows.
argo2prefect parses your Argo YAML (Workflow, WorkflowTemplate,
ClusterWorkflowTemplate, CronWorkflow), builds a typed intermediate
representation, and generates clean, idiomatic Prefect code — DAG dependencies,
parallel steps, loops, parameters, schedules, retries and all. Where Argo has no
faithful Prefect equivalent, the tool emits a clear # TODO and a warning rather
than guessing, so you always know what still needs a human.
Why
Argo Workflows are declared in Kubernetes YAML and run as pods. Prefect flows are
plain Python, run anywhere, and come with first-class observability, retries,
caching and a UI. Moving a large fleet of Argo manifests by hand is slow and
error-prone. This tool automates the bulk of it and flags the rest: on the
official argo-workflows examples (217 workflows), 138 convert with no follow-up
at all, 58 convert fully with items flagged for review, and 21 contain
constructs that need a human — run argo2prefect assess to get the same
breakdown for your own fleet.
Installation
Not yet on PyPI — the first release (v0.2.0) is staged but not published. Until it lands, install from this repository.
From this repository (works today; uv
bootstraps Python and the env for you):
# Run without installing anything, straight from git:
uvx --from git+https://github.com/mthatt/argo2prefect argo2prefect assess ./argo-manifests
# Or from a local checkout:
uvx --from . argo2prefect --help
pip install -e ".[dev,generated]" # editable install + test/runtime deps
Once v0.2.0 is on PyPI, this becomes:
uvx argo2prefect assess ./argo-manifests # zero-install (recommended)
uv tool install argo2prefect # persistent CLI
pipx install argo2prefect # pip-world equivalent
pip install argo2prefect # into the current environment
Requires Python 3.10+.
The migration journey
A migration runs in four steps — assess → convert → verify → deploy — and the CLI has a command for each:
# 1. ASSESS: how big is this migration? No code is written. Every workflow is
# graded automatic / review / manual by running the real conversion
# pipeline in memory, with a fleet report in Markdown + JSON + HTML.
argo2prefect assess ./argo-manifests -o ./assessment
# 2. CONVERT: the directory is converted as ONE linked project — templateRef /
# workflowTemplateRef resolve across files, WorkflowTemplate manifests are
# emitted once into shared_templates.py, and MIGRATION_REPORT.md
# consolidates every remaining TODO with file:line anchors.
argo2prefect convert ./argo-manifests -o ./prefect_flows
# 3. VERIFY: prove every generated module imports before anyone runs it.
argo2prefect verify ./prefect_flows
# 4. DEPLOY: emit a Prefect Cloud prefect.yaml + step-by-step runbook.
argo2prefect convert ./argo-manifests -o ./prefect_flows --force \
--emit-prefect-yaml --source-repo https://github.com/acme/flows
Also useful:
argo2prefect convert workflow.yaml # single manifest to stdout
argo2prefect convert ./manifests -o ./flows --dry-run # see what would be written
argo2prefect inspect examples/argo/cron-backup.yaml # quick manifest summary
Existing output files are never overwritten unless you pass --force.
See COVERAGE.md for the full Argo-feature → Prefect-equivalent matrix and each feature's automation status.
inspect prints a quick summary:
CronWorkflow: nightly-backup
entrypoint: backup
schedule: 0 2 * * * (America/New_York)
parameters: database_url
templates: 1
- backup [container] (image: postgres:16)
Running the generated flow
Every generated file carries a PEP 723
header listing exactly what it needs — Prefect, plus the dependency for the
chosen --runtime (docker, kubernetes, or prefect-shell) — so it is
self-bootstrapping: uv reads it, installs the deps into an isolated
env, and runs the flow. No pip install step:
uv run flow.py # one-off local run (uses default parameters)
uv run flow.py --serve # deploy on the workflow's schedule (Prefect worker)
Without uv, install the deps from the file's PEP 723 header yourself, e.g.
for the default docker runtime:
pip install "prefect>=3,<4" "docker>=7"
python flow.py # one-off run; add --serve to deploy on a schedule
Deploying to Prefect Cloud
--serve above is great for a quick local test, but it ties the schedule to a
process on your machine. For a real handoff, argo2prefect can generate a
Prefect Cloud deployment config so your flows
run on a schedule in the client's workspace — with a step-by-step runbook.
Add --emit-prefect-yaml to any convert:
argo2prefect convert ./argo-manifests -o ./prefect_flows \
--emit-prefect-yaml \
--work-pool my-managed-pool \
--source-repo https://github.com/acme/flows
Alongside the *_flow.py files this writes two things into the output folder:
prefect.yaml— one deployment per workflow (entrypoint, schedule, parameters), wired to a work pool and apullstep that fetches the code.DEPLOY.md— the exact commands to get from zero to scheduled runs.
The end-to-end client experience
- Convert with
--emit-prefect-yaml(above). - Log in — this, not
prefect.yaml, is what binds the deployments to their account and workspace:prefect cloud login -k <API_KEY>
- Create the work pool (one time):
prefect work-pool create my-managed-pool --type prefect:managed
- Deploy every flow:
prefect deploy --all
The flows now appear in the Prefect Cloud UI, each on its schedule.
Start on Managed, then pick the right pool
The default work pool type is Prefect Managed (prefect:managed): Prefect
hosts the compute, so your client stands up no worker and no infrastructure —
the fastest path to a first green run. Managed compute has CPU/memory/time
limits, though, and has no Docker daemon or Kubernetes cluster access, so
both prefect.yaml and DEPLOY.md explicitly encourage switching to a pool
that matches the workload once things work:
| Workload | Recommended pool | Worker/infra |
|---|---|---|
| Light Python (get started) | prefect:managed (default) |
none — Prefect-hosted |
| Closest to Argo | kubernetes |
in-cluster worker |
| Serverless containers | ecs:push / cloud-run:push / azure-container-instance:push |
none (cloud-credentials block) |
| Simple self-hosted | process / docker |
a worker you run |
Switching is a one-line edit to work_pool.name in prefect.yaml plus a
prefect work-pool create, then prefect deploy --all again.
Code delivery (important)
A Cloud worker runs off-machine, so it must fetch your flow code. Pass
--source-repo <git-url> to emit a git_clone pull step (recommended). Without
it, prefect.yaml falls back to a local directory and both files flag — with a
# TODO and a checklist item — that a Managed/serverless pool cannot read it.
Flags:
--emit-prefect-yaml,--work-pool NAME(defaultmanaged-pool),--work-pool-type TYPE(defaultprefect:managed),--source-repo URL. Emitting the config requires a file/directory output (-o).
Execution runtimes
Argo runs every template in a container on Kubernetes. When generating Prefect
code you choose how that work should execute with --runtime:
| Runtime | How container/script templates run | Needs on the worker |
|---|---|---|
docker (default) |
Docker SDK: containers.run(...) with env, auto-remove, combined logs |
a Docker daemon |
shell |
the command/script runs directly on the Prefect worker host (image ignored) | the interpreter/CLI on PATH |
kubernetes |
official Kubernetes client: a Job is created, polled to completion, logs returned, and always deleted |
kubeconfig or in-cluster credentials |
The kubernetes runtime reads its namespace from $A2P_NAMESPACE (default
default). resource templates always use kubectl, http templates use
Python's stdlib urllib, and suspend templates map to time.sleep (fixed
duration) or a # TODO for indefinite pauses.
Mapping reference
The highlights (the complete matrix with per-feature automation status is COVERAGE.md):
| Argo concept | Prefect output |
|---|---|
container / script / resource / http |
@task function |
dag / steps template |
@flow subflow |
spec.entrypoint |
top-level @flow (the served deployment) |
dag.tasks[].dependencies / depends |
.submit() + wait_for=[...] (status gates flagged) |
templateRef / workflowTemplateRef |
import from the generated shared_templates.py |
steps (list of groups) |
sequential groups, parallel .submit() within a group |
withItems / withParam / withSequence |
.map() / unmapped(...) |
when (incl. {{= expr-lang }}) |
if guard (flagged for review) |
inputs.parameters |
task/flow function arguments |
arguments.parameters (workflow-level) |
typed main-flow parameters + shared WORKFLOW_PARAMETERS dict |
{{inputs.parameters.x}} |
the local x argument |
{{workflow.parameters.x}} |
WORKFLOW_PARAMETERS['x'] |
{{tasks.X.outputs.result}} |
upstream future X_fut (or X_fut.result() when embedded) |
{{tasks.X.outputs.parameters.NAME}} |
loud helper that raises until you map it (A2P-105) |
{{item}} / {{item.key}} |
the mapped loop variable |
retryStrategy (limit + backoff) |
@task(retries=, retry_delay_seconds=/exponential_backoff) |
activeDeadlineSeconds |
timeout_seconds= on the task/flow |
onExit |
on_completion= + on_failure= state hooks |
synchronization (mutex/semaphore) |
with concurrency(...) guard (+ limit-creation guidance) |
memoize |
cache_policy=INPUTS + cache_expiration= |
CronWorkflow.schedule(s) / .timezone / .suspend |
flow.serve(cron=...) / schedules in prefect.yaml (paused if suspended) |
Example
Input (examples/argo/dag-diamond.yaml), a classic diamond DAG:
templates:
- name: diamond
dag:
tasks:
- { name: A, template: echo, arguments: { parameters: [{ name: message, value: "{{workflow.parameters.greeting}} from A" }] } }
- { name: B, template: echo, dependencies: [A], arguments: { parameters: [{ name: message, value: "B saw: {{tasks.A.outputs.result}}" }] } }
- { name: C, template: echo, dependencies: [A], arguments: { parameters: [{ name: message, value: "C" }] } }
- { name: D, template: echo, dependencies: [B, C], arguments: { parameters: [{ name: message, value: "D" }] } }
Output (see examples/prefect/dag-diamond_flow.py):
@flow(name="diamond")
def diamond():
"""Argo dag template 'diamond'."""
# A -> template 'echo'
a_fut = echo.submit(message=f"{WORKFLOW_PARAMETERS['greeting']} from A")
# B -> template 'echo'
b_fut = echo.submit(
message=f"B saw: {a_fut.result()}", wait_for=[_f for _f in [a_fut] if _f is not None]
)
# C -> template 'echo'
c_fut = echo.submit(message="C", wait_for=[_f for _f in [a_fut] if _f is not None])
# D -> template 'echo'
d_fut = echo.submit(message="D", wait_for=[_f for _f in [b_fut, c_fut] if _f is not None])
return None
Committed reference conversions live in examples/prefect/.
What needs manual review
The tool is honest about its limits. Every follow-up item carries a stable
TODO(A2P-###) code in the generated code, is collected in the file's
docstring, and lands in MIGRATION_REPORT.md with a file:line anchor.
The recurring ones:
- Artifacts (
inputs/outputs.artifacts): storage is identified (s3/gcs/azure/http/git/…) and each artifact gets an anchored TODO with its location, but fetching/publishing is yours to wire — Prefect uses results/storage blocks instead of Argo's artifact repository. - Named output parameters (
outputs.parameterswithvalueFrom): Argo read these from files inside the container; only stdout is captured. A reference to one generates a helper that raises with mapping guidance until you port it — a loud failure instead of silently-wrong data. whenconditions: translated (including common{{= expr-lang }}) and marked for review. Anything untranslatable — regex=~, unknown functions — becomes an explicitif Falsewith a warning, never invalid code.- Secret/configmap env vars (
valueFrom): flagged with a placeholder; wire up Prefect Secret blocks or worker env yourself. - Volumes /
volumeClaimTemplates: configure on your Prefect work pool. templateRefto a manifest outside the conversion input,containerSet,data, and plugin templates: emitted as stubs that raiseNotImplementedError. (templateRefwithin the input resolves via the shared module — include the whole fleet in one run.)
Workflow parameters are shared through a module-level WORKFLOW_PARAMETERS dict
that the entry-point flow updates at runtime; this is correct for in-process task
runners (the default). If you switch to a distributed task runner, pass
parameters explicitly instead.
Project layout
src/argo2prefect/
models.py # typed intermediate representation (IR)
parser.py # Argo YAML -> IR
project.py # multi-manifest loading + cross-file templateRef linking
expressions.py # {{...}} / {{= expr-lang }} expression translation
naming.py # Argo names -> valid Python identifiers
generator.py # IR -> Prefect 3 Python source (per-runtime backends)
assess.py # fleet grading + assessment / migration reports
todos.py # stable TODO(A2P-###) code catalogue
deploy.py # deployment plans -> Prefect Cloud prefect.yaml + DEPLOY.md
cli.py # assess / convert / verify / inspect commands
examples/
argo/ # sample Argo manifests
prefect/ # committed reference conversions (regenerated from the tool)
tests/
corpus/ # 200+ real-world manifests (upstream argo-workflows examples)
golden/ # pinned generator output for the curated examples
... # parser, expression, generator, CLI and end-to-end tests
Development
uv run --extra dev --extra generated pytest # with uv
# or
pip install -e ".[dev,generated]" && pytest
The end-to-end tests generate flows, import them and actually execute them with
Prefect (using host-available echo/sh, no Docker or cluster needed); they are
skipped if Prefect is not installed. One test additionally runs a generated flow
via uv run to verify the PEP 723 self-bootstrapping path (skipped if uv is not
on PATH).
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file argo2prefect-0.2.0.tar.gz.
File metadata
- Download URL: argo2prefect-0.2.0.tar.gz
- Upload date:
- Size: 69.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d00e2346111177d7b4ebc37748bacc063255a26cf51dd51a16a8262380aaf1a0
|
|
| MD5 |
4acad27d601f0bde63a312ed49b03214
|
|
| BLAKE2b-256 |
e765afeb6606548764778187b446f8f12635b4fa3f4060b3579231d28e2c0034
|
Provenance
The following attestation bundles were made for argo2prefect-0.2.0.tar.gz:
Publisher:
release.yml on mthatt/argo2prefect
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
argo2prefect-0.2.0.tar.gz -
Subject digest:
d00e2346111177d7b4ebc37748bacc063255a26cf51dd51a16a8262380aaf1a0 - Sigstore transparency entry: 2118925834
- Sigstore integration time:
-
Permalink:
mthatt/argo2prefect@a8d6aeafb1459767a0f44adce9c142fedbdeea1f -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/mthatt
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a8d6aeafb1459767a0f44adce9c142fedbdeea1f -
Trigger Event:
push
-
Statement type:
File details
Details for the file argo2prefect-0.2.0-py3-none-any.whl.
File metadata
- Download URL: argo2prefect-0.2.0-py3-none-any.whl
- Upload date:
- Size: 56.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4c9e31084ac9394fc2a401e7912d63eeb7d7dbbf5f261525b939d721e3f078e9
|
|
| MD5 |
a6117e64b7fda63293ba7215b8c56108
|
|
| BLAKE2b-256 |
3ee01261186302d67a5c200be5411b0139022c6d4e97426f6985f953932691e0
|
Provenance
The following attestation bundles were made for argo2prefect-0.2.0-py3-none-any.whl:
Publisher:
release.yml on mthatt/argo2prefect
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
argo2prefect-0.2.0-py3-none-any.whl -
Subject digest:
4c9e31084ac9394fc2a401e7912d63eeb7d7dbbf5f261525b939d721e3f078e9 - Sigstore transparency entry: 2118925886
- Sigstore integration time:
-
Permalink:
mthatt/argo2prefect@a8d6aeafb1459767a0f44adce9c142fedbdeea1f -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/mthatt
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a8d6aeafb1459767a0f44adce9c142fedbdeea1f -
Trigger Event:
push
-
Statement type: