Home DevOps Airflow Dynamic DAGs: One Config Powers Every Ticker
Advanced 3 min · September 04, 2026
Airflow Dynamic DAGs and Task Mapping

Airflow Dynamic DAGs: One Config Powers Every Ticker

Adding one stock ticker needed a full code review.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 30 min
  • TaskFlow and task mapping basics
  • YAML or JSON config management
  • DagBag testing with pytest
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
✦ Definition~90s read
What is Airflow Dynamic DAGs and Task Mapping?

Dynamic DAGs build pipelines from config or API lists, using factories for DAG counts and expand mapping for task counts.

Hardcoding one DAG per stock ticker is like printing a separate menu for every customer instead of one menu with choices.
Plain-English First

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.

📊 Production Insight
Factories fit per-ticker SLAs.
Mapping fits one batch SLA.
Rule: DAGs differ, maps repeat.
🎯 Key Takeaway
Factories scale DAG counts.
Mapping scales task counts.
Pick by ownership.

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.

dags/ticker_factory.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import yaml
from airflow.decorators import dag, task
from datetime import datetime

with open("dags/tickers.yaml") as f:
    TICKERS = yaml.safe_load(f)["tickers"]

def make_ticker_dag(symbol: str):
    @dag(dag_id=f"ticker_{symbol.lower()}_daily", schedule="@daily",
         start_date=datetime(2026, 1, 1), catchup=False, tags=["equities"])
    def ticker_pipeline():
        @task
        def fetch():
            return {"symbol": symbol}

        @task
        def store(payload: dict):
            return {"stored": payload["symbol"]}

        store(fetch())
    return ticker_pipeline()

for _symbol in TICKERS:
    globals()[f"ticker_{_symbol.lower()}_daily"] = make_ticker_dag(_symbol)
📊 Production Insight
Factory cut 12 files to 2.
Parse time fell 38s to 11s.
Rule: config lists, not copies.
🎯 Key Takeaway
One factory builds all tickers.
Slugs derive names safely.
Config edits ship tickers.

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.

dags/ticker_batch.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from airflow.decorators import dag, task
from datetime import datetime

@dag(schedule="@daily", start_date=datetime(2026, 1, 1), catchup=False, tags=["equities"])
def ticker_batch():
    @task
    def load_ticker(symbol: str):
        return {"symbol": symbol, "rows": 1500}

    @task(trigger_rule="none_failed")
    def join_loads(results: list):
        return {"tickers": len(results)}

    symbols = ["AAPL", "NVDA", "MSFT", "TSLA", "AMZN"]
    join_loads(load_ticker.expand(symbol=symbols))

ticker_batch()
# add.partial(y=10).expand(x=[1, 2, 3])  # constants in partial, mapped in expand
# expand_kwargs([{...}, {...}]) for explicit sets; .zip(fillvalue=...) pairs XComArgs
# max_active_tis_per_dagrun=8 throttles the fan-out; max_map_length caps at 1024
📊 Production Insight
50 tickers mapped in one DAG.
One failure retried solo in 2 min.
Rule: map homogeneous work.
🎯 Key Takeaway
Expand fans tasks cleanly.
Failures isolate per ticker.
Batch simply.

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.

📊 Production Insight
map_index found the bad ticker fast.
Unmapped logs hid it for hours.
Rule: join on none_failed.
🎯 Key Takeaway
Indexes identify tickers.
Joins aggregate lists.
Tolerate partial data.

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.

tests/test_dag_factory.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from airflow.models import DagBag

def test_factory_builds_all_tickers():
    bag = DagBag(dag_folder="dags/", include_examples=False)
    assert not bag.import_errors, bag.import_errors
    ids = [d for d in bag.dags if d.startswith("ticker_")]
    assert len(ids) >= 5
    assert len(ids) == len(set(ids))

def test_ticker_dag_shape():
    bag = DagBag(dag_folder="dags/", include_examples=False)
    dag = bag.get_dag("ticker_nvda_daily")
    assert dag is not None
    assert len(dag.tasks) == 2
📊 Production Insight
DagBag tests caught 3 bad slugs.
Untested factory broke 120 DAGs once.
Rule: test the factory, not files.
🎯 Key Takeaway
Bag tests validate generation.
Two-ticker smoke covers shape.
Schema first.

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.

⚠ Never Call APIs at Parse Time
Top-level requests.get in a factory runs on every scheduler heartbeat and rate-limits your vendor. Cache the ticker list to YAML on a schedule and generate from the file.
📊 Production Insight
Parse-time API calls hit 429 daily.
Cached YAML ended the throttling.
Rule: files generate, jobs fetch.
🎯 Key Takeaway
Snapshot lists to files.
Factories read files only.
Split by owner.
● Production incidentPOST-MORTEMseverity: high

The Ticker That Needed a Code Review

