Generating Airflow DAGs dynamically from configuration

Learn how to build many similar Airflow DAGs from YAML or database config without slowing the DAG parser, and when dynamic task mapping is the better tool.

On this page

How to generate Airflow DAGs dynamically from configuration safely

Generating DAGs from configuration works when two conditions hold: the parser stays cheap, and each generated DAG stays isolated from the others. A team with ten or fifty pipelines that differ only in table names, schedules or owners can avoid copy-paste sprawl by keeping one template and one config source, but the generator itself runs every time Airflow parses the DAG folder. Every database query, Variable read or network call placed at module level runs on each parse cycle, so the generator should read a static file or a precomputed config and do nothing heavier. The Airflow best practices state that top-level DAG code should avoid database access, heavy computation and network calls, and that Variables should not be read at parse time, so confirm those rules against your own generator before you scale it.

The Airflow documentation describes a pattern where generated DAG objects are assigned into the module's globals() so the scheduler can discover them. That mechanism works, but the FAQ advises against putting many DAGs in one file because Airflow favors isolation between DAGs. Decide early whether you want one DAG per file, generated from a loop in a small set of files, or many DAGs per file for a narrow case. One DAG per file gives clearer failure isolation and simpler diffs in version control, at the cost of more files. Whichever pattern you choose, give each generated DAG a stable dag_id derived from its config key so renames do not orphan run history, and check that a bad config entry fails one DAG rather than the whole folder.

Dynamic task mapping solves a different problem. It uses expand() to fan out one task across a runtime list of inputs inside a single DAG, and partial() to pass fixed arguments to every copy. Use it when the same pipeline must process many partitions, files or tables discovered during a run, not when you need many independent schedules or owners. The [core] max_map_length setting caps a single expansion at 1,024 tasks by default, and max_active_tis_per_dag limits how many mapped copies run at once across active runs. Before you pick a tool, ask whether the variation belongs in config that defines separate pipelines or in data that appears during a run. Confirm the current defaults for your Airflow version in the task mapping documentation.

What the DAG parser runs on every pass and what that costs

Airflow re-parses DAG files on a loop, so any code outside a task body runs again and again, including the code that generates DAGs from configuration. The four risks below are the ones the best practices name most often, and each one shows up in a different symptom on the scheduler side.

Database access at parse time

Querying the metadata database or an external database from top-level DAG code runs that query on every parse cycle. Generated DAGs multiply the cost, because one lookup per configuration entry becomes one lookup per DAG per pass. The symptom is a slow scheduler loop and a DAG list that lags behind config changes. Check that the generator reads only from a local, cached source, and move any lookup that the task needs into the task body.

Heavy computation in module code

Loops that build large structures, parse big files, or run pandas transforms at import time make every parse expensive. Generation logic should only build small DAG objects from a compact config. The symptom is parse durations that grow with config size and scheduler lag after a deploy. Measure parse time for the full config set, not for a single sample DAG.

Network calls during parsing

HTTP requests, API lookups or cloud SDK calls made while the module loads depend on external services being reachable at every parse. A timeout or rate limit can turn into import errors or missing DAGs, which look like a deployment failure. Confirm that the generator never calls the network, and that any external catalog is synced to files or a cache before parsing starts.

Reading Variables at parse time

Calling Variable.get() outside a task reads the metadata database on every parse and ties DAG structure to mutable state. A changed value can change the DAG shape between runs, which makes history hard to explain. The symptom is DAGs that appear, disappear or change task counts without a code change. Keep structural settings in versioned config files and read Variables only inside tasks.

Keep config loading cheap

Generation code runs on every parse, so the config loader should read a small, versioned file, validate it once, and return plain Python objects. Validation failures should raise clear errors that name the offending entry, because an unhandled exception is reported as an import error and the DAG disappears from the UI. Ask your team to time the parser against the full config before each release.

Purge metadata with db clean

