From zenml-io-skills
Migrate Flyte workflows, tasks, LaunchPlans, and Flytekit code to idiomatic ZenML pipelines. Handles concept mapping (`@task`->`@step`, `@workflow`->`@pipeline`, `map_task()`->dynamic `.map()`, `conditional()`->dynamic branching, `LaunchPlan`->schedule/config split), code translation, special-type migration (`FlyteFile`, `FlyteDirectory`, `StructuredDataset`, `FlyteSchema`), Docker/image mapping, and flags unsupported patterns (`@eager`, `ContainerTask`, reference entities, checkpointing, interruptible semantics) for human review. Use this skill whenever the user mentions Flyte migration, converting Flyte to ZenML, porting Flyte workflows, replacing Flyte with ZenML, or asks how a Flyte concept maps to ZenML -- even if they do not explicitly say "migrate". Also use when they paste Flytekit code and ask to make it work with ZenML, or when they describe a workflow using Flyte terminology (`@dynamic`, `LaunchPlan`, `map_task`, `conditional`, `ImageSpec`, `FlyteFile`, `StructuredDataset`, `reference_task`, `reference_workflow`) in a ZenML context. If the user just asks a quick conceptual question ("what is the ZenML equivalent of LaunchPlan?" or "how should FlyteFile map?"), answer it directly from the concept map -- no need to run the full migration workflow.
npx claudepluginhub joshuarweaver/cascade-ai-ml-engineering --plugin zenml-io-skillsThis skill uses the workspace's default tool permissions.
This skill translates Flyte workflows into idiomatic ZenML pipelines. It handles the full migration workflow: analyzing Flytekit code, classifying each concept, translating what maps cleanly, flagging what needs redesign, and producing a working ZenML project with a migration report.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
This skill translates Flyte workflows into idiomatic ZenML pipelines. It handles the full migration workflow: analyzing Flytekit code, classifying each concept, translating what maps cleanly, flagging what needs redesign, and producing a working ZenML project with a migration report.
Flyte and ZenML are closer to each other than Flyte and older scheduler-first systems like Airflow. Both are Python-first orchestration frameworks that care about typed execution units, retries, scheduling, and containerized execution. So the migration is often more like “rewire the execution story” than “translate operator objects into functions.”
But there are still sharp edges. Flyte has a richer workflow transport type system (FlyteFile, StructuredDataset, FlyteSchema), a stronger registered execution surface via LaunchPlan, and special execution features like @dynamic, @eager, map_task(), conditional(), ContainerTask, and reference entities. ZenML can often reach the same business outcome, but not always with the same runtime semantics.
This means migration is not a decorator-swap exercise. Some patterns translate directly, some need approximation, and some require honest redesign.
Every Flyte concept falls into one of these categories:
| Type | Meaning | Action |
|---|---|---|
| Direct | Clean 1:1 mapping exists | Translate automatically |
| Approximate | Conceptual equivalent exists but semantics differ | Translate with caveats noted in the migration report |
| Absent | No safe ZenML core equivalent | Flag for human review with redesign suggestions |
See references/concept-map.md for the full mapping tables.
Ask the user for the Flyte source before doing anything else. The ideal input set is:
LaunchPlan definitionsImageSpec or other container/image configurationtask_config=..., plugin resources, external job specs)Read everything thoroughly. For each workflow, identify:
@task, @workflow, nested workflows, subworkflowsFlyteFile, FlyteDirectory, StructuredDataset, FlyteSchema@dynamic, map_task(), conditional(), @eagercache=True, cache_version, timeout, interruptible=TrueResources, ImageSpec, per-task images, ContainerTask, plugin task_configLaunchPlan, default_inputs, fixed_inputs, schedules, notificationsreference_task, reference_workflow, reference_launch_planFor each component identified in Phase 1, classify it as direct, approximate, or absent. Use the quick guide below and the full mapping tables in references/concept-map.md.
Use direct sparingly. In Flyte migrations, many things are mechanically easy to rewrite but still semantically different enough that they belong in the approximate bucket.
Usually straightforward translations (often still classified as approximate):
@task -> @step@workflow -> @pipelineStepRetryConfigResourceSettingsApproximate translations (translate with caveats):
@dynamic -> @pipeline(dynamic=True)map_task() -> .map() inside a dynamic pipelineconditional() -> dynamic pipeline branching with .load()StructuredDataset / FlyteSchema -> dataframe or table artifactFlyteFile / FlyteDirectory -> Path artifact or wrapper type + materializerImageSpec / per-task image settings -> DockerSettingsLaunchPlan scheduling/defaults -> pipeline defaults + ScheduleWhen in doubt, classify Flyte decorator-level concepts as approximate and explain the runtime difference in the migration report.
Absent / needs redesign (flag for human review):
@eagerContainerTaskreference_task, reference_workflow, reference_launch_planinterruptible=Truemap_task(min_success_ratio=...)Before writing code, present a concrete summary:
"Here's what I found in your Flyte code:
- Direct translations (will migrate cleanly): [list]
- Approximate translations (will work but with noted caveats): [list]
- Needs redesign (cannot auto-migrate safely): [list with brief explanation]
Shall I proceed with the migration?"
If there are HIGH-severity flags, explain them in story form: what the Flyte code was relying on, what ZenML does differently, and what redesign is safest.
Translate the Flyte workflow into a ZenML project. Follow these conventions strictly.
Every migrated project MUST use this layout:
migrated_pipeline/
├── steps/ # One file per step
│ ├── extract.py
│ ├── transform.py
│ └── load.py
├── pipelines/
│ └── my_pipeline.py # Pipeline definition
├── materializers/ # Only when special Flyte types need them
├── configs/
│ ├── dev.yaml
│ └── prod.yaml
├── run.py # CLI entry point (argparse, not click)
├── README.md
└── pyproject.toml
This matches the zenml-pipeline-authoring skill's conventions. Key rules:
steps/run.py uses argparsepyproject.toml uses zenml>=0.94.1 and requires-python = ">=3.12"configs/dev.yaml and configs/prod.yamlREADME.md explaining the migrated pipeline and what still needs human reviewzenml init at project rootSee references/code-patterns.md for detailed side-by-side examples. The core rules are:
@task -> @step@workflow -> @pipeline@dynamic -> @pipeline(dynamic=True)map_task() -> .map() only inside dynamic pipelinesconditional() -> .load()-driven branching in dynamic pipelinesResources(...) -> ResourceSettings(...)ImageSpec(...) -> DockerSettings(...)retries=N -> StepRetryConfig(max_retries=N, ...)Schedule(...)These choices should be deliberate, not improvised:
FlyteFile / FlyteDirectory
Path artifactstrStructuredDataset / FlyteSchema
pd.DataFrame, polars.DataFrame, or pyarrow.TableExternally managed data
register_artifact / ExternalArtifactExotic Python objects
CloudpickleMaterializer can be a temporary unblocker, but never present it as the intended long-term production stateKeep migration comments brief and useful:
# Migration note: for short caveats# TODO(migration): for items requiring user actionMIGRATION_REPORT.md, not in large inline commentsWhen translating an approximate pattern, add a short inline note that explains the semantic difference:
@step
def read_remote_input(path: Path) -> pd.DataFrame:
# Migration note: the original Flyte workflow used StructuredDataset
# metadata to control reader behavior. This ZenML step now assumes the
# artifact is a pandas DataFrame and validates the expected columns here.
...
For patterns with no safe ZenML equivalent:
# TODO(migration) comment# TODO(migration): UNSUPPORTED -- Flyte ContainerTask with file-based IO
# contract. ZenML has no first-class raw container task primitive here.
# Redesign this as a wrapper step that submits the external job and stages IO
# explicitly, or replace it with a custom step operator.
@step
def run_external_job(...) -> None:
...
After generating the ZenML project, produce a MIGRATION_REPORT.md in the project root. Use this structure:
# Migration Report: [workflow] -> [pipeline]
## Summary
- **Source workflow**: `[workflow_name]`
- **Target pipeline**: `[pipeline_name]`
- **Tasks migrated**: X direct, Y approximate, Z flagged
- **LaunchPlans reviewed**: N
- **Plugin-backed tasks reviewed**: M
## Direct Translations
| Flyte Concept | ZenML Target | Notes |
## Approximate Translations
| Flyte Concept | ZenML Target | What Changed |
## Flagged for Review
| Flyte Pattern | Severity | Issue | Suggested Redesign |
## Type and Artifact Mapping
| Flyte type/pattern | ZenML representation | Notes |
## Scheduling and LaunchPlan Mapping
## Infrastructure and Containerization Mapping
## Limitations and Key Differences
## What's NOT Migrated
## What You Get for Free After Migration
## Recommended Next Steps
In the report, put Limitations and Key Differences before What You Get for Free After Migration so the user sees the caveats first.
After migration is complete, always include a "Recommended Next Steps" section in the report AND communicate it to the user.
zenml-quick-wins skillAlways suggest this first:
"Now that the migration is done, I'd recommend running the
zenml-quick-winsskill to add metadata logging, experiment tracking, alerts, and other production-readiness features."
For every flagged pattern, include the relevant ZenML docs. Common Flyte-migration links:
https://docs.zenml.io/how-to/steps-pipelines/dynamic-pipelineshttps://docs.zenml.io/how-to/steps-pipelines/schedule-a-pipelinehttps://docs.zenml.io/stacks/stack-components/orchestratorshttps://docs.zenml.io/how-to/containerization/containerizationhttps://docs.zenml.io/how-to/infrastructure-deployment/auth-managementhttps://docs.zenml.io/concepts/artifacts/materializershttps://docs.zenml.io/how-to/deployment/deployment"For easier access to ZenML docs while you finish the migration, you can install the ZenML docs MCP server:
claude mcp add zenmldocs --transport http https://docs.zenml.io/~gitbook/mcp"
When there are 2+ HIGH-severity flags, generate a copy-paste Slack message for zenml.io/slack that includes:
**Flyte -> ZenML Migration Help**
I'm migrating a Flyte workflow (`[workflow_name]`) that uses [patterns]. The migration skill flagged these as needing redesign:
1. **[Pattern]**: [brief description + code snippet]
- Suggested workaround: [X]
- Why this matters: [what changes without a proper solution]
2. **[Pattern]**: [brief description + code snippet]
- Suggested workaround: [Y]
I've implemented the workarounds above, but I'm wondering if there's a better approach, an upcoming feature, or a pattern I'm missing.
When the migration reveals a real capability gap in ZenML, offer to open a GitHub issue on zenml-io/zenml using gh issue create.
/simplifyAfter migration is complete, always suggest running /simplify on the generated code. Migration often leaves temporary comments, repeated wrappers, and verbose explanations that should be cleaned up.
zenml-pipeline-authoring for deeper customizationRecommend zenml-pipeline-authoring for:
Always mention the relevant ones in the migration report.
Flyte's special types are part of the transport layer. ZenML relies on Python types plus materializers. That changes:
FlyteFile / FlyteDirectory often need more than a plain string pathStructuredDataset and FlyteSchema may carry metadata that has to be recreated intentionallyExternalArtifact / register_artifact are often the cleanest migration targetFlyte dynamic features are runtime engine features. ZenML dynamic pipelines load values back into Python to shape the graph. That means:
@dynamic is only an approximate matchmap_task() and .map() are similar in goal but not identical in backend semanticsconditional() must be treated carefully when it depends on runtime valuesLaunchPlan is not just Flyte's cron object. It is also the registered execution surface with defaults, fixed inputs, and notifications. ZenML's Schedule only covers the scheduling slice. The rest has to be made explicit with pipeline defaults, config, deployments, or wrapper pipelines.
Flyte makes raw container execution and plugin-backed task contracts first-class. ZenML is stronger at portable Python pipelines plus stack abstractions. This means:
ImageSpec and DockerSettings are close in spirit, not identical in lifecycleContainerTask is a redesign boundary| Anti-pattern | Why it's wrong | What to do instead |
|---|---|---|
Translating FlyteFile to str | Loses artifact semantics and remote/localization behavior | Use Path, plus metadata or a wrapper type if URI semantics matter |
Translating StructuredDataset to Any | Destroys the tabular contract and hides schema drift | Use explicit dataframe/table types |
Replacing map_task() with a plain Python loop in a static pipeline | Removes parallel semantics and per-item behavior | Use a dynamic pipeline with .map() or redesign |
Replacing conditional() with plain if in a non-dynamic pipeline | Breaks runtime branch semantics | Use @pipeline(dynamic=True) or move the decision into a step |
Mapping LaunchPlan to only Schedule | Loses fixed inputs, defaults, notifications, and trigger identity | Split it into config, wrapper pipelines, deployments, and schedules |
Treating interruptible=True as "just add retries" | Spot/preemptible behavior is not the same as retry behavior | Move spot handling to the target backend config |
Rewriting ContainerTask as a normal Python step without reviewing IO protocol | Changes the execution contract completely | Wrap the external job or build a custom operator |
Ending the migration on CloudpickleMaterializer | It is a temporary escape hatch, not a stable design | Create proper materializers or simplify the data contract |
| Mirroring Flyte plugin config fields 1:1 | Plugin semantics do not carry over automatically | Migrate the business outcome, not the config object |
For topics beyond migration (stack setup, artifact handling, deployment, productionization), query the ZenML docs at https://docs.zenml.io.