Symptom
The equities team covered 12 tickers with 12 nearly identical DAG files. Adding NVDA meant copying aapl_daily.py, renaming strings, and opening a PR. Review took 2 days, and a find-replace typo changed the AAPL table name in the shared template. The next nightly run wrote 3 days of AAPL rows into the NVDA table and overwrote 3 months of history. Rollback meant 12 file reverts, and it wasn't a logic bug at all.
Assumption
The team assumed explicit files were clearer than factories and that copy-paste was safe with review. Each ticker had drifted slightly, so nobody trusted a shared template. Config-driven generation felt too magical for production data.
Root cause
Duplicated DAGs multiplied every bug by ticker count with no single source of truth. No config list existed, so discovery meant grepping filenames. No factory validation meant typos reached the scheduler. Task counts were static, so scaling tickers scaled files instead of maps.
Fix
A tickers.yaml list now drives a DAG factory that generates one DAG per ticker with validated ids, plus 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.
Key lesson
  • 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.
Production debug guideMissing DAGs, bad ids, and map failures — with exact commands.4 entries
Symptom · 01
New ticker DAG missing from the UI
Fix
Check tickers.yaml syntax with python -c import yaml. List parsed DAGs with airflow dags list | grep ticker. Fix the slug and run airflow dags show ticker_nvda_daily.
Symptom · 02
Generated DAG fails import with duplicate id
Fix
Audit ids with airflow dags list | sort | uniq -d. Enforce slug regex in the factory. Test with pytest tests/test_dag_factory.py -v.
Symptom · 03
Mapped tasks fail for one ticker only
Fix
Open Grid map_index for the failed ticker. Reproduce with airflow tasks test ticker_batch load_ticker --map-index 7 manual__2026-09-01. Fix the per-ticker partition logic.
Symptom · 04
Factory change breaks all generated DAGs
Fix
Validate with airflow dags test ticker_aapl_daily 2026-09-01 on two tickers. Roll back the factory commit and re-run python dags/ticker_factory.py to time parses.
Generated DAGs vs Mapped Tasks Compared
PatternScalesBest when
DAG factoryDAG count from configPer-ticker owners or schedules
expand mappingTask count in one DAGHomogeneous batch work
Static filesNothing, copies multiplyNever for entities
API at parseRate-limit errorsNever, snapshot first
Validated configSafe ticker addsAlways with factories
partial + expandConstants plus mapped argsMap everything blindly
expand_kwargs / zipSets or paired inputsAccidental cross-products
Mapped task groupMulti-step unit repeatsSingle-task tunnel vision
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
dagsticker_factory.pyfrom airflow.decorators import dag, taskConfig-Driven DAG Factories
dagsticker_batch.pyfrom airflow.decorators import dag, taskTask Mapping With Expand
teststest_dag_factory.pyfrom airflow.models import DagBagValidation and Testing of Generated DAGs

Key takeaways

1
Generate DAG counts from config factories and task counts with expand. partial/expand_kwargs/zip/concat cover every fan-out shape. partial/expand_kwargs/zip/concat cover every fan-out shape.
2
One ticker add must be a config edit, never a code copy.
3
Validate ids and shapes with DagBag tests in CI.
4
Never call live APIs at parse time; snapshot to files. Throttle with max_active_tis_per_dagrun under the 1024 max_map_length ceiling. Throttle with max_active_tis_per_dagrun under the 1024 max_map_length ceiling.
5
Split factories by owner when SLAs diverge.

Common mistakes to avoid

4 patterns
×

Copying DAG files per ticker

Symptom
One typo corrupts a neighbor ticker table and rollback needs 12 reverts.
Fix
Drive one factory from tickers.yaml with derived names.
×

Calling live APIs at DAG parse time

Symptom
Scheduler hits vendor 429 every 30 seconds and parses slow.
Fix
Snapshot API lists to YAML on a schedule; factories read files. Snapshot entity lists to YAML; factories read files, never sockets. Snapshot entity lists to YAML; factories read files, never sockets.
×

Using mapping for divergent SLAs

Symptom
One ticker SLA change forces batch-wide retries and pages.
Fix
Use factories with per-ticker DAGs when SLAs differ. Mapping fits homogeneous batches; throttle with max_active_tis_per_dagrun and split factories by owner. Mapping fits homogeneous batches; throttle with max_active_tis_per_dagrun and split factories by owner.
×

Skipping factory validation

Symptom
Duplicate dag_ids break all generated DAGs at once.
Fix
Add DagBag uniqueness tests and slug regex checks in CI.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How do you add a new ticker without a code deploy?
Q02SENIOR
When would you use expand mapping versus a DAG factory?
Q03SENIOR
How do you keep dynamic DAGs safe at 120 DAGs?
Q01 of 03JUNIOR

How do you add a new ticker without a code deploy?

ANSWER
Append it to tickers.yaml. The factory generates the DAG and CI validates ids, so no PR on DAG code is needed.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is a DAG factory?
02
What does expand do?
03
How do I debug one mapped ticker?
04
Can factories read from an API?
05
Factory or mapping for 5 tickers, one owner?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

3 min read · try the examples if you haven't

Previous
Airflow dbt ELT Orchestration
18 / 37 · Airflow
Next
Airflow Backfill and Catchup in Depth