Variables and Configuration

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:

dags/booknest_vars.py (excerpt): a variable in Python and in a templatePython
    @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 }}'")
Output
$ 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.