Cosmos (github.com/astronomer/astronomer-cosmos (https://github.com/astronomer/astronomer-cosmos 1,273 ), Apache 2.0, by Astronomer 149,584 ) reads a dbt 37,942 project and builds an Airflow 129 DAG (DbtDag) or task group (DbtTaskGroup) from it. The current release, 1.15.1 (4 August 2026), supports Airflow 3 since 1.10. It joins Airflow's own environment, so install it against Airflow's constraints file; dbt keeps its own virtual environments (cosmos/Dockerfile, built on the image of The Compose Stack):
FROM l2/airflow-booknest:3.3.2
COPY constraints-3.13.txt dbt-2.0.6-cp311-abi3-manylinux_2_28_x86_64.whl /tmp/
RUN pip install --no-cache-dir "apache-airflow==3.3.2" "astronomer-cosmos==1.15.1" \
-c /tmp/constraints-3.13.txt \
&& python -m venv /opt/airflow/dbt2_venv \
&& /opt/airflow/dbt2_venv/bin/pip install --no-cache-dir /tmp/dbt-2.0.6-*.whlFour small objects configure it: ProjectConfig (where the project is), ProfileConfig (how dbt connects), ExecutionConfig (where and with which dbt it runs) and RenderConfig (what to render, and how). A profile mapping builds dbt's profiles.yml from an Airflow connection, so dbt's credentials live with Airflow's (Connections and Testing):
PROJECT = ProjectConfig("/opt/airflow/dbt") # Chapter 4's dbt project
PG_PROFILE = ProfileConfig( # dbt credentials from an Airflow connection
profile_name="booknest", target_name="pg",
profile_mapping=PostgresUserPasswordProfileMapping(
conn_id="booknest_pg", profile_args={"schema": "analytics"}))
DBT_CORE = ExecutionConfig(dbt_executable_path="/opt/airflow/dbt_venv/bin/dbt") # 1.12.5
booknest_dbt_cosmos = DbtDag(
dag_id="booknest_dbt_cosmos",
project_config=PROJECT,
profile_config=PG_PROFILE,
execution_config=DBT_CORE,
render_config=RenderConfig(load_method=LoadMode.DBT_LS), # dbt ls decides nodes and order
schedule=None,
tags=["ch07", "cosmos"],
)LoadMode.DBT_LS runs dbt ls while the DAG file is parsed, so dbt's own selection logic decides the nodes; DBT_MANIFEST reads a manifest.json built in CI instead, and CUSTOM parses the SQL without dbt. Cosmos caches the dbt ls result, so later parses are cheap:
$ airflow dags report -o plain cosmos/booknest_dbt_cosmos.py 0:00:00.380985 1 14 ['booknest_dbt_cosmos'] $ airflow tasks list booknest_dbt_cosmos customers_snapshot_snapshot dim_book.run dim_book.test ... fct_sales.run fct_sales.test genre_groups_seed stg_books_run ...
A model with tests becomes a group with run and test tasks, a model without tests a single _run task, and seeds and snapshots get their own operators. Each task also declares an Airflow asset per model; under Airflow 3 Cosmos writes their URIs with slashes (duckdb:///.../main/fct_sales), not the dots Airflow 2 accepted.