Import and export
AutoAir stores every DAG as a spec (JSON), which is the document that the canvas, code mode and deploys work on. Importing converts an existing DAG into a spec. Exporting renders a spec in another format. The supported formats, their entry points in the UI and CLI, and the constructs that survive the conversion:
| Format | Import | Export | Lossless |
|---|---|---|---|
| Python DAG file | UI, CLI, from Airflow | UI, CLI (autoair generate) | Export: yes. Import: most constructs (see below). |
| Airflow native YAML DAG (Airflow 3.4, AIP-85) | UI, CLI | UI, CLI | Only for DAGs that YAML can express. Otherwise export reports errors. |
| dag-factory YAML (astronomer/dag-factory 1.0+) | No | UI, CLI | No. One-way, with warnings. |
AutoAir spec JSON (.autoair.json) | UI | UI | Yes, including overlays and layout |


Import in the UI
Open File → Import and choose From Python file…, From Airflow…, From YAML… or From AutoAir spec (JSON)…. The command palette entry Import DAG… opens the same dialog. The Import a DAG dialog ("Convert an existing DAG into an editable AutoAir document.") has one tab per source:
| Tab | Input | Notes |
|---|---|---|
| Python | Paste the DAG file, or Upload file | "it is parsed, not executed by your browser". The server imports it in a sandbox (see below). |
| From Airflow | Airflow DAG id, then Fetch | "Import a DAG that already runs in Airflow from its source file." AutoAir fetches the source through Airflow's API with your credentials. |
| YAML | Paste or Upload file | An Airflow native YAML DAG |
| AutoAir spec | Paste or Upload file | "Open an exported .autoair.json file (older spec versions are migrated)." |
Then:
- Click Import.
- The result lists every DAG found in the source, each with its warnings ("N warnings") or not importable and the reason. "No DAGs were found in this file." means the source defines none.
- Click Open as unsaved on the DAG you want. If your current document has unsaved changes, AutoAir asks before discarding them. The spec is laid out automatically when it has no positions.
- Review the canvas and the Problems panel, then deploy.
Nothing is written by an import. The original file in Airflow is untouched until you deploy. When the imported DAG's dag_id matches the file's, the deploy then replaces it with the AutoAir-generated file. AutoAir refuses to overwrite a file it did not create, so a DAG imported from Airflow usually needs a new id, or you remove the original file yourself.
Unmanaged DAGs
Opening a DAG id that exists in Airflow but not in AutoAir, for example through a deep link or the AutoAir tab on an Airflow DAG page, shows <dag_id> is not managed by AutoAir. That dialog offers:
- Import this DAG from Airflow: the same import as the From Airflow tab.
- Start an empty DAG.
- Open in code editor, when the DAG cannot be converted to the visual editor: "Open its code, fix or simplify it and apply." See Code mode.
Import Python DAGs
The Python importer runs the file with Airflow's DagBag in a sandboxed subprocess on the AutoAir server, then reverse-engineers each DAG object it defines. This recovers operators and their arguments (including those of operator base classes and XCom references nested in lists and dictionaries), @task functions with their source, task groups, dependencies and their Labels, schedule, params with their schema keywords, DAG settings (dagrun_timeout, is_paused_upon_creation, fail_fast, max_consecutive_failed_dag_runs, render_template_as_native_obj, owner_links) and task settings (retries, retry_delay, execution_timeout, retry_exponential_backoff, executor, executor_config, inlets and the others the inspector shows). A task argument that overrides default_args is kept even when it equals Airflow's default (retries=0 against default_args={"retries": 3}). Because importing executes the file, it requires:
- the permission
code:execute(Author, Admin), allow_python_code(AUTOAIR_ALLOW_PYTHON_CODE,features.python_code). Otherwise the import is refused with "importing Python DAGs executes their code and is disabled by policy (allow_python_code)",features.imports.
The subprocess gets an environment with AutoAir's secrets removed and is killed after AUTOAIR_PYTHON_IMPORT_TIMEOUT seconds (default 90; the error reads "import timed out after …s"). Module-level code in the file does run, including imports, network calls and file access, with the AutoAir server's permissions. Only import files you would let Airflow parse.
From Airflow imports have the same requirements. They also need features.airflow and Airflow read access to the DAG's source. When the file defines several DAGs, only the requested one is kept.
What does not round-trip
The importer reports what it could not convert as warnings on the DAG. Common ones:
| Warning | What to do |
|---|---|
| "<task>: mapped argument 'x' cannot be imported; the task is imported without mapping (re-create the mapping in the builder)" | Mapped tasks (.partial().expand() / .expand_kwargs()) are imported with their operator, partial() arguments and mapping. This warning means an expand value is not a list or a plain XCom reference (for example .map() or .zip() on an XCom). Re-create the expand in the inspector. |
| "<task>: python_callable is a lambda and was not imported; …" / "… is a partial, not a plain function; …" | Callables passed as operator arguments (python_callable= of PythonOperator, BranchPythonOperator, ShortCircuitOperator, PythonSensor and others) are imported when they are plain functions: one defined in the DAG file becomes an inline function (its source, without decorators, plus the file's imports it uses), one imported from another module a Python reference. A lambda or a functools.partial is not imported: define it with def in the file, or set the value again in the inspector. |
| "<task>: python_callable: fn uses names defined elsewhere in the DAG file (…)" | The function was imported, but module-level names it reads (constants, other helper functions) are not carried with it. Inline them in the function or move them to an importable module. |
| "<task>: the function uses names defined elsewhere in the DAG file (…)" | A TaskFlow (@task) function reads module-level names that are not imports, such as constants or helper functions. The file's imports it uses are carried into the task body (import math becomes an import inside the function); other names are not. Inline them or move them to an importable module. |
| "<task>: parameter *args is not supported …" / "… positional argument(s) beyond the named parameters (*args) are not imported" / "default of parameter 'x' is not a literal …" | TaskFlow call arguments are stored by name: positional arguments bind to the parameters in order, and keyword-only and positional-only parameters and their literal defaults are kept. Values that only reach *args / **kwargs, and defaults that are not literals, are not imported; set them on the task. |
| "<task>: value of type X dropped" | Arbitrary Python objects as arguments. Set the value again in the inspector. |
| "<task>: source of the python callable is unavailable" | The function's source could not be found, for example when it is built dynamically. |
| "timetable X is not supported; imported without schedule" | Custom timetables. Choose a schedule in the builder. |
| "unsupported asset condition …" | Complex asset expressions. Rebuild the condition in the schedule editor. |
| "deadline alerts are not imported" / "DAG callbacks are not imported" / "<task>: task callbacks are not imported" | Re-add them under callbacks, retries and deadlines. |
| "<task>: argument X is not imported" | A task argument the spec has no field for, such as email, do_xcom_push, max_retry_delay, wait_for_downstream, task-level params or retry_policy. Set it again in the inspector where it has a field, or keep the DAG as code. |
| "<task>: operator class X is defined in the DAG file; move it to an importable module …" | The task is imported, but the generated DAG cannot import a class that only exists in the original file. Move the class to a module on the DAG folder's path. |
| "<task>: retry_delay: duration … is not a valid timeout/delay; not imported" | A negative, infinite or not-a-number duration. Durations larger than datetime.timedelta holds are imported as its largest whole number of seconds. |
Edges from or to a task group are imported as edges between the group's tasks, with the same dependencies and label. A converted DAG that fails AutoAir's validation is reported as not importable with "converted spec is invalid: …". Dynamic DAG factories (loops that create many DAGs) import as the individual DAGs they produced at parse time. The loop itself is lost.
Airflow native YAML DAGs
AutoAir reads and writes Airflow's native YAML DAG format (Airflow 3.4, AIP-85, schema https://airflow.apache.org/schemas/dag/2026-10-30.json). YAML import does not execute code and does not need code:execute.
Import warnings include "run: stub tasks are imported as EmptyOperator", "timetable X imported as no schedule", "timetable arguments X are not imported", "default_args X are not imported" (files with a top-level default_args mapping, which older AutoAir versions wrote) and "X 'value' is not a duration AutoAir reads; dropped". A document without dag_id is rejected ("not an Airflow YAML Dag document (missing dag_id)").
Export (File → Export → Airflow YAML DAG, or the Airflow YAML tab of the Code panel) is refused for anything YAML cannot express. The toast reads "N constructs cannot be expressed in Airflow YAML", and the list includes:
- Python tasks, inline functions, script files and Python references
- Non-operator nodes, dynamic task mapping, setup/teardown, retry policies, task callbacks, asset inlets and outlets
- Every schedule except none, a preset and a plain cron expression: multi-cron, interval, cron with an interval, events, asset, asset-or-time, asset-and-time, partitioned cron and partitioned asset schedules
- DAG callbacks, deadlines,
owner_linksand OpenLineage enable/disable - Durations that are not a whole number of seconds (
retry_delay,execution_timeout,dagrun_timeout), andtimedeltavalues of operator arguments other thanresponse_timeoutofHITLOperatorandexecution_deltaof the external task sensor and marker
Durations are written as Airflow's duration strings (retry_delay: 5m, dagrun_timeout: 2h), which Airflow's YAML importer parses for arguments typed timedelta. YAML DAGs have no default_args: the DAG's default arguments are exported as a task template named default_args that every task extends, and task values still override them. Start and end dates are written with their UTC offset and the DAG timezone as the schedule's timetable timezone; importing such a file restores the DAG timezone.
Exports that succeed can still warn: "task groups are flattened: YAML Dags have no task groups (task ids keep the group prefix)", "edge label 'x' is dropped", "param schemas (type, enum, ...) are dropped; only defaults are kept" and "the DAG timezone 'X' is dropped: YAML Dags take it from start_date (set a start date)". A DAG whose output setting is Airflow YAML DAG is deployed as YAML. Its settings note says "Task groups and Python tasks aren't supported in YAML."
The Airflow YAML tab of the Code panel is editable: Apply YAML to canvas imports your edits as unsaved changes ("YAML applied to the canvas (unsaved)").
dag-factory YAML
File → Export → dag-factory YAML writes <dag_id>.dag-factory.yaml for teams that use astronomer/dag-factory 1.0 or later ("One-way export for teams using astronomer/dag-factory."). It is lossy, and the toast counts the warnings: XCom references and Python references become plain strings, inline functions and files are dropped, asset schedules become a flat list, and Python tasks, custom nodes and blueprint steps are skipped with their dependencies. Every other setting the file does not carry is a warning too: edge labels, DAG settings such as params, dagrun_timeout, callbacks, deadlines and owner_links, schedules other than none, preset, cron, interval and asset schedules, a cron schedule's timezone, data intervals or run_immediately, and per task setup/teardown, callbacks, retry policies, asset inlets and outlets and expand_kwargs mappings. Edges from or to a task group are exported as dependencies of the group's tasks, and disabled tasks are left out as in the Python output. AutoAir cannot import dag-factory files.
Export
File → Export:
| Menu item | File | Notes |
|---|---|---|
| Python file (.py) | <dag_id>.py | The generated DAG file, as deployed with the base definition (no environment overlay). Fails with "Fix the problems first: no code could be generated." while the DAG has errors. |
| Airflow YAML DAG | <dag_id>.yaml | See above |
| dag-factory YAML | <dag_id>.dag-factory.yaml | See above |
| AutoAir spec (JSON) | <dag_id>.autoair.json | The complete document, including environment overlays, notes and layout. Re-import it with From AutoAir spec (JSON)…. |
The Code tab in the bottom panel shows the same renderings live: Python, Airflow YAML, dag-factory and Spec JSON.
Command line
# Python DAG file -> one <dag_id>.autoair.json per DAG in the file
autoair import-python dags/legacy_etl.py -o specs/
# Airflow YAML DAG -> spec JSON on stdout (issues on stderr)
autoair import-yaml dags/orders.yaml > specs/orders.autoair.json
# spec -> Python / Airflow YAML / dag-factory
autoair generate specs/orders.autoair.json -o dags/orders.py
autoair export-yaml specs/orders.autoair.json > dags/orders.yaml # exit 1 if not lossless
autoair export-dag-factory specs/orders.autoair.json > orders.dag-factory.yaml
# validate a spec (optionally with the DagBag import check)
autoair validate specs/orders.autoair.json --import-check --env prod
import-python prints "import error: …", "<dag>: warning: …" and "<dag>: failed: …" lines on stderr. Commands of a disabled feature (features.imports, features.python_code) exit with code 2. The import, export and generate commands work on the base definition. autoair validate --env <name> validates with an environment's overlay and rules. The full list is in the CLI reference.
Bulk import
File → Import → From a DAG repository… converts every DAG of a repository in one pass. The Import a DAG repository dialog takes one of two sources:
| Source | Fields | Notes |
|---|---|---|
| Folder | Choose folder… | Only .py files are uploaded; hidden folders and __pycache__ are skipped. The chosen folder is treated as the repository root, so helper modules next to the DAGs resolve as they do in a DAG folder. |
| Git repository | Repository URL (https), Branch or tag, Folder in the repository, Access token (private repositories) | The server makes a shallow clone. The token is sent to the git host for this clone only and is never stored or logged. |
- Choose the source and click Scan. Each candidate file is imported with the same Python importer as a single file (Airflow's DagBag in an isolated subprocess without AutoAir's secrets). Nothing is saved yet.
- Review the result. The summary counts files, convertible DAGs, DAGs that already exist and files that failed. Each file shows a result (imported, no DAG, excluded, error, not scanned), its DAGs and their warnings; a
dag_iddefined in two files is flagged. - Select the DAGs to deploy. A DAG that already exists is only replaced when you tick overwrite existing for it; otherwise it is deployed create-only and reported as "exists - not overwritten".
- Click Deploy N DAGs. Each DAG goes through the normal deploy path, so validation, policy and approvals apply; the per-DAG outcome is deployed, exists - not overwritten, invalid, ejected, awaiting approval or failed.
The menu entry requires the code:execute and spec:write permissions and the python_code feature, because importing executes the DAG files. The server applies these settings (reference):
| Setting | Default | Effect |
|---|---|---|
interchange.bulk_max_files | 500 | Python files imported per request; more are reported as not scanned. |
interchange.bulk_workers | 4 | Files imported concurrently, one isolated subprocess each. |
interchange.bulk_exclude | tests, hidden folders, __pycache__, node_modules | fnmatch patterns over repository-relative paths that are never imported. Files that do not contain both airflow and dag are skipped as well (Airflow's safe-mode heuristic). |
interchange.bulk_git_allowed_hosts | ["*"] | Hosts that may be cloned (https only), for example ["github.com", "gitlab.acme.internal"]. An empty list disables git sources. |
interchange.bulk_git_timeout | 120 | Seconds a shallow clone may take. |
Restrict interchange.bulk_git_allowed_hosts in production: the clone runs on the AutoAir server. The API behind the dialog is POST /api/interchange/bulk/scan (operation bulkImportDags); it returns the per-file result and never writes. The CLI has no bulk command; in a script, loop over files:
for f in dags/*.py; do autoair import-python "$f" -o specs/; done
Limits, permissions and API
| Setting | Default | Description |
|---|---|---|
AUTOAIR_FEATURES__IMPORTS | true | Python, YAML, spec and Airflow imports (UI, API, CLI) |
AUTOAIR_PYTHON_IMPORT_TIMEOUT | 90 | Seconds a Python import may run |
AUTOAIR_INTERCHANGE__MAX_SOURCE_CHARS | 2000000 | Largest source accepted ("source is larger than … characters") |
AUTOAIR_INTERCHANGE__YAML_MAX_NODES | 200000 | YAML node limit (guards against alias expansion attacks) |
limits.rate_limits.import | 60 | Imports per user and window |
| Method and path | Permission |
|---|---|
POST /api/interchange/python/import ({source, filename}) | builder:use + code:execute |
POST /api/interchange/airflow/{dag_id}/import | builder:use + airflow:read + code:execute; 404 while features.airflow or features.imports is off |
POST /api/interchange/yaml/import ({source}) | builder:use |
POST /api/interchange/spec/import ({source}) | builder:use |
POST /api/interchange/yaml/export | builder:use |
POST /api/export/dag-factory, POST /api/codegen | builder:use |
GET /api/schema/spec | builder:use (JSON Schema of the spec) |
See the spec format and the REST API reference.