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.

References