One of the most crucial steps of the analytics journey is indeed the last one: the communication. The way you share your results will inevitably impact on the value perceived by your audience.


This follows up into an obvious 2-step process:

  1. To clearly identify your audience. It could be a marketing team, a support team or your company management.
  2. To know your audience and adapt your communication to it. Different groups could have diferent needs.

I had some audience which really prefer an email to receive valuable information. And I don’t mean an attached document, which usually becomes impossible to find when it’s needed – I mean the email body.

My aim with this post is to show a way to create, schedule and send emails to briefly show to your internal audience the key indicators of a report in a attractive and responsive manner. The report itself will be uploaded to a file sharing platform so the email will just be a brief summary with the key insights and a button to download the report.

Please note that this is not an alternative to customer-oriented massive emailing products as mailchip.

The stack

  • mjml: a framework for design responsive html emails. This will allow a maximum email clients compatibility at minimum effort. I’ll create a mjml template for each report.
  • jinja2: the famous templating language for python. This is how I’ll add dynamic capabilities to a static mjml template.
  • owncloud: this is where I actually will upload the full report.
  • airflow: a workflow framework.
  • (Optional) A subject line assistant tool, I use this one.
Stack to create and schedule the email reports.

Design the mjml template

This is a straightforward step if you have worked before with web frontend frameworks, as bootstrap for instance. The mjml site includes a very good documentation, tester, examples, etc.

The key point is to create a mjml template using jinja2 syntax (as in flask or django) to build the dynamic parts of the report.


   <!-- HEADER -->

   <!-- Body -->
     {% for kpi,value in REPORT_KPIS %}
	 <mj-text>{{kpi}}: {{value}}</mj-text>
	 {% endfor %}


(Optional) Write a catchy subject line

If you want to include emojis, you can use a subject line assistant tool to encode the subject.

Example of subject line encoding with emojis.

Let’s automate with a DAG

As I mentioned before I’ll use Airflow to run and manage this workflow. I’m going to create a DAG with implements all the needed tasks to generate the report, the email summary and send the email. I’ll also schedule this report so it’s going to be automatically run each month.

Workflow view as an Airflow DAG.

Report creation & validation

It is not necessary to change these tasks, just make sure to leave the output report file (pdf, csv, excel, etc.) in a handy place.

Adding a validation box could prevent sending wrong reports when there is some kind of anomaly in the data.

Upload the report to owncloud

I use a private instance of owncloud to share my reports. With the python client library for owncloud it’s easy to write a custom airflow hook/operator, so this task is as simple as:

upload_to_owncloud_task = OwncloudUploadFileOperator(

Note that it also stores the report public url in a xcom variable (it will be used later).

Extract report KPIs and generate media files

This task is very dependent on each use case, but in essence it has two aims:

  • Create a dictionary with the report KPIs and data wich will be used to render the mjml template. I’ll also store this dictionary in a xcom variable.
  • Create, if necessary, additional media to be inserted in the email (in my case just images).

Upload the media to a webserver

Usually I create a parent directory in the web server dedicated for the report and a subdirectory for each execution. Then I store the media in this folder with a convenient name so it can easily rendered with jinja2.


<mj-column width='500px'>
  <mj-image padding='10' alt='cpu_chart' align='center'   src='{{REPORT_PATH}}/cpu_chart.png'>

Generate the email html body

Given the report dictionary and the public url to download it from owncloud, both created in previous tasks, now I’ll render the template to create the final html content.

from jinja2 import Template, filters
from commands import getoutput

# Get the public url into the dictionary

# Render the mjml doc
with open(MJML_TEMPLATE, 'r') as myfile:
  # load the template as a string and create a jinja2 template
  template = Template(

  # render the template
  # _REPORT_DATA is a dictionary with the kpis, urls, etc...
  rendered = template.render(_REPORT_DATA)

   # save the rendered mjml file
   with open(FILE_PREFIX+".mjml", "w") as text_file:

   # use mjml binary to render the html
   getoutput("/usr/bin/mjml -r %s -o %s" % \

Send the email

Finally send the email with the html body and a plain text alternative:

from smtplib import SMTP
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

with open(HTML_FILE, 'r') as html_content:
   # Create a multipart message
   msg = MIMEMultipart('alternative')

   # Use the enconded subject line written above
   msg['Subject'] = "your subject line goes here"
   msg['From'] = me
   msg['To'] = ", ".join(DESTINATIONS)

   # Create the body of the message (a plain-text
   # and an HTML version).
   plain_alternative = "Your plain text alternative goes here"

   # Record the MIME types of both parts
   # - text/plain and text/html.
   part1 = MIMEText(plain_alternative, 'plain')
   part2 = MIMEText(, 'html')

   # Attach parts into message container.

   # Send the message via local SMTP server.
   smtp.sendmail(me, DESTINATIONS, msg.as_string())


Once created the workflow it can be easily replicated to be used in other reports with diferentes data, templates and schedules. In my experience, it has supposed a high improvement of the effectiveness of the communication and the audience engagement.

That’s how an example mail looks like in MS Outlook 2010.

Example mail in MS Outlook 2010.

3 thoughts on “Engaging your audience with email attractive contents (mjml + jinja2 + owncloud + airflow)

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s