DECLARE IT Which slice? one run per slice Has it landed? held until ready How many arrived? the scheduler counts Go, or stop? you set the wait policy in a task · after the run started in the schedule · before the run exists 36 / 104
outlet_events[sales].extra = {"day": "2026-03-10"} # the scheduler schedules on this outlet_events[sales].add_partitions("2026-03-10") Same object. One is a dict the scheduler never reads. One is what it schedules on. 37 / 104
Assets — datasets became assets, and the model was reworked underneath, explicitly to make room for partitions. 75 · New Asset-Centric Syntax — @asset, so a Dag can be written data-first instead of task-first. 40 / 104
the timetable the task Decided when before the run exists while the run executes Producer API CronPartitionTimetable PartitionedAtRuntime Use it when the key follows the cadence the key comes from the data 45 / 104
live_region_stats(self, outlet_events): outlet_events[self].add_partitions("tw") For a watermark read from the source, a late file, a key from the payload. 48 / 104
def multi_region_stats(self, outlet_events): outlet_events[self].add_partitions(["tw", "jp", "us"]) Each key gets its own event. Duplicate keys collapse to one. 49 / 104
minutes DayWindow twenty-four hours WeekWindow seven days MonthWindow every day of the month QuarterWindow every day of the quarter YearWindow every day of the year SegmentWindow every key you declare 75 / 104
summarize(*, triggering_asset_events, dag_run=None): day = dag_run.partition_key # "2026-03-10" hours = [e.partition_key for e in triggering_asset_events[hourly_sales]] 77 / 104
┐ ├── ┘ FixedKeyMapper("all_countries") ──→ all_countries default_partition_mapper=RollupMapper( upstream_mapper=FixedKeyMapper("all_countries"), window=SegmentWindow(["tw", "jp", "us"]), ) Hold until every declared region arrives, then fire once. 81 / 104
— where keys come from AirflowPlugin.timetables a partition mapper — how keys map AirflowPlugin.partition_mappers a window — what one period contains AirflowPlugin.windows 85 / 104
* * * *")): ... # 02-18 00:00, 01:00, 02:00 ... a list of ticks with DAG("daily_sales_summary", schedule=PartitionedAssetTimetable(assets=hourly_sales)): ... # no ticks at all Backfill asks for a list of runs. Only one of these can answer. 89 / 104
--dag-id ingest_hourly_sales \ --from-date 2026-02-18 \ --to-date 2026-02-20 Airflow detects the Dag is partitioned and creates one run per partition. 90 / 104
producer — CronPartitionTimetable. Nothing downstream changes yet. Then · one consumer on PartitionedAssetTimetable. Default mapper, nothing to configure. The one real hazard: partitioned events do not trigger non-partition-aware Dags. Move a producer and its consumers together. 95 / 104
disagree on grain StartOf*Mapper · partition_mapper_config one slice is a region and a day ProductMapper a period is many upstream slices RollupMapper + a window …and one of them is sometimes late MinimumCount on that rollup one coarse input drives many fine runs FanOutMapper + max_downstream_keys a fixed set of names, not a time grain SegmentWindow + FixedKeyMapper 96 / 104
team_b_stats, partition_mapper_config={ combined_stats: StartOfYearMapper(), team_a_stats: StartOfHourMapper(), }, ) 2026 can never equal 2026-03-10T09. No error. No warning. No log line. 98 / 104