Astronomer Cosmos

Astronomer Cosmos: Rendering dbt as Airflow Tasks

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):

cosmos/Dockerfile: Cosmos in Airflow's environment, dbt v2 in its ownDockerfile
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-*.whl

Four 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):

include/booknest_cosmos.py and dags_cosmos/booknest_dbt_cosmos.py (excerpts)Python
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:

Output of 30
$ 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.