Old run history and task instances grow the metadata database, but purging them is a maintenance task, not a parsing task. The documented way is airflow db clean, run on a schedule outside DAG files, with a retention decision made first. Confirm the retention window with your compliance owner before purging, because deleted history cannot be recovered from Airflow itself.

Choosing a generation pattern: YAML files, database config, one DAG per file or many

Config lives in YAML files inside the same repository as the DAG code, so each change follows the same commit, review and deployment path as the pipelines it drives. The DAG processor reads the files each time it parses the module, so a changed file takes effect on the next parse after it reaches the bundle.

  • Parse path: Every parse opens and reads the YAML files, so file I/O and parsing repeat on each cycle.
  • Parse time: Cost grows with file size and the number of entries that create tasks. Measure parse duration in your own environment as the file grows.
  • Versioning: Git history records each config change, and a pull request shows reviewers exactly which pipelines changed.
  • Validation: Add a schema check to CI so a malformed entry fails before merge and never reaches the scheduler.
  • Secrets: Store connection IDs and references in the file, and keep credentials in your secrets backend.

Confirm that your deployment syncs the YAML files with the DAG bundle, and that the team can roll back a config change by reverting one commit.

Configuration options for generated DAGs and mapped tasks

This table compares the settings and patterns that control how generated DAGs and mapped tasks are built and how many run at once. Read each row as a question: what does this control, and does it fit the configuration shape your team has. The main takeaway is that globals() assignment shapes DAG discovery, while expand(), max_map_length and max_active_tis_per_dag shape the size and concurrency of mapped work.

OptionWhat it controlsWhen it fits
globals() assignmentPlaces DAG objects built in a loop into a module's namespace so the DAG processor discovers them at parse timeA small, stable set of similar DAGs where each DAG should stay isolated and the config loads quickly
expand()Creates one mapped task instance per item in a list or dictionary argument, decided at run timeWork that varies per run, such as a list of files, partitions or tables discovered by an upstream task
partial()Fixes the arguments shared by every mapped instance, so only the mapped argument variesMapped tasks that share a connection, target schema or operator settings across all items
[core] max_map_lengthCaps how many mapped task instances a single expansion may create, with a default of 1,024Confirm the value in your airflow.cfg or environment variables before sizing config-driven fan-out
max_active_tis_per_dagLimits how many mapped task instances of a task run at once across all active runs of the DAGProtecting a shared database, API or warehouse from bursts when many runs map over the same items
Parse-time config readsControls whether YAML, database or Variable reads happen while the file is parsed, which repeats on every parser cycleKeep reads small and cached, and move heavy lookups into a task so parsing stays fast
airflow db cleanPurges old metadata rows, such as historical task instances and logs, to keep the metadata database smallSet retention rules with your data owners and confirm the retention window and schedule for your environment

When dynamic task mapping is the better tool than generating more DAGs

Use dynamic task mapping when the work is the same pipeline applied to a list that only exists at run time, such as partitions or files discovered by an upstream task. A @task that returns a list can feed expand() inside one DAG, so the DAG count stays flat and each item gets its own task instance with its own log, state and retry history. The alternative of generating one DAG per item makes the item list a parse-time concern and multiplies the objects the scheduler must track. Before choosing, confirm the item count will stay under the [core] max_map_length cap, which is 1,024 by default, because a single expansion beyond that limit fails.

Generate separate DAGs when each config entry needs its own schedule, owner, connection set, access boundary or failure history. Mapped task instances share one DAG, one schedule and one set of DAG-level permissions, so a source system with its own cutoff time, a team that must not see another team's runs, or a pipeline that needs to be paused independently is better served by its own DAG. Ask the owners of each config entry whether they would accept a shared pause button and a shared run history. If the answer is no, generation is the right tool, and the generator should keep the per-entry DAG count bounded and documented.

