Daily jobs usually load the day that just ended. In Airflow 3, the run scheduled at midnight on
15 May has ds = 2026-05-15, so yesterday is the logical date minus one day. Take it from the run,
not from the clock.
Copy and paste
In a template:
"{{ macros.ds_add(ds, -1) }}" # 2026-05-14
"{{ (logical_date - macros.timedelta(days=1)) | ds_nodash }}" # 20260514
In a BashOperator:
from airflow.providers.standard.operators.bash import BashOperator
load = BashOperator(
task_id="load_yesterday",
bash_command="python load.py --date {{ macros.ds_add(ds, -1) }}",
)
In a TaskFlow task:
from airflow.sdk import task
@task
def load(logical_date=None):
yesterday = logical_date.subtract(days=1).to_date_string() # '2026-05-14'
Yesterday in your local timezone:
yesterday = logical_date.in_timezone("Europe/Rome").subtract(days=1).to_date_string()
logical_date is a pendulum DateTime, so subtract, in_timezone and to_date_string are
available. A plain logical_date - timedelta(days=1) works as well.
Why not yesterday_ds
Airflow 2 offered yesterday_ds, yesterday_ds_nodash, tomorrow_ds, prev_ds and next_ds.
Airflow 3 removed them, together with execution_date, and a template that still uses one fails when
the task starts:
jinja2.exceptions.UndefinedError: 'yesterday_ds' is undefined
Replace yesterday_ds with macros.ds_add(ds, -1) and tomorrow_ds with macros.ds_add(ds, 1).
Why not datetime.now()
A task that computes yesterday from the clock gives a different answer when it starts late, when a
retry runs after midnight, or when someone reruns it next week to fix the data. The logical date
belongs to the run, so macros.ds_add(ds, -1) returns the same day for the same run every time,
backfills included.
Is ds already yesterday?
It depends on the schedule. With the default schedule of Airflow 3, ds is the day of the run. In
Airflow 2, and in Airflow 3 with CronDataIntervalTimetable, the midnight run of 15 May has
ds = 2026-05-14: it is already yesterday, and ds_add(ds, -1) would go back two days.
| Schedule | ds of the run at 00:00 on 15 May |
Yesterday |
|---|---|---|
"0 0 * * *" in Airflow 3 (default) |
2026-05-15 | macros.ds_add(ds, -1) |
CronDataIntervalTimetable, or Airflow 2 |
2026-05-14 | ds |
Why your DAG processes yesterday’s data explains the two models.
Tested with Airflow 3.3.2.