A variable is a key and a string (often JSON) for settings that change without a deploy: thresholds, e-mail addresses, feature flags. Like connections, variables come from a secrets backend, AIRFLOW_VAR_<KEY> environment variables or the database (airflow variables set booknest_min_orders 100 stored this one). booknest_vars fails the day if too few orders arrived:
@task
def check_volume(ds=None) -> str:
minimum = int(Variable.get("booknest_min_orders")) # at run time, not parse time
orders = PostgresHook(postgres_conn_id="booknest_pg").get_first(
"SELECT count(*) FROM orders WHERE order_ts::date = %s", parameters=(ds,))[0]
if orders < minimum:
raise ValueError(f"only {orders} orders on {ds}, expected {minimum}")
return f"{orders} orders on {ds}, minimum {minimum}"
notify = BashOperator(
task_id="notify",
bash_command="echo 'report goes to {{ var.value.booknest_alert_email }}'")$ airflow variables get booknest_alert_email data-alerts@example.com $ python dags/booknest_vars.py Done. Returned value was: 195 orders on 2026-06-30, minimum 100 report goes to data-alerts@example.com
The e-mail address came from AIRFLOW_VAR_BOOKNEST_ALERT_EMAIL in the compose file, so airflow variables list, which reads only the database, omits it. Read variables inside tasks or templates ({{ var.value.key }}, {{ var.json.key }}), never at the top of a DAG file: the DAG processor re-parses files every 30 seconds or so, and each top-level lookup is a round trip. Airflow 129 's own settings follow the same pattern: AIRFLOW__SECTION__KEY overrides airflow.cfg, and airflow config get-value shows the result.