How to send emails with SQL and Python: send your first email in 5 minutes

TL;DR: Using a Fabi.ai Smartbook, start by querying your data using SQL. Then using SQL or Python, build strings, Pandas or Polars DataFrames and figures that you want to share with your team. Using the Fabi.ai email cell, you can then embed those in an email using a simple templating approach {{your_dataframe}}.

Ever needed to send regular analytics reports but found yourself frustrated with rigid, screenshot-filled emails from BI tools? We recently launched a new feature in Fabi.ai that make it incredibly easy to build and send emails just using SQL and Python.

In this tutorial, we’ll show you how to send your first email. If you prefer video tutorials, you can follow along here:

Why standard BI email reports often fall short

Most automated reports from BI platforms have some annoying limitations that reduce their effectiveness:

  • They're basically just screenshots you have to squint at, making them particularly difficult to read on mobile devices (and we know that most execs who are the ones asking for this are reading emails on their phones)
  • You can't really customize them much beyond basic filtering options. Reports are much more interesting if the insights pops out and it’s not up to the consumer to figure out what to make of the data
  • There's no way to add conditional formatting or dynamic content based on data thresholds. For example, a well-placed emoji can go a very long way to helping get a point across 🔥

This means your carefully analyzed data often ends up ignored in someone's inbox because it's too cumbersome to extract meaningful insights.

A better way: Python + SQL for custom report emails

The combination of Python's flexibility and SQL's data querying power creates opportunities for much more useful reports. Instead of sending generic dashboards, you can craft emails that automatically highlight exactly what stakeholders need to know.

Email send using SQL and Python with different indicators based on conditions.

Let's walk through how to build a weekly usage report that automatically highlights the most important changes in your data.

Step 1: Query your data

First, pull in your data with a simple SQL query in a Fabi.ai Smartbook:

SELECT * FROM users_data

This example uses a CSV with user information, creation dates, widget counts, and week-over-week changes. The beauty of starting with SQL is that you can transform and filter your data right at the query level before it even reaches your Python processing.

Step 2: Add visual elements

Create a chart to give your recipients a visual overview of trends:

import pandas as pd
import plotly.express as px
from datetime import datetime

# Create a copy of the dataframe to avoid modifying the original
df = dataframe1.copy()

# Extract month and year from the User creation date
df['Month-Year'] = df['User creation date'].dt.strftime('%Y-%m')

# Group by month-year and prospect/customer status, count users
user_counts = df.groupby(['Month-Year', 'Prospect or customer']).size().reset_index(name='Count')

# Create the stacked bar chart
fig = px.bar(
    user_counts, 
    x='Month-Year', 
    y='Count', 
    color='Prospect or customer',
    title='Total Number of Users Created Over Time',
    labels={'Month-Year': 'Month-Year', 'Count': 'Number of Users'},
    color_discrete_sequence=['#FF8A80', '#80D8FF'],
    width=800,
    height=450
)

# Improve layout
fig.update_layout(
    xaxis_title='Month-Year',
    yaxis_title='Number of Users',
    legend_title='User Type',
    barmode='stack'
)

# Display the chart
fig.show()

Visual elements like charts provide immediate context that numbers alone can't convey. They help recipients quickly grasp patterns and trends without having to analyze rows of data.

Step 3: Highlight key insights

This is where things get interesting. You can use Python to identify and format your most important metrics.

First let’s filter the data on April users:

import pandas as pd
import numpy as np

# Convert the dataframe1 to a pandas DataFrame if it's not already
df = dataframe1.copy()

# Convert User creation date to datetime if it's not already
if not pd.api.types.is_datetime64_any_dtype(df['User creation date']):
    df['User creation date'] = pd.to_datetime(df['User creation date'])

# Filter for users created in April 2025
april_2025_users = df[df['User creation date'].dt.year == 2025]
april_2025_users = april_2025_users[april_2025_users['User creation date'].dt.month == 4]

Then let’s look for the week-over-week change and add an emoji based on whether the change is positive or negative.

# Function to add emojis based on percentage change
def add_emoji(value):
    if value > 0:
        return f"{value}% ↗️ 🟢"  # Up arrow and green dot for increase
    else:
        return f"{value}% ↘️ 🔴"  # Down arrow and red dot for decrease