The trade-offs show up in three places. Run visibility is simpler with mapping, since one run view shows every item, but a single failing item can make the whole run look red unless you plan alerting around mapped task states. Retries are set per task, so every mapped copy shares the same retry policy, and a per-item policy requires separate DAGs or a different design. Concurrency is controlled with max_active_tis_per_dag, which caps how many mapped copies run at once across all active runs of the DAG, not only within one run. Set it deliberately: too low and a large expansion takes many hours to drain, too high and a source database receives a burst of connections. Test the chosen value against your source systems' limits in staging before production.

How to set up a config-driven DAG generator in your own Airflow

This sequence turns a folder of YAML or database-backed configuration into a set of Airflow DAGs that are validated before merge, parse quickly in the DAG processor, and map cleanly to the configuration that produced them. Each step ends with a check you can run before the next one begins.

  1. Fix the config schema and validate it in CI

    Your platform or data engineering lead defines the required fields for each pipeline entry, such as schedule, owner, source, target, and retry settings, and writes the schema as a versioned file in the DAG repository. CI validates every config file against that schema on each merge request, so a malformed entry fails review instead of failing at parse time in production. The step is done when a deliberately broken config file fails the CI job and a valid one passes.

  2. Write the generator as a pure function

    Engineers write a function that takes a parsed config entry and returns a DAG object, with no database queries, Variable reads, API calls, or file reads outside the config path at import time. Heavy or external lookups move into the tasks themselves, which run on workers rather than in the parser. The Airflow best practices explain why top-level DAG code should avoid these calls. The step is done when the generator can be called in a plain Python shell with a sample entry and returns the expected DAG without network access.

  3. Assign generated DAGs into module globals with stable IDs

    The module that loops over config entries assigns each generated DAG into its globals() under a deterministic dag_id derived from the config key, such as the pipeline name with a fixed prefix. Stable IDs keep run history, pausing state, and permissions attached to the same DAG across config reloads. The Airflow FAQ describes this assignment pattern, and it also advises keeping the number of DAGs per file small because Airflow prefers isolation between DAGs. The step is done when two consecutive parses produce identical dag_id values for the same config.

  4. Load the DAG bundle locally on the same Airflow version

    Engineers install the same Airflow version and constraints file that the scheduler uses, then load the DAG bundle with a local parse so import errors surface before deployment. Running against a different Airflow minor version can hide or create import problems, so the environment should match the production image. In Airflow 3, the DAG processor runs as a separate component, so this local check should mirror that process rather than relying on an older webserver-based habit. The step is done when the local parse reports no import errors for the full config set.

  5. Confirm the DAG count matches the config

    A short test compares the number of DAG objects discovered by the loader with the number of active entries in the config, and fails if they differ. This catches silent failures such as a generator branch that skips entries, a filename collision, or a DAG ID that overwrites another. Teams usually add the same check to the staging environment so that a count drift appears before production. The step is done when the test passes on the current config and fails when one entry is intentionally duplicated.

  6. Measure parse time in the DAG processor logs

    After deployment to staging, the platform team reviews the DAG processor logs for the time each generated file takes to parse and watches for growth as entries are added. Parse cost rises with the number of generated DAGs and with any work done during import, so this measurement shows whether the pattern still holds at production scale. Ask your Airflow administrator which log level and metrics are enabled in your deployment, since the operations guidance for tuning parsing depends on them. The step is done when you have a recorded baseline parse time per file and a threshold that triggers a review.

  7. Test removal of a config entry

    Engineers remove one entry from a test config and deploy it to staging to see what happens to the DAG that no longer has a source definition. Airflow keeps the DAG metadata and history, so the team needs a written policy: pause the DAG, archive it, or delete it, and who approves each action. The test confirms that the policy is applied and that run history remains available for audit where your retention rules require it. The step is done when the removal path is documented and the staging result matches the documented policy.

Security and operational risks in generated DAG code

Generated DAGs run whatever the configuration tells them to run, so the people and systems that can edit config are part of your security boundary. The four risks below connect who can change config, how secrets are referenced, how changes are traced, and how you notice when a change breaks the scheduler.

Who can change config

