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') }}"
),
)| run | ds | params.region | payout_min_amount | XCom from sum_payouts | rendered command |
|---|---|---|---|---|---|
| scheduled__2026-03-01 | 2026-03-01 | all | 20 | 41872 | payout-report --day 2026-03-01 --region all --min 20 --hosts 41872 |
| scheduled__2026-03-02 | 2026-03-02 | all | 20 | 43105 | payout-report --day 2026-03-02 --region all --min 20 --hosts 43105 |
| scheduled__2026-03-03 | 2026-03-03 | all | 25 | 42710 | payout-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 template | What it renders to | For the 2026-03-01 run |
|---|---|---|
| {{ ds }} | the window's start date | 2026-03-01 |
| {{ data_interval_start }} | the window's start, as a datetime | 2026-03-01T00:00:00+00:00 |
| {{ data_interval_end }} | the window's end | 2026-03-02T00:00:00+00:00 |
| {{ run_id }} | the run's id | scheduled__2026-03-01T00:00:00+00:00 |
| {{ params.region }} | this run's param | all |
| {{ var.value.payout_min_amount }} | a variable, as a string | 20 |
| {{ var.json.payout_report.sender }} | a field of a JSON variable | finance@example.com |
| {{ conn.warehouse.host }} | a field of a connection | wh.internal |
| {{ ti.xcom_pull(task_ids='sum_payouts') }} | an XCom from this run | 41872 |
| {{ macros.ds_add(ds, -1) }} | a helper: the date 1 day earlier | 2026-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
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.