# Apply the emoji function to the percentage column
april_2025_users['Formatted Change'] = april_2025_users['WoW widget creation diff (%)'].apply(add_emoji)

This conditional formatting immediately draws attention to what's changed since the last report. The visual indicators make it easy to spot positive and negative trends at a glance, rather than forcing recipients to compare numbers manually. You could even enhance this by using Python to detect and flag anomalies. Tons of potential for advanced analysis and reporting.

And then finally we find the top users with highest and lowest changes:

# Create dataframes for top 10 increases and decreases
top_increases = april_2025_users.sort_values('WoW widget creation diff (%)', ascending=False).head(15)

# Filter for only negative WoW changes before sorting for decreases
negative_changes = april_2025_users[april_2025_users['WoW widget creation diff (%)'] < 0]
top_decreases = negative_changes.sort_values('WoW widget creation diff (%)', ascending=True).head(15)

# Select and rename columns for better readability
columns = ['User ID', 'Name', 'Number of widgets created', 'Formatted Change', 'Prospect or customer']
top_increases = top_increases[columns].rename(columns={'Formatted Change': 'WoW Change'})
top_decreases = top_decreases[columns].rename(columns={'Formatted Change': 'WoW Change'})

With that last piece, we now have two DataFrames: top_increases and top_decreases.

Step 4: Compose your email

Now put it all together with a few lines of Python:

Hey there,

Here are the top users in terms of increased widget creation:
{{top_increases}}

Here are the top users in terms of decreased widget creation:
{{top_decreases}}

Users created by month:
{{fig}}
Fabi.ai email cell

What makes this approach powerful is that your email content isn't static. It changes based on what's happening in the data. If there's a sudden spike in negative changes, that will be front and center. If a particular customer segment is showing unusual activity, you can have Python automatically highlight that segment.

Step 5: Send and schedule

In Fabi.ai, you can:

  1. Preview your email to make sure it looks right
  2. Send a test to yourself to check formatting across devices
  3. Set up a schedule for regular delivery (daily, weekly, monthly)
  4. Specify different recipient lists based on report content

Once scheduled, these reports run without any manual intervention, saving hours of work while delivering more valuable insights.

The real value of custom Python email reports

Beyond the technical advantages, this approach fundamentally changes how your organization interacts with data:

1. Improved decision-making

When reports highlight what matters most, recipients can make faster, better-informed decisions. Instead of digging through data to find issues, they see immediately where attention is needed.

2. Higher engagement with analytics

People are much more likely to engage with reports that are easy to understand and relevant to their specific needs. By customizing content based on roles or departments, you can ensure everyone gets exactly what they need.

3. Time savings across the organization

This approach saves time for both report creators and recipients:

  • Report creators set up the process once rather than manually creating reports
  • Recipients spend less time trying to interpret dense data visualizations
  • Follow-up questions decrease because the most important insights are already highlighted

4. More proactive problem-solving

When reports automatically flag concerning trends, teams can address issues before they become serious problems. This shifts your organization from reactive to proactive data use.

Getting started tips

If you're ready to try this yourself:

  • Start with just one or two key metrics rather than trying to replicate an entire dashboard
  • Use colors or symbols to make trends immediately obvious, but don't overdo it – visual clarity is key
  • Include both tables and charts for better context, as different stakeholders prefer different formats, but don’t put too much in a single email
  • Test thoroughly before scheduling anything to ensure data is accurate and formatting works across email clients. Especially if you’re using conditional formatting, think about the different scenarios that might cause unexpected results
  • Ask recipients for feedback and iterate on your design based on what they find most useful

Not only is it easier to build better, more advanced data insights than you can in legacy BI with Fabi.ai Smartbooks, but sending these insights via email puts your work front-and-center and increases the engagement with data.

Give it a try!

This approach has saved me hours of manually creating reports while actually delivering more useful information. The combination of Python's flexibility with SQL's data access makes for powerful, automated reporting that people actually want to read.

The best part is that once you've created one successful report, you can easily adapt the pattern for other metrics and departments. Your data becomes more accessible and actionable across the entire organization.

You can send your first email out in less than 5 minutes for free! And if you have any feedback or questions, please feel free to reach out: hello@fabi.ai

Related reads

Subscribe to Query & Theory