A YAML file or database row that defines a DAG effectively decides which code runs under the DAG author's identity and which connections a task can use. Treat config edits with the same review rigor as DAG code, and record who may merge config changes and who may write to a config table. Confirm in your environment whether config comes from a reviewed repository or from a writable database, and who holds that write access. Check that generated DAG identities and connection access match what each pipeline owner should be able to reach.

Secrets referenced by name

Generation code should pass connection IDs and variable keys from config into tasks, not resolve credential values while the file is parsed. Reading a Variable or secret at parse time runs against the parser on every interval, can expose values in logs or DAG structure, and ties parser health to the secrets backend. Confirm that config holds only names, that your secrets backend is reachable by workers at run time, and that no generated file contains a literal credential.

Audit trails for config changes

A generated DAG has no source file of its own, so the audit trail lives in the config history. Tie each config change to a version control commit or a database record that shows author, reviewer, time and the exact content. Keep generator code and config versioned together so you can reproduce the DAG set that ran on a given date. Confirm how long your repository history and any database audit tables are retained, and who can rewrite them.

Operational signals after changes

A malformed config entry can stop a generator, produce import errors, or silently drop or multiply DAGs. After every config change, watch import errors, the number of DAGs the parser reports, and parse duration for the generating file. A sudden drop in DAG count or a spike in import errors should block the rollout and trigger a rollback to the previous config version. Confirm which alerts exist in your monitoring stack and who is paged when they fire.

Dynamic DAG generation cost, effort and what the work needs from your team

What drives the effort of generating Airflow DAGs from configuration?

The effort follows the number of distinct config shapes, how much existing DAG code has to be restructured, and how mature your CI and staging setup already is. A single template covering one pipeline pattern is a small job. Many pipeline types with different operators, dependencies and ownership rules take longer because each shape needs its own tests, validation and review. Existing DAGs that read Variables or run queries at import time add work, because the parser must be fixed before generation can be trusted. Ask any provider to break the effort down by config shape, existing code to refactor, and the test and promotion steps they will set up, since those three items decide most of the price.

Can an in-house team build dynamic DAG generation from the Airflow docs?

Often yes, if the team already runs Airflow and has someone comfortable with Python packaging and testing. The best practices and FAQ explain the core rules: keep top-level code free of database access, network calls and Variable reads, and assign generated DAG objects into module globals so the scheduler can discover them. The harder parts are judgment calls: choosing between one DAG per file and many, deciding when dynamic task mapping fits better, and building a validation step that catches bad config before it reaches the scheduler. Teams without that experience usually benefit from a short review of their design before they commit to a pattern that is expensive to change later.

How long does dynamic DAG generation take to build?

The duration depends on scope, and no general figure applies. Scope is set by the number of config shapes, the number of pipelines in production, the state of existing code, and whether the generator must plug into an existing CI pipeline and staging environment. A narrow generator for one pipeline family is a different job from a platform-wide pattern used by several teams. DeployOpen plans most engagements as fixed-scope work, so the timeline is written into the agreed scope before work starts. Ask for the milestones, the acceptance tests for each one, and which items depend on decisions from your data owners.

What does the work need from our engineers and data owners?

Your engineers provide access to the current DAG repository, the Airflow environment and the CI system, and they review the generator design and its tests. Data owners confirm which pipelines belong in the config, the schema and allowed values for each config field, the schedule and retry rules, and who approves changes to the config. Expect a few working sessions early on to settle naming, ownership and the review path for config changes, because those choices shape the generator more than the code does. Without named owners for config changes, generated DAGs tend to drift from what the business expects.

How do we ask for a fixed-scope quote for DAG generation?

Send a written description of the pipelines in scope, the config source (YAML files, a database table or both), the number of config shapes, the current Airflow version and deployment method, and the CI and staging setup you already have. Ask the provider to state what is included, such as design, the generator, tests, documentation and handover, and what is excluded, such as changes to upstream systems. Ask which costs are one-time and which recur, for example support or retainer work after handover. DeployOpen bases every quote on a written scope, so the scope document is the thing to compare across providers.

