Airflow Dynamic DAGs: One Config Powers Every Ticker
Adding one stock ticker needed a full code review.
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
- ✓TaskFlow and task mapping basics
- ✓YAML or JSON config management
- ✓DagBag testing with pytest
- Dynamic DAGs generate pipelines from config files or API lists instead of hardcoded per-ticker DAG files
- Key components are DAG factories, config validation, task mapping with expand, and map_index joins
- Performance insight: 120 generated DAGs parse in 11 seconds with lazy imports; duplicated files took 38 seconds and broke weekly
- Production insight: one ticker should never need a PR; config changes deploy without code review or scheduler restarts
Hardcoding one DAG per stock ticker is like printing a separate menu for every customer instead of one menu with choices. A config-driven factory is the single menu: add a dish to the list and every table sees it. Task mapping is the kitchen cooking each ordered dish in parallel from that one menu.
A new ticker meant a new file, a review, and a deploy. Twelve tickers meant twelve copies of the same bug.
You'll replace copies with a factory. One config lists tickers, one function builds DAGs, mapping fans out tasks.
We cover generated DAGs versus mapped tasks, validation, and the anti-patterns that turn factories into fog. Adding a ticker becomes a config edit.
Config changes. Code rests.
Two Kinds of Dynamic: Generated DAGs and Mapped Tasks
Generated DAGs vary DAG count from config: one ticker becomes one DAG. Mapped tasks vary task count inside one DAG via expand.
Use factories when owners or schedules differ per entity. Use mapping when one DAG handles many homogeneous items like tickers in one batch.
Config-Driven DAG Factories: The Ticker Pattern
A YAML list of tickers feeds a loop that builds one @dag per entry with derived table names and tags. No per-ticker files remain.
Validate slugs, enforce unique dag_ids, and keep the factory file side-effect free. Adding NVDA is one YAML line plus CI green.
Task Mapping With Expand
expand() fans one task over a ticker list inside a single DAG. One batch DAG handles 50 tickers with per-ticker map indexes.
Failed tickers retry alone while siblings succeed. The batch stays green-ish with clear per-ticker lineage instead of all-or-nothing.
Split args with partial for constants and expand for mapped ones: add.partial(y=10).expand(x=[1,2,3]) fans three tasks sharing y. Classic operators use the same verbs as classmethods, but task_id, pool, queue, and most BaseOperator args can't map and must ride in partial. expand takes keyword args only, and cross-product is the default: two mapped kwargs multiply instances. Reach for expand_kwargs with a list of dicts for explicit sets, or zip / XComArg.zip with fillvalue to pair iterables positionally. Combine upstream lists with .concat, reshape them pre-map with .map (raising AirflowSkipException inside skips just that instance), and map whole @task_group bodies when a multi-step unit repeats.
Map Index and Downstream Joins
Each mapped task carries a map_index identifying its ticker. Downstream joins aggregate the list of results with none_failed rules.
Inspect per-ticker logs via map_index in Grid view. Joins must tolerate partial skips when one ticker has no data for the interval.
Cap fan-out deliberately: max_map_length (1024 default) bounds instances per task, while max_active_tis_per_dagrun and max_active_tis_per_dag throttle how many run at once. An upstream returning [] maps zero instances: the task skips and downstream follows trigger rules, which by default skips too, so set the join rule explicitly. Read mapped XComs by index with ti.xcom_pull(task_ids=[...], map_indexes=[2]), and never set TriggerRule.ALWAYS on task-generated mapping; the scheduler rejects it at parse time. Repeated mapping chains mapped outputs into the next expand, and Grid view groups instances under task_id[n] with per-index logs.
Validation and Testing of Generated DAGs
Test factories with DagBag: assert every ticker yields a DAG, ids are unique, and each DAG has no import error.
Run airflow dags test on two representative tickers in CI. Config schema validation with pydantic catches bad slugs before the scheduler does.
The Generated-DAG Anti-Patterns
Do not generate DAGs from live API calls at parse time; the scheduler will hammer the API every 30 seconds. Snapshot the list to a file.
Do not branch per ticker inside factories with different SLAs crammed together. Split factories by owner when schedules diverge.
A third trap: divergent SLAs inside one mapped batch. Mapping assumes homogeneous work; when tickers need different owners or deadlines, the factory pattern with separate DAGs wins back. Validate that too: assert per-entity schedule and owner fields in config tests before the scheduler ever sees them.
The Ticker That Needed a Code Review
expand() mapping for per-ticker tasks. Adding NVDA is a one-line config change with CI validation via DagBag uniqueness tests, so it doesn't need a DAG-code PR. Duplicated files were deleted, parse time fell from 38 to 11 seconds, and table names derive from ticker slugs programmatically.- Generate DAGs from config lists; never copy files per entity.
- Validate generated DAG ids and table names in CI before deploy.
- Prefer expand mapping for task counts and factories for DAG counts.
| File | Command / Code | Purpose |
|---|---|---|
| dags | from airflow.decorators import dag, task | Config-Driven DAG Factories |
| dags | from airflow.decorators import dag, task | Task Mapping With Expand |
| tests | from airflow.models import DagBag | Validation and Testing of Generated DAGs |
Key takeaways
Common mistakes to avoid
4 patternsCopying DAG files per ticker
Calling live APIs at DAG parse time
Using mapping for divergent SLAs
Skipping factory validation
Interview Questions on This Topic
How do you add a new ticker without a code deploy?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
That's Airflow. Mark it forged?
3 min read · try the examples if you haven't