Templated Fields

Concepts covered: Templating and params

A templated field is an operator argument whose string Airflow treats as a Jinja template: anything inside {{ }} is an expression, evaluated against the run's context, and the result replaces it. The DAG file holds one task with --day {{ ds }}; each run gets a task instance whose command carries its own date. Rendering happens on the worker, at the start of the try, after the task instance exists and before its code executes, which is why a template can reach XComs pushed by upstream tasks of the same run and the current value of a variable.

from airflow.providers.standard.operators.bash import BashOperator

report = BashOperator(
    task_id="payout_report",
    bash_command=(
        "payout-report"
        " --day {{ ds }}"
        " --region {{ params.region }}"
        " --min {{ var.value.payout_min_amount }}"
        " --hosts {{ ti.xcom_pull(task_ids='sum_payouts') }}"
    ),
)
rundsparams.regionpayout_min_amountXCom from sum_payoutsrendered command
scheduled__2026-03-012026-03-01all2041872payout-report --day 2026-03-01 --region all --min 20 --hosts 41872
scheduled__2026-03-022026-03-02all2043105payout-report --day 2026-03-02 --region all --min 20 --hosts 43105
scheduled__2026-03-032026-03-03all2542710payout-report --day 2026-03-03 --region all --min 25 --hosts 42710

4 values in that command came from 4 different places, and the template reached all of them with one syntax. ds is the run's own date, the start of the window it covers, as a YYYY-MM-DD string. params.region is this run's param. var.value.payout_min_amount fetches the variable as the task runs, which is why the 2026-03-03 run renders 25. ti.xcom_pull(...) reads the count sum_payouts pushed in this run, so the command line carries a number computed 20 minutes earlier by another task, on another worker.

In a templateWhat it renders toFor the 2026-03-01 run
{{ ds }}the window's start date2026-03-01
{{ data_interval_start }}the window's start, as a datetime2026-03-01T00:00:00+00:00
{{ data_interval_end }}the window's end2026-03-02T00:00:00+00:00
{{ run_id }}the run's idscheduled__2026-03-01T00:00:00+00:00
{{ params.region }}this run's paramall
{{ var.value.payout_min_amount }}a variable, as a string20
{{ var.json.payout_report.sender }}a field of a JSON variablefinance@example.com
{{ conn.warehouse.host }}a field of a connectionwh.internal
{{ ti.xcom_pull(task_ids='sum_payouts') }}an XCom from this run41872
{{ macros.ds_add(ds, -1) }}a helper: the date 1 day earlier2026-02-28

Which arguments get rendered

Only some arguments are templates. Each operator lists the names it renders in a tuple called template_fields, and for BashOperator that tuple holds bash_command and env plus cwd. A {{ ds }} in any other argument, or in task_id, stays as 8 literal characters. In a @task function, strings passed as arguments to its call are rendered, because they travel as the operator's op_kwargs, which is a templated field, while a string written inside the function body is Python source, which the renderer never reads. The next example prints both, and the 2026-03-01 run prints 2026-03-01 {{ ds }}. Declaring a ds=None parameter is the cleaner route to the same value.

@task
def report(day):
    inside = "{{ ds }}"
    print(day, inside)

report("{{ ds }}")
from datetime import datetime
from datetime import timedelta
from airflow.sdk import dag, task
@dag(
    schedule=timedelta(days=1),
    start_date=datetime(2026, 3, 1),
    params={"region": "all"},
)
def payout_report():
    @task
    def sum_payouts():
        return 41872

    @task
    def report(day, region, hosts):
        print(day, region, hosts)

    report(
        "{{ ds }}",
        "{{ params.region }}",
        sum_payouts(),
    )
payout_report()
  • Scheduled runs only
  • Plus a manual run for fr
3 nightly runs and, in the second variant, a manual run whose conf picks fr at 14:00 on the 2nd; each run's report task renders its own ds and region at the start of its try.

The UI keeps the result. Open a task instance and pick Rendered Templates: it shows every templated field with the string the worker produced for that run, which is the fastest check that {{ params.region }} rendered to fr rather than all, or that the XCom pull found a number rather than None.

About this lesson

This section is part of the Tasks and Data: Beginner lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.