How does dynamic DAG generation affect an upgrade to Airflow 3?

Generated DAGs must pass the same checks as hand-written ones after the upgrade, so generation code should be tested against Airflow 3 before you move. The upgrade guide recommends moving to the latest 2.x first and checking DAG code with the Ruff AIR301 and AIR302 rules, and it requires Airflow 2.7 or later before upgrading. Task code can no longer use database sessions or models directly, and SubDAGs were removed in favor of TaskGroups, so generators that emit SubDAGs or call metadata models inside tasks need changes. Ask any provider to include the generator in the upgrade test plan and to list which generated patterns the Airflow 3 changes affect.

Limits to confirm in your environment before you build

Dynamic generation is limited by a few Airflow settings and by your own parser budget, and none of them are fixed numbers you can assume. The four cards below cover the mapping cap, the concurrency cap, parse-time cost and version-specific behavior, so you can measure each one against your own DAG count and Airflow release before committing to a design.

Mapping cap for the largest job

Dynamic task mapping caps a single expansion through the [core] max_map_length setting, which defaults to 1,024 tasks. Start from your largest real fan-out, such as the number of tables, partitions or tenants one run must process, and compare it with the value configured in your environment. If a job can exceed the cap, plan to batch the work into several mapped groups or several runs rather than raising the limit without testing the scheduler load it creates.

Concurrency across active runs

The max_active_tis_per_dag setting limits how many mapped task instances of a DAG run at once across all active runs, not just within one run. Several overlapping runs of a generated DAG can therefore compete for the same slots, which can delay the runs your business cares about most. Measure how many runs overlap in your schedule, then set the limit so that critical pipelines keep capacity during peak windows.

Parse-time budget for your bundle

Every generated DAG is rebuilt each time the DAG processor parses its file, so the cost grows with the number of configuration entries, the size of each file and any database or network calls made at import. The Airflow best practices advise against reading Variables and running queries at parse time. Measure parse duration and scheduler lag on the infrastructure you run, with your actual config volume, and set a budget your team reviews after every bundle change.

Version-specific behavior and lint checks

Generation patterns behave differently across Airflow releases, and Airflow 3 changed how DAG processing and task code access metadata. Confirm the exact version your team runs against the supported versions table, then run the Ruff AIR301 and AIR302 rules that the upgrade guide recommends against your generated code. Treat these checks as a gate in CI so that a version upgrade does not silently break a generator that worked before.

How DeployOpen scopes, builds and supports config-driven Airflow DAGs

DeployOpen approaches dynamic DAG generation as an engineering scope, not a quick script. We agree the configuration source, the parse-time limits and the handover in writing, then build and test the generator inside infrastructure you control. Engagements are fixed-scope, with ongoing support available as a retainer.

How we scope

  • We inventory the DAG families, configuration sources and owners that the generator must serve.
  • We agree whether one DAG per file, many DAGs per file or dynamic task mapping fits each pipeline family.
  • We record parse-time limits, expected DAG counts and the Airflow 3 deployment layout in the written scope.

How we build

  • We write the generator to read configuration without database access, network calls or Variable reads at parse time.
  • We add loader tests that confirm every generated DAG imports cleanly and carries the expected tasks and schedules.
  • We set mapping limits and concurrency caps, such as max_map_length and max_active_tis_per_dag, against the workload agreed in scope.

How we hand over

  • You receive the generator source, the configuration schema, and examples for adding a new pipeline without code changes.
  • Documentation covers parse-time behavior, how to test a configuration change in staging, and how to roll back a bad change.
  • We walk your engineers through the parser metrics and the checks they should watch after each release.

How we support

  • Support can be added as a retainer for schema changes, Airflow upgrades and new pipeline families.
  • We review parser timing and generated DAG counts when Airflow or the configuration volume changes.
  • Ask any provider to confirm response terms, upgrade coverage and what is included in a retainer before you sign.

Sources and further reading

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.