An email at the end of a daily DAG is a simple way to tell people the data is ready. The EmailOperator sends an HTML message through an SMTP connection, and its subject and body are templates, so they can include the date and values returned by earlier tasks.

Copy and paste

1. Install the SMTP provider, if it is not installed yet:

pip install apache-airflow-providers-smtp

2. Create the connection. For a server that uses port 587 and STARTTLS:

export AIRFLOW_CONN_SMTP_DEFAULT='{"conn_type": "smtp", "host": "smtp.example.com", "port": 587, "login": "reports@example.com", "password": "app-password", "extra": {"from_email": "reports@example.com", "disable_ssl": true}}'

For a server that uses port 465 and SSL, set "port": 465 and leave out "disable_ssl".

3. Send the report:

import pendulum
from airflow.sdk import dag, task
from airflow.providers.smtp.operators.smtp import EmailOperator

@dag(schedule="0 7 * * *", start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), catchup=False)
def sales_report():
    @task
    def summarize(ds=None) -> dict:
        # query your warehouse here
        return {"orders": 1240, "revenue": 58310.5}

    summary = summarize()

    send = EmailOperator(
        task_id="send_report",
        to=["sales-team@example.com"],
        subject="Daily sales {{ ds }}",
        html_content="""
        <h2>Sales for {{ ds }}</h2>
        <ul>
          <li>Orders: {{ ti.xcom_pull(task_ids='summarize')['orders'] }}</li>
          <li>Revenue: {{ '{:,.2f}'.format(ti.xcom_pull(task_ids='summarize')['revenue']) }}</li>
        </ul>
        """,
        conn_id="smtp_default",
    )

    summary >> send

sales_report()

The message for 15 May arrives with the subject Daily sales 2026-05-15 and lists Orders: 1240 and Revenue: 58,310.50.

The connection settings

Setting Where Default
Sender address extra.from_email, or from_email= on the operator none
SSL from the start (port 465) on, unless extra.disable_ssl is true on
STARTTLS (port 587) on, unless extra.disable_tls is true on
Timeout in seconds extra.timeout 30
Attempts on connection errors extra.retry_limit 5

Port 587 needs "disable_ssl": true: with SSL left on, the client tries to start an SSL session on a port that expects plain text followed by STARTTLS. Many providers, Gmail included, require an app password instead of the account password.

Things to know

Tested with Airflow 3.3.2 and SMTP provider 3.0, against a local SMTP server.

References