How to use
- Copy the installation command and review the source package before installing it in your own agent environment.
- Check the source document for required tools, credentials and supported hosts.
- Describe your input and desired deliverable; review the output against the source material.
npx skills add https://github.com/kestra-io/agent-skills --skill migrate-airflow-kestraBefore you start
- Installation does not include a model subscription, API allowance or permission to access your files.
- Repository stars and installation counts describe activity, not an independent quality or security test.
Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.
Migrate Airflow DAG to Kestra
Migrate an Apache Airflow DAG file to a production-ready Kestra flow with proper namespace files, correct task ordering, and schema-validated YAML.
When to use
Use this skill when the request involves migrating from Apache Airflow to Kestra, including:
- Migrating an Airflow DAG (
.py) to a Kestra flow YAML - Translating
@task-decorated functions or operators into Kestra tasks - Converting Airflow DAG dependencies into Kestra's sequential/parallel task structure
Required inputs
- Path to the Airflow DAG
.pyfile ($ARGUMENTS) - Output directory for generated files (defaults to
kestra-migrate/next to the DAG) - Target Kestra namespace (ask if not provided; default to
company.team)
Workflow
Step 1 — Read and analyse the Airflow DAG
Read the DAG file in full. Extract:
- DAG metadata:
dag_id,schedule,default_args,tags - Tasks: for each
@task-decorated function or operator, note:
- Task ID (function name or
task_id) - Python imports and dependencies (
pippackages used) - Whether it contains important business logic (pandas transforms, ML, data processing, API calls, multi-step computation) — these become namespace files
- Whether it is a trivial one-liner — these can be inlined
- Dependencies: map
>>operators,set_upstream/set_downstreamcalls, and direct task invocations into an ordered dependency graph - Parallelism: identify tasks that share the same upstream and have no dependency on each other — these must be wrapped in
io.kestra.plugin.core.flow.Parallel - Data passing: identify XCom pushes/pulls or return values passed between tasks — these become
outputFiles→inputFilesin Kestra
Step 2 — Fetch and validate the Kestra schema
curl -s https://api.kestra.io/v1/plugins/schemas/flow -o /tmp/kestra_schema.jsonExtract the definitions needed for this flow:
python3 -c "
needed = [
'io.kestra.plugin.scripts.python.Commands',
'io.kestra.plugin.core.flow.Parallel',
'io.kestra.plugin.core.flow.WorkingDirectory',
'io.kestra.plugin.core.trigger.Schedule',
]
content = open('/tmp/kestra_schema.json').read()
for t in needed:
idx = content.find(f'\"{ t }\"')
if idx >= 0:
print(f'=== {t} ===')
print(content[idx:idx+3000])
"Do not generate any YAML until schema definitions have been read and validated.
Step 3 — Create namespace files
For every task that contains important business logic (see rule below), extract the Python function body into a standalone script under <output-dir>/scripts/.
Namespace file rules:
- One file per Airflow task, named after the task:
<task_id>.py - The script must be self-contained: read inputs from local files (e.g.
products.json), write outputs to local files (e.g.category_stats.json) - Remove all Airflow imports (
from airflow …,from pendulum …) — they have no equivalent - Replace XCom
ti.xcom_push/ return values withjson.dump(result, open("output.json", "w")) - Replace XCom
ti.xcom_pullwithjson.load(open("input.json")) - Keep all business logic, pandas transforms, API calls, and helper functions intact
When to use a namespace file vs inline:
| Situation | Decision |
|---|---|
| Task has pandas/numpy/ML logic | Namespace file |
| Task makes HTTP requests with processing | Namespace file |
| Task has > 10 lines of logic | Namespace file |
| Task is a single API call or shell command | Inline in flow |
Task is a BashOperator one-liner | Inline in flow |
Step 4 — Generate the Kestra flow YAML
Apply all rules below. Write the flow to <output-dir>/flow.yaml.
Generation rules
Task structure — the YAML list marker (-) MUST be on the id line, type MUST be the very next line
This is the single most important formatting rule. Every task block in tasks: (including nested tasks inside Parallel) must follow this exact pattern:
- The YAML sequence dash (
-) goes on the `id:` line — never ontype:or any other property. - `type:` is always the second line, indented to align with
id:(no dash). - All remaining properties (
containerImage,namespaceFiles,commands, etc.) follow aftertype:, at the same indentation level astype:.
# ✅ CORRECT — dash on id, type immediately after, then everything else
- id: fetch_products
type: io.kestra.plugin.scripts.python.Commands
containerImage: python:3.11
namespaceFiles:
enabled: true
include:
- scripts/fetch_products.py
commands:
- python scripts/fetch_products.py
# ❌ WRONG — dash on type instead of id
- type: io.kestra.plugin.scripts.python.Commands
id: fetch_products
# ❌ WRONG — dash on containerImage
- containerImage: python:3.11
id: fetch_products
type: io.kestra.plugin.scripts.python.Commands
# ❌ WRONG — dash on namespaceFiles
- namespaceFiles:
enabled: true
id: fetch_products
type: io.kestra.plugin.scripts.python.Commands
# ❌ WRONG — dash on commands
- commands:
- python scripts/fetch_products.py
id: fetch_products
type: io.kestra.plugin.scripts.python.Commands
# ❌ WRONG — dash on dependencies
- dependencies:
- pandas
id: fetch_products
type: io.kestra.plugin.scripts.python.Commands
# ❌ WRONG — type is not the second property
- id: fetch_products
containerImage: python:3.11
type: io.kestra.plugin.scripts.python.Commands
# ❌ WRONG — extra blank line between id and type
- id: fetch_products
type: io.kestra.plugin.scripts.python.CommandsThis rule applies everywhere a task appears: top-level tasks:, inside Parallel.tasks:, inside WorkingDirectory.tasks:, and in triggers: blocks. No exceptions.
Container image — always python:3.11
Every Python task (io.kestra.plugin.scripts.python.Commands) must specify:
containerImage: python:3.11Never omit this property. Never use python:3.13-slim or any other image.
Namespace files — always use for important Python logic
For tasks backed by a namespace file, always declare:
namespaceFiles:
enabled: true
include:
- scripts/<task_id>.pyReference the script in commands:
commands:
- python scripts/<task_id>.pyData passing between tasks
Use outputFiles to capture JSON written by a script and store it in Kestra internal storage:
outputFiles:
- result.jsonInject that output into a downstream task via inputFiles:
inputFiles:
result.json: "{{ outputs.<upstream_task_id>.outputFiles['result.json'] }}"Parallelism — mirror the Airflow fan-out/fan-in
Tasks with a shared upstream and no mutual dependency must be wrapped in io.kestra.plugin.core.flow.Parallel:
- id: compute_analytics
type: io.kestra.plugin.core.flow.Parallel
tasks:
- id: compute_category_stats
type: io.kestra.plugin.scripts.python.Commands
containerImage: python:3.11
...
- id: compute_brand_stats
type: io.kestra.plugin.scripts.python.Commands
containerImage: python:3.11
...Tasks that are independent at the fetch/ingest stage should also run in parallel.
Schedule trigger
Map Airflow schedule to a Kestra Schedule trigger:
| Airflow | Kestra cron |
|---|---|
@daily | @daily |
@hourly | @hourly |
0 6 * * * | 0 6 * * * |
None | Omit trigger entirely |
triggers:
- id: daily
type: io.kestra.plugin.core.trigger.Schedule
cron: "@daily"DAG metadata mapping
| Airflow | Kestra |
|---|---|
dag_id | id |
default_args.owner | # comment or label |
tags | labels |
description | description |
catchup=False | No equivalent needed |
retries | Not set at flow level; handle per-task if needed |
Schema compliance
- Use only task types and properties explicitly defined in the fetched schema
- Never invent property names
- Property keys must be unique within each task block
Secrets and credentials
- Never hardcode tokens, passwords, or API keys
- Use
{{ secret('SECRET_NAME') }}Pebble expressions
Output structure
<output-dir>/
├── flow.yaml # Kestra flow
└── scripts/
├── <task_id>.py # One file per task with business logic
└── ...flow.yaml skeleton
id: <dag_id>
namespace: <namespace>
description: <dag description>
tasks:
# Stage 1 — parallel fetch
- id: fetch_data
type: io.kestra.plugin.core.flow.Parallel
tasks:
- id: <task_id>
type: io.kestra.plugin.scripts.python.Commands
containerImage: python:3.11
namespaceFiles:
enabled: true
include:
- scripts/<task_id>.py
dependencies:
- <pip-package>
commands:
- python scripts/<task_id>.py
outputFiles:
- <output>.json
# Stage 2 — parallel transforms
- id: compute_analytics
type: io.kestra.plugin.core.flow.Parallel
tasks:
- id: <task_id>
type: io.kestra.plugin.scripts.python.Commands
containerImage: python:3.11
namespaceFiles:
enabled: true
include:
- scripts/<task_id>.py
inputFiles:
<input>.json: "{{ outputs.<upstream_id>.outputFiles['<input>.json'] }}"
dependencies:
- <pip-package>
commands:
- python scripts/<task_id>.py
outputFiles:
- <output>.json
# Stage 3 — sequential summary
- id: <final_task_id>
type: io.kestra.plugin.scripts.python.Commands
containerImage: python:3.11
namespaceFiles:
enabled: true
include:
- scripts/<final_task_id>.py
inputFiles:
<a>.json: "{{ outputs.<task_a>.outputFiles['<a>.json'] }}"
<b>.json: "{{ outputs.<task_b>.outputFiles['<b>.json'] }}"
dependencies:
- <pip-package>
commands:
- python scripts/<final_task_id>.py
outputFiles:
- <output>.json
triggers:
- id: schedule
type: io.kestra.plugin.core.trigger.Schedule
cron: "<mapped-cron>"Deploying with kestra-ops
After generating the files, if the user wants to deploy, use the kestra-ops skill. Key note on namespace file uploads: always upload files individually to avoid path nesting issues:
for f in <output-dir>/scripts/*.py; do
name=$(basename "$f")
kestractl nsfiles upload <namespace> "$f" "scripts/$name" --override
doneDo not use directory upload (kestractl nsfiles upload <ns> ./scripts scripts) — it nests the directory name, producing scripts/scripts/<file> instead of scripts/<file>.
Example prompts
- "Migrate
dags/ingest_pipeline.pyfrom Airflow to Kestra, output tokestra/" - "Convert this Airflow DAG to a Kestra flow in the
analytics.productsnamespace" - "Migrate the Airflow DAG at
dags/etl.pyand deploy it using kestra-ops"



